[轉]File Descriptor泄漏導致Crash: Too many open files,descriptorcrash
在實際的Android開發過程中,我們遇到了一些奇奇怪怪的Crash,通過sigaction再配合libcorkscrew以及一些第三方的Crash Reporter都捕獲不到發生Crash的具體資訊,十分頭疼。然後我們通過Bugly上報的Java的CallStack觀察發現這些Crash發現了一些共同的資訊:
看來是和OpenGL有關係,於是我們進一步對程式輸出的log進行觀察,又發現:
從這個log裡面我們獲得了幾個資訊:
我們對我們已有的裝置反覆實驗,確實了只有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-write10 (/dev/log/main): cloexec write-only11 (/dev/log/radio): cloexec write-only12 (/dev/log/events): cloexec write-only13 (/dev/log/system): cloexec write-only14 (/system/framework/framework-res.apk):15 (/system/framework/core-libart.jar):16 (pipe:[282578]): nonblock17 (/dev/alarm):18 (/dev/cpuctl/tasks): cloexec write-only19 (/dev/cpuctl/bg_non_interactive/tasks): cloexec write-only20 (socket:[282569]): read-write21 (pipe:[282570]):22 (pipe:[282570]): write-only23 (pipe:[282578]): nonblock write-only24 (anon_inode:[eventpoll]): read-write25 (/data/app/---app_name---/base.apk):26 (/data/data/---app_name---/databases/bugly_db): cloexec read-write27 (socket:[285047]): read-write28 (anon_inode:mali-8938): cloexec29 (socket:[282605]): nonblock read-write30 (socket:[283605]): nonblock read-write31 (/dev/null): read-write32 (/dev/ump): read-write33 (socket:[285045]): nonblock read-write34 (/dev/null): read-write35 (/dev/mali): read-write36 (anon_inode:mali-8938): cloexec37 (anon_inode:mali-8938): cloexec38 (/data/app/---app_name---/base.apk):39 (anon_inode:mali-8938): cloexec40 (anon_inode:mali-8938): cloexec41 (/dev/null): read-write42 (/dev/null): read-write43 (/data/app/---app_name---/base.apk):44 (/dev/null): read-write45 (anon_inode:mali-8938): cloexec46 (/data/data/---app_name---/files/DefaultFont.ttf):47 (/data/app/---app_name---/base.apk):48 (anon_inode:sync_fence):49 (/dev/null): read-write50 (socket:[285060]): cloexec read-write52 (anon_inode:mali-8938): cloexec53 (anon_inode:mali-8938): cloexec54 (/dev/null): read-write55 (anon_inode:sync_fence):56 (pipe:[284134]): write-only58 (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的耗盡如同記憶體的耗盡一樣會導致程式的各種異常情況發生,但是前者不如後者那麼知名也不如後者容易被察覺。