【團隊分享】蒼翼之刃:論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上再次測試的時候,發現了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)”