1. 程式人生 > >【團隊分享】蒼翼之刃:論File Descriptor洩漏如何導致Crash?

【團隊分享】蒼翼之刃:論File Descriptor洩漏如何導致Crash?


蒼翼之刃

外文:BlazBlue RevolutionReburning,是由Arc Systerm Works 正式授權,並由 91Act 負責研發的橫板動作手遊,森利道全程監修,也是蒼翼默示錄系列唯一的一部橫板動作手遊。

遊戲內部代號為:BBRR,全稱是BLAZBLUE Revlution Reburning,續BBCT、BBCS、BBCS2、BBEX、BBCP 之後的又一力作。

背景

在*nix系統中,許多的資源都會被定義為File Descriptor(下面簡稱FD),例如普通檔案、socket、std in/out/error等等。每個*nix系統中,單個程序可以使用的FD數量是有上限的。不同的*nix系統中,這個上限各有區別,例如在Android裡面這個上限被限制為1024。

案例分析

在實際的Android開發過程中,我們遇到了一些奇奇怪怪的Crash,通過sigaction再配合libcorkscrew以及一些第三方的Crash Reporter都捕獲不到發生Crash的具體資訊,十分頭疼。

然後我們通過Bugly上報的Java的CallStack觀察發現這些Crash發現了一些共同的資訊:


看來是和OpenGL有關係,於是我們進一步對程式輸出的log進行觀察,又發現:


從這個log裡面我們獲得了幾個資訊:

1.   幾乎所有出現這種Crash的裝置,都是Adreno的GPU

2.   幾乎所有Crash都會伴隨著requestBufferfailed

我們對我們已有的裝置反覆試驗,確實了只有Adreno

的裝置(小米3,HTC M8,華為P7等)會在特定條件下出現這種奇奇怪怪的隨機Crash。而其他裝置例如小米Pad(Tegra),三星S3(Mali)等都不會出現這種問題。

這個問題確實頭疼,在網上搜索了很久也沒找到有用的資訊。直到在某次小米3上再次測試的時候,發現了log裡面還有一條必然出現的資訊:

E/MemoryHeapBase(18703): error creatingashmem region: Too many open files

這個資訊間接的指出了問題,也給了我們一些提示:似乎打開了過多的檔案。於是靠著這個靈光,我們嘗試著在程式中輸出所有已開啟的檔案:

SHOWFILE HANDLES:
 0 (socket:[285038]): read-write
 1 (/dev/null): read-write
 2  (/dev/null):read-write
 3 (/dev/log/main): cloexec write-only
 4 (/dev/log/radio): cloexec write-only
 5 (/dev/log/events): cloexec write-only
 6 (/dev/log/system): cloexec write-only
 7 (/sys/kernel/debug/tracing/trace_marker): write-only
 8  (/dev/__properties__):
 9 (/dev/binder): cloexec read-write
10 (/dev/log/main): cloexec write-only
11 (/dev/log/radio): cloexec write-only
12 (/dev/log/events): cloexec write-only
13 (/dev/log/system): cloexec write-only
14 (/system/framework/framework-res.apk):
15 (/system/framework/core-libart.jar):
16 (pipe:[282578]): nonblock
17 (/dev/alarm):
18 (/dev/cpuctl/tasks): cloexec write-only
19 (/dev/cpuctl/bg_non_interactive/tasks): cloexec write-only
20 (socket:[282569]): read-write
21 (pipe:[282570]):
22 (pipe:[282570]): write-only
23 (pipe:[282578]): nonblock write-only
24 (anon_inode:[eventpoll]): read-write
25 (/data/app/---app_name---/base.apk):
26 (/data/data/---app_name---/databases/bugly_db): cloexec read-write
27 (socket:[285047]): read-write
28 (anon_inode:mali-8938): cloexec
29 (socket:[282605]): nonblock read-write
30 (socket:[283605]): nonblock read-write
31 (/dev/null): read-write
32 (/dev/ump): read-write
33 (socket:[285045]): nonblock read-write
34 (/dev/null): read-write
35 (/dev/mali): read-write
36 (anon_inode:mali-8938): cloexec
37 (anon_inode:mali-8938): cloexec
38 (/data/app/---app_name---/base.apk):
39 (anon_inode:mali-8938): cloexec
40 (anon_inode:mali-8938): cloexec
41 (/dev/null): read-write
42  (/dev/null):read-write
43 (/data/app/---app_name---/base.apk):
44 (/dev/null): read-write
45 (anon_inode:mali-8938): cloexec
46 (/data/data/---app_name---/files/DefaultFont.ttf):
47 (/data/app/---app_name---/base.apk):
48 (anon_inode:sync_fence):
49 (/dev/null): read-write
50 (socket:[285060]): cloexec read-write
52 (anon_inode:mali-8938): cloexec
53 (anon_inode:mali-8938): cloexec
54 (/dev/null): read-write
55 (anon_inode:sync_fence):
56 (pipe:[284134]): write-only
58 (anon_inode:sync_fence):
62 (anon_inode:sync_fence):
63 (anon_inode:sync_fence):

通過不停測試程式,發現已開啟的檔案數量一直有增無減,而當這些被開啟的檔案數量接近1024的時候,上面的eglSwapBuffers必然出錯。於是乎我們得出一箇中間結論:

·        如果程式開啟的檔案數量過多,會導致OpenGL swap buffer失敗!

這從字面上看著似乎有些扯淡,因為這兩者總感覺沒啥聯絡。這個問題只會出現在Adreno的GPU上面,於是我們猜想:

·        Adreno的驅動在swap buffer的時候,需要申請新的FD,這個FD可能是某些硬體IO,具體不得而知;

·        如果程式中其他的各種FD使用過多接近上限,會導致Adreno的驅動申請不到必要的FD,因此導致swap buffer失敗。

這樣看起來似乎就比較有道理了。雖然sawpbuffer本身是不會Crash的,他並沒有raise任何signal,只是簡單的返回了一個錯誤的結果,但這會導致上層邏輯出現異常。這些異常在不同的裝置上表現不一樣:

·        有的裝置會在Java層的eglSwapBuffers觸發Java層的Exception導致Crash;

·        有的裝置不會出現異常,但是會導致OpenGL停止工作(halt rendering),其表現結果就是程式卡住無響應;

·        有的裝置可能什麼都不會發生,但是如果你的互動觸發了其他邏輯:比如按回退鍵彈出對話方塊,對話方塊也需要FD,但是獲得不到,那麼彈出對話方塊的邏輯將丟擲異常,

於是這就有了各種奇奇怪怪的Crash

解決方案

通過對程式碼的排查,我們發現在使用SoundPool處理音效的時候,確實存在FD洩露的情況:

1 private SoundPool m_soundPool;
2  publicint loadSound(String path) {
3     int soundID = m_soundPool.load(getAssets().openFd(path), 0);
4     return soundID;
5  }
6 public unloadSound(int soundID) {
7     m_soundPool.unload(soundID);
8  }

雖然我們在不需要這些音效的時候,對其進行了解除安裝處理,但不知道是SoundPool類自身的缺陷,還是我們的使用不當,在實際測試中我們發現unload過後,在load中通過openFd開啟的FD並沒有被釋放掉。

強制呼叫System.gc()在一些裝置(例如小米3)上可以釋放掉這部分FD,但是另一些裝置(例如HTC M8)即使強制gc這無法解除安裝掉它們,於是便出現了FD洩露的情況。

最終我們自行對這些FD進行管理,並且在unload的時候手動呼叫這些FD的close方法:

1 private SoundPool m_soundPool;
 2 private HashMap<Integer, AssetFileDescriptor> m_soundFdMap
 3 public int loadSound(String path) {
 4     AssetFileDescriptor fd = getAssets().openFd(path);
 5     int soundID = m_soundPool.load(fd, 0);
 6     m_soundFdMap.put(soundID, fd);
 7     return soundID;
 8  }
 9  public unloadSound(int soundID) {
10    m_soundPool.unload(soundID);
11    m_soundFdMap.get(soundID).close();
12  }

這之後FD再無洩露的情況發生,之前的各種裝置上面的各種奇奇怪怪的Crash都被處理好了。

小結

這個問題粗略說起來就是:因為播放了太多的音效,導致Adreno底層渲染失敗,以至於上層邏輯各種失措,產生了很多奇奇怪怪的Crash。

準確的解釋應該是:程式中的FD洩露如同記憶體洩露一樣是同樣需要得到關注的問題,FD的耗盡如同記憶體的耗盡一樣會導致程式的各種異常情況發生,但是前者不如後者那麼知名也不如後者容易被察覺

小編有話說

不總結哪來經驗,不分享經驗何用?

在此小編號召大家多總結,互分享,踴躍給我們投稿,把自己踩過並爬出來的坑樹個指示牌警醒後人,讓猿們的開發生活更加美好!

投稿方式:將文章和個人介紹郵件到 [email protected],字數不限。

本文系騰訊Bugly特邀文章,轉載請註明作者和出處“騰訊Bugly(http://bugly.qq.com)”