【轉載】Fd leak in Android
寫得很好。
FD(File Descriptor)檔案描述符在形式上是非負整數,它是一個索引值,指向核心為每個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在Linux系統中,一切裝置都視作檔案,檔案描述符為Linux平臺裝置相關的程式設計提供了一個統一的方法。
在stability測試的過程中,經常會出現許多FD洩漏導致的莫名其妙的FC,而crash的堆疊也是千奇百怪, 可能出現在應用層、framework層、Native層,其中以framework層居多。 所以當出現這個問題以後往往認為是framework出現問題了, 實際上從後面Debug的結果來看許多都是應用出現了問題。 同一個問題也會經常出現不同的堆疊。這就是FD洩漏的一個重要的特性,問題出現的不確定性。
FD作為檔案控制代碼的例項,可以用來表示一個開啟的檔案,一個開啟的網路流(socket),管道或者資源(如記憶體塊),輸入輸出(in/out/error)。 在Linux系統中,每個程序可以使用的FD數量是有上限的,在Android中這個上限為1024,表示每個程序可以建立的file descriptors 不能超多1024個。可以通過下面兩種方式檢視:
- ulimit -a
- cat /proc/sys/fs/file-max
通過使用ulimit –n 2048 可以臨時提高程序可以擁有的file為2048個。
相比較傳統的記憶體洩漏,FD洩漏在大部分情況下不會出現記憶體不足的情況,所以出現問題的時候會更加隱晦。由於發生FD洩漏的時候記憶體可能不會出現不足,所以不會出發系統的GC操作,導致只有通過crash程序的方式去自我恢復。事實上在很多情況下,就算觸發系統GC,也不一定能夠回收已經建立的控制代碼檔案。
Android中FD洩漏的幾種型別
1. Resource related
Android應用可能會需要很多資源,像輸入輸出流,資料庫資源Cursor, Binder裝置。如果沒能夠很好的處理這些資源,不僅可能造成記憶體的洩漏,也可能會出現FD洩漏。
輸入輸出
輸入輸出流的使用在任何程式中都會比較頻繁,像FileInputStream,FileOutputStream,FileReader,FileWriter 等輸入輸出如果不斷建立但是不及時關閉,不僅可能造成記憶體的洩露了也可能會造成FD的溢位。每次new一個FileInputStream、FileOutputStream 都會在程序中建立一個FD, 用來指向這個開啟的檔案,而如果反覆執行下面的程式碼,FD檔案會持續不斷地增加,直至超過1024出現FC。
String filename = prefix + "temp";
File file = new File(getCache(),fileName);
try{
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
} catch (FileNotFoundException e){
} catch (IOException e){
}
在/proc/${程序id}/fd/ 目錄下執行ls –l檢視到增加的FD指向建立的檔案,這裡建立了不同的file,即使是對同一個檔案,也會建立多個FD來指向這個開啟的檔案流。
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 80 -> /data/data/com.example.hu.memleakdemo/cache/48temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 81 -> /data/data/com.example.hu.memleakdemo/cache/49temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 82 -> /data/data/com.example.hu.memleakdemo/cache/50temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 83 -> /data/data/com.example.hu.memleakdemo/cache/51temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 84 -> /data/data/com.example.hu.memleakdemo/cache/52temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 85 -> /data/data/com.example.hu.memleakdemo/cache/53temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 86 -> /data/data/com.example.hu.memleakdemo/cache/54temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 87 -> /data/data/com.example.hu.memleakdemo/cache/55temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 88 -> /data/data/com.example.hu.memleakdemo/cache/56temp
lr-x—— u0_a86 u0_a86 2015-06-20 01:25 89 -> /data/data/com.example.hu.memleakdemo/cache/57temp
最終導致應用程序出現FC,並打出如下的Log, 表示這個程序FD數量已經到達了上限,無法再建立新的FD,只有終止程序。
E/Parcel ( 3601): dup failed in Parcel::read, fd 1 of 2
E/Parcel ( 3601): dup(1020) = -1 [errno: 24 (Too many open files)]
E/Parcel ( 3601): fcntl(1020, F_GETFD) = 1 [errno: 24 (Too many open files)]
E/Parcel ( 3601): flat 0x0 type 0
正確的做法是能夠在final中將流進行關閉,這樣無論中途是否出現異常導致程式中斷,都會將流順利關閉。
String filename = prefix + "temp";
File file = new File(getCache(),fileName);
try{
file.createNewFile();
FileOutputStream out = new FileOutputStream(FileDescriptor. file );
}catch(Exception e){
}
final{
if(out != null){
out.close();
}
}
Cursor leak
與輸入輸出相似,資料庫查詢的Cursor如果沒有及時進行Close,也會出現FD洩漏的情況。 如下面這段異常的Log:
AndroidRuntime: FATAL EXCEPTION: IntentService[ContactSaveService]
AndroidRuntime: Process: com.android.contacts,
AndroidRuntime: android.database.sqlite.SQLiteException: unable to open database file (code 14)
AndroidRuntime: at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:179)
AndroidRuntime: at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:135)
AndroidRuntime: at android.content.ContentProviderProxy.delete(ContentProviderNative.java:544)
AndroidRuntime: at android.content.ContentResolver.delete(ContentResolver.java:1330)
AndroidRuntime: at com.android.contacts.ContactSaveService.saveContact(ContactSaveService.java:478)
AndroidRuntime: at com.android.contacts.ContactSaveService.onHandleIntent(ContactSaveService.java:222)
AndroidRuntime: at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:66)
AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:102)
AndroidRuntime: at android.os.Looper.loop(Looper.java:148)
AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:61)
這個問題是在Stability測試環境下出現的,由於未能夠在Cursor使用後及時進行關閉,最終出現了FD的溢位。 從上面FC的Log可以看到,異常出現在ContentProvider跨程序傳遞傳遞的時候,出現了異常,顯示無法開啟database檔案。看到unable to open database file,大膽地猜測是否是出現了FD的洩漏。然後在Log中找到了下面這段,確定了是FD洩漏造成了這次的FC。
E/JavaBinder( 3319): java.lang.RuntimeException: Could not write CursorWindow to Parcel due to error -24.
E/JavaBinder( 3319): at android.database.CursorWindow.nativeWriteToParcel(Native Method)
E/JavaBinder( 3319): at android.database.CursorWindow.writeToParcel(CursorWindow.java:705)
E/JavaBinder( 3319): at android.database.BulkCursorDescriptor.writeToParcel(BulkCursorDescriptor.java:63)
E/JavaBinder( 3319): at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:127)
E/JavaBinder( 3319): at android.os.Binder.execTransact(Binder.java:453)
而對於Cursor沒有及時關閉這個問題,下面這種情況很容易造成開發者的疏忽,導致出現問題:
public void problemMethod() {
Cursor cursor = query(); // 假設 query() 是一個查詢資料庫返回 Cursor 結果的函式
if (flag == false) { // 出現了提前返回
return;
}
cursor.close();
}
2.Thread related
HandlerThread
下面這段異常Log是在Monkey測試中出現的,所以沒有明確的操作步驟,測試指令碼提取的crash堆疊如下,顯示的是無法分配JNI資源,看上去是一個典型了記憶體洩漏問題。
12-28 21:35:21.571 E/AndroidRuntime(11308): FATAL EXCEPTION: main
12-28 21:35:21.571 E/AndroidRuntime(11308): Process: com.tct.fmradio, PID: 11308
12-28 21:35:21.571 E/AndroidRuntime(11308): java.lang.OutOfMemoryError: Could not allocate JNI Env
12-28 21:35:21.571 E/AndroidRuntime(11308): at java.lang.Thread.nativeCreate(Native Method)
12-28 21:35:21.571 E/AndroidRuntime(11308): at java.lang.Thread.start(Thread.java:1063)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.tct.fmradio.platform.QcomFMDeviceImpl.createRecordSinkThread(QcomFMDeviceImpl.java:391)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.tct.fmradio.platform.QcomFMDeviceImpl.<init>(QcomFMDeviceImpl.java:295)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.tct.fmradio.device.FMDeviceImpl.createFMDevice(FMDeviceImpl.java:18)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.tct.fmradio.service.FMService.onCreate(FMService.java:1231)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.app.ActivityThread.handleCreateService(ActivityThread.java:2918)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.app.ActivityThread.access$1900(ActivityThread.java:157)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1457)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.os.Handler.dispatchMessage(Handler.java:102)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.os.Looper.loop(Looper.java:148)
12-28 21:35:21.571 E/AndroidRuntime(11308): at android.app.ActivityThread.main(ActivityThread.java:5509)
12-28 21:35:21.571 E/AndroidRuntime(11308): at java.lang.reflect.Method.invoke(Native Method)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
12-28 21:35:21.571 E/AndroidRuntime(11308): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
12-28 21:35:21.572 I/ActivityManager( 6054): handleApplicationCrash
12-28 21:35:21.572 I/JRDRecordService( 6054): jrdCrashHandler invoke ytf…
但是在檢視Log以後,在前面找到了下面的Log,發現原來是FD發生了洩漏。具體表現為開啟FMRadio後,點選Recent按鍵500~600次, /proc//fd/目錄下fd檔案數量不斷增長,直到超過1024閥值發生FC.
12-28 21:35:21.569 E/art (11308): ashmem_create_region failed for ‘indirect ref table’: Too many open files 12-28 21:35:21.570 W/art (11308): Throwing OutOfMemoryError “Could not allocate JNI Env” 12-28 21:35:21.570 D/AndroidRuntime(11308): Shutting down VM 12-28 21:35:21.571 W/Adreno-GSL(11308): <gsl_ldd_control:475>: ioctl fd 31 code 0xc0200933 (IOCTL_KGSL_TIMESTAMP_EVENT) failed: errno 24 Too many open files 12-28 21:35:21.571 W/Adreno-GSL(11308): <ioctl_kgsl_syncobj_create:2979>: (20, 7, 46028) fail 24 Too many open files
此問題為FM有一個Service 在每次oncreate都會建立一個handlerthread,並且沒有釋放,而通過Recent方式會反覆的呼叫這個service的oncreate程式碼,造成了洩漏。
public void onCreate() {
Log.i(TAG, "onCreate()");
super.onCreate();
mDefaultName = getString(R.string.default_name_text);
// create a thread that messages will be processed on
new HandlerThread("FMService");
thread.start();
mMessenger = new FMMessenger(thread.getLooper(), mOnActionListener,null, null);
.... ...
}
通過在onDestroy新增下面語句,即可釋放handlerthread所佔用的控制代碼
mHandlerThread.quitSafely();
在Android中使用執行緒,尤其是HandlerThread要尤其的謹慎,必須要確保建立HandlerThread的函式不會被反覆的呼叫導致執行緒反覆的被建立。 如果迴圈呼叫下面這段程式碼幾百次,就會出現FD洩漏。
HandlerThead handerThread = new HandlerThead("test");
handlerThead.start();
在不需要執行緒Loop的時候呼叫HandlerThead.quitSafely()或者HandlerThead.quit();銷燬loop,釋放控制代碼資源。
Thread
HandlerThread實際上是帶有Loop的thread,而對於傳統的Java Thread,需要宣告Loop以後才會出現FD的增加。因為宣告Loop相當於增加了一塊緩衝區,需要有一個FD來標識。如果反覆呼叫下面這段程式碼也會出現FD洩漏。
Thread thread = new Thread (new Runnable(){
@Override
public void run(){
Looper.prepare();
// do things
Looper.loop();
}
});
thread.start();
如果確定不需要Looper,可以使用Looper.quit()或者Looper.quitSafely()來退出looper,避免出現FD洩漏。
3.Input channel file realted
WindowManager.addview
公司的同一個手機專案的的stability測試中,出現了兩個crash的log,堆疊幾乎完全不一樣,但是後面發現原來都是沒有及時呼叫WindowManager.removeView造成的。 第一份log如下:
process:com.android.systemui Crash happen at 2016-07-07 19:15:48 process:com.android.systemui pid:2438 Classname:java.lang.RuntimeException Filename:InputChannel.java Methodname:nativeReadFromParcel LineNumber:-2 Cause:Could not read input channel file descriptors from parcel. stackTrace: java.lang.RuntimeException: Could not read input channel file descriptors from parcel. at android.view.InputChannel.nativeReadFromParcel(Native Method) at android.view.InputChannel.readFromParcel(InputChannel.java:148) at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:759) at android.view.ViewRootImpl.setView(ViewRootImpl.java:550) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86) at com.android.systemui.assist.AssistManager.onConfigurationChanged(AssistManager.java:119) at com.android.systemui.statusbar.phone.PhoneStatusBar$11.onVerticalChanged(PhoneStatusBar.java:962) at com.android.systemui.statusbar.phone.NavigationBarView.notifyVerticalChangedListener(NavigationBarView.java:699) at com.android.systemui.statusbar.phone.NavigationBarView.onSizeChanged(NavigationBarView.java:690) at android.view.View.sizeChange(View.java:16892) at android.view.View.setFrame(View.java:16854) at android.view.View.layout(View.java:16770) at android.view.ViewGroup.layout(ViewGroup.java:5488) at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2190) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1950) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1126) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6333) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858) at android.view.Choreographer.doCallbacks(Choreographer.java:670) at android.view.Choreographer.doFrame(Choreographer.java:606) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5473) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
可以看到上面的Log是在Parcel傳遞的時候出現了異常。這個Bug問題出在沒有能夠處理好onConfigurationChanged中的程式碼。 onConfigurationChanged程式碼中出現了WindowManager.addView反覆呼叫,卻缺少對應的removeView匹配的情況。 WindowManager.addView每次呼叫,都會在server(WindowManagerService)和Client(使用者程序)端建立fd檔案來作為socket通訊,如果不呼叫removeView這個fd將得不到釋放。事實上,如果SystemServer所在的程序的FD數量超過1024個,還會造成Android的重啟。
而第二份crash的log如下:
pid:2494 Classname:java.lang.RuntimeException Filename:Bitmap.java Methodname:nativeCreateFromParcel LineNumber:-2 Cause:Could not allocate dup blob fd. stackTrace: java.lang.RuntimeException: Could not allocate dup blob fd. at android.graphics.Bitmap.nativeCreateFromParcel(Native Method) at android.graphics.Bitmap.access$100(Bitmap.java:36) at android.graphics.Bitmap$1.createFromParcel(Bitmap.java:1516) at android.graphics.Bitmap$1.createFromParcel(Bitmap.java:1508) at android.app.ActivityManager$TaskThumbnail.readFromParcel(ActivityManager.java:1390) at android.app.ActivityManager$TaskThumbnail.init>(ActivityManager.java:1411) at android.app.ActivityManager$TaskThumbnail.init>(ActivityManager.java:1359) at android.app.ActivityManager$TaskThumbnail$1.createFromParcel(ActivityManager.java:1403) at android.app.ActivityManager$TaskThumbnail$1.createFromParcel(ActivityManager.java:1401) at android.app.ActivityManagerProxy.getTaskThumbnail(ActivityManagerNative.java:3367) at android.app.ActivityManager.getTaskThumbnail(ActivityManager.java:1418) at com.android.systemui.recents.misc.SystemServicesProxy.getThumbnail(SystemServicesProxy.java:357) at com.android.systemui.recents.misc.SystemServicesProxy.getTaskThumbnail(SystemServicesProxy.java:336) at com.android.systemui.recents.model.RecentsTaskLoader.getAndUpdateThumbnail(RecentsTaskLoader.java:430) at com.android.systemui.recents.model.RecentsTaskLoadPlan.executePlan(RecentsTaskLoadPlan.java:250) at com.android.systemui.recents.model.RecentsTaskLoader.loadTasks(RecentsTaskLoader.java:477) at com.android.systemui.recents.Recents$TaskStackListenerImpl.run(Recents.java:143) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5473) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
這份Log很容易讓人誤以為是Binder傳遞了過大的Bitmap導致的異常。而這個Bug的問題出在沒有處理好ActivityManagerProxy.getTaskThumbnail 的程式碼。 ActivityManagerProxy.getTaskThumbnail會在不斷地點選Recents按鍵的時候被反覆呼叫。而ActivityManagerProxy.getTaskThumbnail需要建立一個Parcel與ActivityManagerService通訊,這時候就會建立一個FD指向Socket的埠,如果此時發現FD已經滿了就會報出異常。
所以兩個問題確實是屬於同一個問題,都屬於FD洩漏。我們在兩份Log中到找到了如下的Log:
2016-07-28 08:39:22,956 : 07-28 08:39:21.856 E/Parcel ( 2490): dup() failed in Parcel::read, i is 1, fds[i] is -1, fd_count is 2, error: Too many open files 2016-07-28 08:39:22,956 : 07-28 08:39:21.856 E/Surface ( 2490): dequeueBuffer: IGraphicBufferProducer::requestBuffer failed: -22 2016-07-28 08:39:22,956 : 07-28 08:39:21.857 I/Adreno ( 2490): DequeueBuffer: dequeueBuffer failed 2016-07-28 08:39:22,956 : 07-28 08:39:21.857 E/Parcel ( 2490): dup() failed in Parcel::read, i is 0, fds[i] is -1, fd_count is 2, error: Too many open files 2016-07-28 08:39:22,956 : 07-28 08:39:21.857 E/Surface ( 2490): dequeueBuffer: IGraphicBufferProducer::requestBuffer failed: -22 2016-07-28 08:39:22,956 : 07-28 08:39:21.857 I/Adreno ( 2490): DequeueBuffer: dequeueBuffer failed 2016-07-28 08:39:22,956 : 07-28 08:39:21.858 E/Parcel ( 2490): dup() failed in Parcel::read, i is 0, fds[i] is -1, fd_count is 2, error: Too many open files 2016-07-28 08:39:22,956 : 07-28 08:39:21.858 E/Surface ( 2490): dequeueBuffer: IGraphicBufferProducer::requestBuffer failed: -22 2016-07-28 08:39:22,956 : 07-28 08:39:21.858 I/Adreno ( 2490): DequeueBuffer: dequeueBuffer failed 2016-07-28 08:39:22,956 : 07-28 08:39:21.858 E/Parcel ( 2490): dup() failed in Parcel::read, i is 0, fds[i] is -1, fd_count is 2, error: Too many open files 2016-07-28 08:39:22,956 : 07-28 08:39:21.858 E/Surface ( 2490): dequeueBuffer: IGraphicBufferProducer::requestBuffer failed: -22 2016-07-28 08:39:22,972 : 07-28 08:39:21.858 I/Adreno ( 2490): DequeueBuffer: dequeueBuffer failed 2016-07-28 08:39:31,458 : 07-28 08:39:30.356 E/InputChannel-JNI( 2490): Error 24 dup channel fd 1015.
Multi-Task
一臺手機在跑Monkey的時候出現的這個問題,錯誤的堆疊資訊如下,看著堆疊,掛在了native層, abort message 發現原來是FD檔案超了
07-14 23:40:49.781 F/libc (10511): Fatal signal 6 (SIGABRT), code -6 in tid 10511 (m.android.email) 07-14 23:40:49.838 F/DEBUG ( 747): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 07-14 23:40:49.838 F/DEBUG ( 747): Build fingerprint: 'Vertu/VM-08/tron:6.0.1/6.0.1_0.184.0.032/china_032:user/release-keys' 07-14 23:40:49.838 F/DEBUG ( 747): Revision: '0' 07-14 23:40:49.838 F/DEBUG ( 747): ABI: 'arm64' 07-14 23:40:49.838 F/DEBUG ( 747): pid: 10511, tid: 10511, name: m.android.email >>> com.android.email <<< 07-14 23:40:49.838 F/DEBUG ( 747): signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr -------- 07-14 23:40:49.889 F/DEBUG ( 747): Abort message: 'FORTIFY: FD_SET: file descriptor >= FD_SETSIZE' 07-14 23:40:49.889 F/DEBUG ( 747): x0 0000000000000000 x1 000000000000290f x2 0000000000000006 x3 0000000000000000 07-14 23:40:49.889 F/DEBUG ( 747): x4 0000000000000000 x5 0000000000000001 x6 0000000000000000 x7 0000000000000000 07-14 23:40:49.889 F/DEBUG ( 747): x8 0000000000000083 x9 525e43451f3c3d1f x10 7f7f7f7f7f7f7f7f x11 0101010101010101 07-14 23:40:49.889 F/DEBUG ( 747): x12 0000007f873b7888 x13 f98a40dee6834299 x14 f98a40dee6834299 x15 00267f6a359179b2 07-14 23:40:49.889 F/DEBUG ( 747): x16 0000007f873b1568 x17 0000007f873442fc x18 0000007f84267000 x19 0000007f877cf088 07-14 23:40:49.889 F/DEBUG ( 747): x20 0000007f877cefc8 x21 0000000000000002 x22 0000000000000006 x23 0000007fd5d595d0 07-14 23:40:49.889 F/DEBUG ( 747): x24 0000000000000413 x25 0000007fd5d595cc x26 0000000000000002 x27 0000007fd5d59bc0 07-14 23:40:49.889 F/DEBUG ( 747): x28 0000007f7ddc2394 x29 0000007fd5d59370 x30 0000007f87341a98 07-14 23:40:49.889 F/DEBUG ( 747): sp 0000007fd5d59370 pc 0000007f87344304 pstate 0000000020000000 07-14 23:40:49.898 F/DEBUG ( 747): 07-14 23:40:49.898 F/DEBUG ( 747): backtrace: 07-14 23:40:49.898 F/DEBUG ( 747): #00 pc 0000000000069304 /system/lib64/libc.so (tgkill+8) 07-14 23:40:49.898 F/DEBUG ( 747): #01 pc 0000000000066a94 /system/lib64/libc.so (pthread_kill+68) 07-14 23:40:49.898 F/DEBUG ( 747): #02 pc 00000000000239f8 /system/lib64/libc.so (raise+28) 07-14 23:40:49.898 F/DEBUG ( 747): #03 pc 000000000001e198 /system/lib64/libc.so (abort+60) 07-14 23:40:49.898 F/DEBUG ( 747): #04 pc 00000000000215e0 /system/lib64/libc.so (__libc_fatal+128) 07-14 23:40:49.898 F/DEBUG ( 747): #05 pc 0000000000021604 /system/lib64/libc.so (__fortify_chk_fail+32) 07-14 23:40:49.898 F/DEBUG ( 747): #06 pc 000000000006fb28 /system/lib64/libc.so (__FD_SET_chk+32) 07-14 23:40:49.898 F/DEBUG ( 747): #07 pc 0000000000000f74 /system/vendor/lib64/libqti-perfd-client.so (mpctl_send+1048) 07-14 23:40:49.898 F/DEBUG ( 747): #08 pc 0000000000000fec /system/lib64/libqti_performance.so 07-14 23:40:49.898 F/DEBUG ( 747): #09 pc 0000000000004968 /system/framework/oat/arm64/QPerformance.odex (offset 0x4000)
我們發現問題出在如下的程式碼上:
if (action == COMPOSE) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
} else if (message != null) {
}
寫一封新郵件,startactivity 使用的flag是multiTask,也就是說,每點選建立新的郵件,都會建立task。而Monkey在跑的時候建立了n個郵件的task, 而對應開啟的ComposeActivityEmail.java 的 “插入快速語” 會建立很多個fd , 最終導致FD超過1024, 程序崩潰。 實際上,通過反覆如下程式碼就會出現這個問題:
Intent intent = new Intent();
Intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
Intent.setClass(MainActivity.this, SecondActivity.class);
startActivity(intent);
應用的input event由WindowManagerService管理,WMS內部會建立一個InputManager,兩者通過InputChannel來完成,WMS需要註冊兩個InputChannel與InputManager連線,其中Server端InputChannel註冊在InputManager(SystemServer),Client端註冊在應用程式主執行緒中。InputChannel使用Ashmem匿名共享記憶體來傳遞資料,它由一個fd檔案描述符指向,同時read端和write端各佔用一個fd。建立一個新的Task時, server(system_server)和client(app)都會構建FD。
所以設定為Intent.FLAG_ACTIVITY_MULTIPLE_TASK類似flag的時候,如果沒有處理好Activity的生命週期,可能會出現system_server程序先於應用程序到達FD上限,造成 Android系統重啟。
如何解決此類問題
FD會發生洩漏的根本原因是沒有對資源進行有效的管理。無論是檔案資源,裝置資源,Socket資源,輸入輸出流還是執行緒等,如果被頻繁的呼叫而沒有即使釋放,甚至根本就沒有釋放,將會使得FD越積越多,最終導致了洩漏的發生。
3.1. 確定問題
這類問題比較容易在Stabiliy、Monkey、JrdLog中出現,當在這些測試中遇到掛在不可思議的地方的時候就可以看看是否可能是FD洩漏引起的問題。
要確定這個問題是否由FD洩漏引起,一般情況下可以通過下面的關鍵字too many files open,ashmem_create_region failed for ‘indirect ref table’: Too many open files,leak , release ……
並不是說有上面的關鍵字,就一定是FD洩漏,JNI物件洩漏也會出現too many的欄位;也並不是說沒有上面的關鍵字就一定不是FD洩漏問題,具體到問題還是需要具體分析。
3.2 探索復現路徑
每個程序建立的FD檔案都在/proc/${問題應用所在程序}/fd/這個目錄下,在這個目錄下執行ls |wc –l 或者在任意adb shell目錄下使用lsof ${問題應用所在程序} |wc –l, 都可以檢視程序的FD檔案數量,如果能夠找到某個或者某些操作能夠讓FD數量穩定的增長,那麼就容易解決。
一般可以嘗試反覆點選Recent, 反覆開啟應用或者Activity,然後按back返回等操作,反覆橫豎切換等。
由於存在1024這個閾值,普通使用者在使用過程中一般不會出現這個問題,出現這個問題一般都是Stability測試或者Monkey出現的。
如果stability這樣的測試中,可以通過編寫指令碼每1 min 抓取一次FD的數量,觀察在什麼時間段FD數量出現了穩定的增長過程,然後再對比期間所做的測試操作,通過分析嘗試去找到手動的復現路徑。
而Monkey測試就沒有什麼規律了,因為測試本身就是亂序的操作過程,要知道復現只能夠通過聯絡Log上下文,檢視操作了哪些Activity,做了哪些操作,哪些Log是反覆列印的,哪些Log出現不正常的現象,然後一個個去嘗試排除。
3.3定位問題程式碼段
一旦有了復現路徑以後,要找到相應出問題的程式碼段的返回就會小了很多。
如果能定位到具體的檔案,可以先檢視一下最近的提交記錄。如果這個問題之前沒有後來才出現,一般都是改出來的問題,按照之前debug經驗來看,這種情況還是佔了大多數。但是有些問題之前沒出現,可能只是沒有被測試來而已,很多原生的程式碼也是會出現問題的。
如果運氣不好遇到這些原生問題,可以重點檢視哪些可能會被反覆呼叫的程式碼段,在Java層面比如說onResume,onConfigurationchanged 之類函式,檢視在這些程式碼段中是否由建立什麼執行緒、資源,而在onPause或者onDestroy沒有被釋放掉的。