在實際的Android開發過程中,我們遇到了一些奇奇怪怪的Crash,通過sigaction再配合libcorkscrew以及一些第三方的Crash Reporter都捕獲不到發生Crash的具體信息,十分頭疼。然后我們通過Bugly上報的Java的CallStack觀察發現這些Crash發現了一些共同的信息:
看來是和OpenGL有關系,於是我們進一步對程序輸出的log進行觀察,又發現:
從這個log里面我們獲得了幾個信息:
- 幾乎所有出現這種Crash的設備,都是Adreno的GPU
- 幾乎所有Crash都會伴隨着requestBuffer failed
我們對我們已有的設備反復試驗,確實了只有Adreno的設備(小米3,HTC M8,華為P7等)會在特定條件下出現這種奇奇怪怪的隨機Crash。而其他設備例如小米Pad(Tegra),三星S3(Mali)等都不會出現這種問題。這個問題確實頭疼,在網上搜索了很久也沒找到有用的信息。直到在某次小米3上再次測試的時候,發現了log里面還有一條必然出現的信息:
E/MemoryHeapBase(18703): error creating ashmem region: Too many open files
這個信息間接的指出了問題,也給了我們一些提示:似乎打開了過多的文件。於是靠着這個靈光,我們嘗試着在程序中輸出所有已打開的文件:
SHOW FILE 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失敗。
這樣看起來似乎就比較有道理了。雖然sawp buffer本身是不會Crash的,他並沒有raise任何signal,只是簡單的返回了一個錯誤的結果,但這會導致上層邏輯出現異常。這些異常在不同的設備上表現不一樣:
- 有的設備會在Java層的eglSwapBuffers觸發Java層的Exception導致Crash;
- 有的設備不會出現異常,但是會導致OpenGL停止工作(halt rendering),其表現結果就是程序卡住無響應;
- 有的設備可能什么都不會發生,但是如果你的交互觸發了其他邏輯:比如按回退鍵彈出對話框,對話框也需要FD,但是獲得不到,那么彈出對話框的邏輯將拋出異常,
於是這就有了各種奇奇怪怪的Crash。
解決方案
通過對代碼的排查,我們發現在使用SoundPool處理音效的時候,確實存在FD泄露的情況:
1 private SoundPool m_soundPool; 2 public int 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的耗盡如同內存的耗盡一樣會導致程序的各種異常情況發生,但是前者不如后者那么知名也不如后者容易被察覺。
轉自:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=28