Android句柄泄漏(Fd leak)排查

8,987 阅读4分钟

背景

句柄泄漏(Fd leak)不是什么新鲜问题了,网上一搜这种问题,有很多种解决办法。 但没有系统性,导致排查问题效率极低。笔者希望通过这一篇文章,帮助大家理清思路,快速解决问题。

排查问题的黄金步骤

  1. 查log
  2. 找复现步骤
  3. 查代码

本文也将按照这几步,逐步深入到问题中。

查log

一般发生这种问题时,堆栈如下:

java.lang.RuntimeException
Could not read input channel file descriptors from parcel.
android.view.InputChannel.nativeReadFromParcel(Native Method)
android.view.InputChannel.readFromParcel(InputChannel.java:148)
android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:804)
android.view.ViewRootImpl.setView(ViewRootImpl.java:770)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3716)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2901)
android.app.ActivityThread.-wrap11(Unknown Source:0)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
android.os.Handler.dispatchMessage(Handler.java:106)
android.os.Looper.loop(Looper.java:173)
android.app.ActivityThread.main(ActivityThread.java:6653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)android.view.InputChannel.nativeReadFromParcel(Native Method)

显然这种堆栈是没什么卵用的,唯一的用处就是,你把这log去网上一搜,大家说这可能是句柄泄漏(Fd leak)问题。

找复现步骤

这一步就有些学问了。 你有2种手段来找到这个问题的复现步骤:

  1. 利用“严格模式”,让app主动输出log,直接定位到发生问题的代码。
  2. 通过纯黑盒的方式,找到发生crash的操作步骤。

利用"严格模式(Strick Mode)"输出日志

严格模式的具体使用,大家可以移步官网:developer.android.com/reference/a…

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }

我们只要加入detectLeakedClosableObjects() penaltyLog()这两个方法,logcat里面搜索“Strick Mode”关键字,就能直接定位到是哪个Closable没有关闭。而往往大量的Closeable没有关闭,会导致句柄泄漏(Fd leak)的发生。

但是,严格模式不是那么的可靠,他并不能帮你发现所有的问题。我就经历过靠严格模式排查了一遍过后,问题依然存在的情况。最后还是靠找到复现步骤、人工查代码解决的。所以接下来的东西也很重要:

找到发生crash的操作步骤

找复现步骤,可以通过静置手机、跑monkey等等手段,“等待”crash的发生。但,这太没效率。本文希望通过以下步骤,缩短这个“等待”的时间

找到一部fd上限低的手机

我们知道导致句柄泄漏(Fd leak)的原因是:当前进程所持有的fd(file descriptor)数量,超过了操作系统所设置的上限。那我们首先要知道发生crash的手机对每个进程设置的fd上限是怎样的。

根据我的调研,华为mate10 fd上限是3万多,这种手机是不大容易复现问题的,上限太高。小米note1,华为p9,上限是1024,这类手机是比较容易复现问题的。另外,从发生crash时前后的logcat来看,有些机型,会在发生句柄泄漏(Fd leak)问题后,动态的调高fd上限。比如小米mix 6。

那怎么找呢?执行

adb shell ulimit -a (无需root)

你将会看到:

nofiles(descriptors)一项所示的数量,就是每个进程可持有的fd上限。

更快一些:在到达fd上限之前,就找到步骤

首先,你需要一部root的手机。 进入adb shell,su,以下命令,可以查看你的app进程所占用的fd数量:

lsof | grep <进程号> | wc -l

你每做一些操作(或者静置手机不动),就运行一下这个命令,对比前后几次fd数量的变化,可以大大缩小问题的范围。

by the way:

每次都运行命令笨不笨呢?有没有更好的办法呢?可以尝试使用linux中的watch命令。例如:

watch -n 2 'adb shell su lsof | grep <进程号> | wc -l'

不过如果直接adb shell su lsof的话,查出来的结果为0。为何adb shell su这命令直接运行,不能正确的给adb提权到root?我也没深究。有经验的朋友可以在评论区回复,笔者不胜感激。

查代码

在进行了上面的排查步骤后,查代码的范围也被大大的缩小了。你可以通过搜索各种“Stream”字段,来看他们有没有正确的关闭。或者,通过删代码这种“做减法”的方式,逐渐定位问题。笔者就是通过“做减法”的方式,定位到是公司内部的一个组件库的问题。