1.简介:
FD(File Descriptor)文件描述符作为一个索引值,用于指向进程内的打开文件。当我们在进程中,打开文件,打开网络流(socket),管道或者其他资源,都会生成文件描述符。然后每个进程中这个值都是有限制的,一般情况下为1024。
2.FD泄露场景
FD泄露与内存泄露有一定的重合,其中第2,5条容易被忽视,在编码中需要特别注意,
- 1.数据库: Cursor使用完未关闭
- 2.输入输出:FileInputStream,FileOutputStream,FileReader,FileWriter 等输入输出使用完未关闭
- 3.HandlerThread: 使用完未调用quit方法。使用时,可以设置name或tag的地方,命名具体一些,便于定位(大部分基本为此泄露)
- 4.Thread: HandlerThread实际上是带有Loop的thread,而对于传统的Java Thread,需要声明Loop以后才会出现FD的增加。因为声明Loop相当于增加了一块缓冲区,需要有一个FD来标识。如果反复调用下面这段代码也会出现FD泄漏。如果确定不需要Looper,可以使用Looper.quit()或者Looper.quitSafely()来退出looper,避免出现FD泄漏。
- 5.WindowManager: WindowManager.addView每次调用,都会在server(WindowManagerService)和Client(用户进程)端创建fd文件来作为socket通信,如果不调用removeView这个fd将得不到释放
- 6.socket: 其他大量使用socket通信,但未关闭的情况
- 7.Dialog:使用完Dialog之后需要主动dismiss。否则也会造成泄露(见附1报错log)
注:JDK7中新增了try-with-resource语法,实现closable的api,cursor或者stream,socket等,都可以使用try with语法自动关闭
4.查看指定进程的文件描述符数量限制
$ adb shell //切换到shell环境
# ps | grep [package_name] //查找我们想要查看的进程的PID
# cat /proc/[进程PID]/limits //查看进程文件描述符数量最大限制
5.查看当前进程文件描述符数量
# cd /proc/[进程PID]/fd //进入df目录下
# ls -l | wc -l //统计文件描述符数量
6.排查方式
- 1.对于已知的FD泄露场景可通过代码全局搜索排查
- 2.对于不确定的可通过重复执行某一操作,查看FD是否持续增长来判断是否有FD泄露,然后有针对性的排查代码逻辑。
附:1. dialog fd泄露报错
AndroidRuntime: FATAL EXCEPTION: main
AndroidRuntime: Process: com.jamdeo.tv.vod, PID: 2110
AndroidRuntime: java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
AndroidRuntime: at android.view.InputChannel.nativeReadFromParcel(Native Method)
AndroidRuntime: at android.view.InputChannel.readFromParcel(InputChannel.java:148)
AndroidRuntime: at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:759)
AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:669)
AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:319)
AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
AndroidRuntime: at android.app.Dialog.show(Dialog.java:325)
7.一个例子
集成sdk后发现fd泄露特别严重,基本播放视频几十分钟之后,必现崩溃。
1.根据第五条所述,发现fd确实在一致增加。
2.ls -l | wc -l > fd.record 输出到文件详细查看,并在不同阶段捕捉快照,如进入前,播放中,退出后,对比可回收与不可回收的文件句柄,发现以下规律:
大量的此种类型句柄类型
lrwx------ 1 root root 64 2019-12-29 10:56 126 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 2019-12-29 10:56 127 -> anon_inode:[eventpoll]
较多的音频打开文件
如”1.mp3“等
3.文件句柄类型:
- fd中如/data/data/xxxx_app/yyyy 之类的文件, 可直接定位
- “anon_inode:[eventpoll]” 和 "anon_inode:[eventfd]", ”pipe“通常是开启了太多的HandlerThread/Looper/MessageQueue, 线程忘记关闭, 或者looper 没有释放
- 对于system server, 如果有大批量的socket 打开, 可能是因为Input Channel 没有关闭
- “/dev/ashmem”, 通常是ContentProvider或者数据库使用关闭情况
4.主要存在两个问题
- 提示音频文件 频繁打开未进行关闭
- 大量的“anon_inode:[eventpoll]” 和 "anon_inode:[eventfd]",代表可能有HandlerThread为调用quit或者safeQuit,一个HandlerThread正好会占用这两种文件句柄。
5.排查
- 使用SoundPool来进行播放提示音及伴奏的播放,每次播放都会进行load操作,频繁的load而且 未调用unload或者releaes导致大量音频提示文件被打开而未进行关闭,造成句柄泄露。
- 对于eventpoll和eventfd的泄露,怀疑是HandlerThread泄露引起,全面排查sdk内代码,未释放的点有几个,但不会造成大量泄露,需要继续排查
6.使用Android Studio工具Methods Record
对于捕捉到的Medhods Record进行分析,发现了大量的FileDownloader和WorkHandler的线程(对此部分可以直接通过CPU详细信息来实时查看应用的线程情况)
7.原因确定后,针对具体实现修改后验证,fd处于稳定状态,泄露修复。