FD是什么
FD全称是File Descriptor,文件描述符,是一个非负整数,唯一标识进程所打开的文件,管道或网络连接。
在Linux(Android是基于Linux内核的)的一切设备皆文件的设计哲学下,文件也可以是一台打印机,硬盘,网络接口。
当进程打开或创建一个文件时,内核会返回一个fd。
类型
fd总共有以下几个类型。
FD类型 | 说明 |
---|---|
socket | 与网络请求相关anon_inode |
anon_inode:[eventpoll] | HandlerThread线程Looper相关 |
anon_inode:[eventfd] | HandlerThread 线程 Looper相关 |
anon_inode:[timerfd] | 系统文件描述符类型,和应用关系不大 |
anon_inode:[dmabuf] | InputChannel泄露时增加明显 |
/vendor/ | 一般是系统操作使用 |
/dev/ashmem | 数据库操作相关 |
pipe: | 一般是系统操作使用 |
/sys/ | 一般是系统操作使用 |
/data/data/ | 打开文件相关 |
/data/app/ | 打开文件相关 |
/storage/emulate/0/ | 打开文件相关 |
fd泄漏
后果
一个进程能持有的FD数量是有限制的,当超过最大持有数后,app会crash来恢复。
Android可以在通过ulimit -n命令来查看一个进程的最大可持有FD数量,以Redmi Note 10Pro为例,其每个进程最大可持有FD数量为32768,可以看到这个数量能应付绝大多数非极端情况了,这也是为什么FD泄漏问题不怎么常见的原因。
bash
chopin:/ $ ulimit -n
32768
泄漏场景
-
输入输出流,Socket和Cursor等常见场景,下面以输入输出流为例介绍下。
kotlinclass MainActivity : AppCompatActivity() { ... private var fileOutputStreams:LinkedList<FileOutputStream> = LinkedList() override fun onCreate(savedInstanceState: Bundle?) { for (i in 1..100) { Log.d("MainActivity","create $i file") val file = File(cacheDir, "testFdFile$i") file.createNewFile() fileOutputStreams.add(FileOutputStream(file))//1 } } ... }
fd结果如下。
注释1处的写法是为了避免fd数量不如预期,这是因为如果FileOutputStream的引用未被持有,Java垃圾回收器认定其为垃圾,将其回收,而FileOutputStream的finalize()方法会释放fd资源。
scalapublic class FileOutputStream extends OutputStreamprivate { final FileDescriptor fd; protected void finalize() throws IOException { // Android-added: CloseGuard support. if (guard != null) { guard.warnIfOpen(); } if (fd != null) { if (fd == FileDescriptor.out || fd == FileDescriptor.err) { flush(); } else { // Android-removed: Obsoleted comment about shared FileDescriptor handling. close(); } } } }
输入输出流,Socket和Cursor用完后记得释放。
-
HandlerThread,Looper。
HandlerThread的特点的是每启动一个HandlerThread就会创建两个FD
anon_inode:[eventfd]
和anon_inode:[eventpoll]
用来实现线程通信,直接上手写个小demo。里面值得注意的有两个点:
-
每启动100个HandlerThread,
anon_inode:[eventfd]
和anon_inode:[eventpoll]
就会各自多100个,HandlerThread:anon_inode:[eventfd]
:anon_inode:[eventpoll]
是1:1:1的关系,所以可凭这个特征定位是否HandlerThread导致的FD泄漏。 -
如果启动完HandlerThread后释放,FD数量是不会增加的,所以用完HandlerThread记得释放,下面两个方法都能达到该目的。
arduinoHandlerThread::quitSafely()//1 HandlerThread::quit()//2
源码如下
kotlinclass MainActivity : ComponentActivity() { private var fileOutputStreams: LinkedList<FileOutputStream> = LinkedList() private var handlerThreads: LinkedList<HandlerThread> = LinkedList() private val TAG = "MainActivity" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val cntOfEventFd = remember { mutableIntStateOf(0) } val cntOfEventPoll = remember { mutableIntStateOf(0) } MainActivityView(cntOfEventFd, cntOfEventPoll, { createMultipleThread() }, { createMultipleThread(true) }, { releasingAllHandlerThread() }) LaunchedEffect(Unit) { while (true) { val res = checkHandlerThreadRelatedFdNum() cntOfEventFd.intValue = res.first cntOfEventPoll.intValue = res.second delay(1000) } } } } private fun createMultipleThread(releasingResource: Boolean = false) { for (i in 1..100) { val handlerThread = HandlerThread("$i HandlerThread") handlerThread.start() handlerThreads.add(handlerThread) if (releasingResource) { handlerThread.quitSafely() } } checkHandlerThreadRelatedFdNum() } private fun releasingAllHandlerThread() { while (handlerThreads.isNotEmpty()) { val curHandlerThread = handlerThreads.pop(); curHandlerThread.quitSafely() } } private fun checkHandlerThreadRelatedFdNum(): Pair<Int, Int> { val fdFile = File("/proc/" + android.os.Process.myPid() + "/fd/") val files = fdFile.listFiles() // 列出当前目录下所有的文件 val length = files?.size; // 进程中的fd数量 Log.d(TAG, "listFd = " + android.os.Process.myPid() + " = " + length) var cntOfEventFd = 0 var cntOfEventPoll = 0 files?.forEach { file -> try { val linkTarget = Os.readlink(file.absolutePath); Log.d(TAG, "$file====>$linkTarget") if (linkTarget.contains("anon_inode:[eventfd]")) { cntOfEventFd++; } else if (linkTarget.contains("anon_inode:[eventpoll]")) { cntOfEventPoll++ } } catch (e: Exception) { Log.d(TAG, "$file====> error") } } return Pair(cntOfEventFd, cntOfEventPoll) } } @Composable fun MainActivityView( cntOfEventFd: State<Int>, cntOfEventPoll: State<Int>, createHandlerThreadWithoutReleasingInvoker: (() -> Unit) = {}, createHandlerThreadWithReleasingInvoker: (() -> Unit) = {}, releasingHandlerThread: (() -> Unit) = {} ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { AnimatedText(cntOfEventFd = cntOfEventFd, cntOfEventPoll = cntOfEventPoll) Button( onClick = { createHandlerThreadWithoutReleasingInvoker.invoke() }, Modifier.padding(10.dp) ) { Text(text = "实例化100个HandlerThread并不释放") } Button( onClick = { createHandlerThreadWithReleasingInvoker.invoke() }, Modifier.padding(10.dp) ) { Text(text = "实例化100个HandlerThread但释放") } Button( onClick = { releasingHandlerThread.invoke() }, Modifier.padding(10.dp) ) { Text(text = "释放所有HandlerThread") } } } @Composable fun AnimatedText(cntOfEventFd: State<Int>, cntOfEventPoll: State<Int>) { Crossfade(targetState = cntOfEventFd.value) { targetCount -> Text( text = "the cnt of anon_inode:[eventfd] is $targetCount", modifier = Modifier.padding(10.dp) ) } Crossfade(targetState = cntOfEventPoll.value) { targetCount -> Text( text = "the cnt of anon_inode:[eventpoll] is $targetCount", modifier = Modifier.padding(10.dp) ) } }
-
-
弹窗造成的fd泄漏。
同样地,写个小demo。
kotlinclass MainActivity : ComponentActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ... val cntOfFd = remember { mutableIntStateOf(0) } MainActivityAnotherView(cntOfFd = cntOfFd) LaunchedEffect(Unit) { while (true) { cntOfFd.intValue = getCntOfFd() delay(1000) } } } } private fun getCntOfFd(): Int { val fdFile = File("/proc/" + android.os.Process.myPid() + "/fd/") val files = fdFile.listFiles() // 列出当前目录下所有的文件 return files?.size ?: 0 } } @Composable fun MainActivityAnotherView(cntOfFd: State<Int>) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { var showDialog by remember { mutableStateOf(false) } Text( text = "cnt of fd is ${cntOfFd.value}", fontSize = 30.sp, modifier = Modifier.padding(10.dp) ) Button(onClick = { showDialog = true }) { Text(text = "弹出100个弹窗") } if (showDialog) { for (i in 1..100) { AlertDialog( onDismissRequest = { }, title = { Text("Dialog Title") }, text = { Text("This is an example dialog.") }, confirmButton = { Button(onClick = {}) { Text("OK") } } ) } } } }
上面代码里应用每秒会检查一次FD数量并更新在屏幕上,同时有一个Button来创建100个弹窗,看看效果。
可以看到点击Button后FD数量从100激增到了1300,相当于每个Dialog都会产生差不多12个FD。
执行下命令看下window相关的数据。
javascriptdumpsys window|grep 'com.example.fddemoapp'
排查方法
-
StrictMode。
示例代码如下。
lessStrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() // or .detectAll() for all detectable problems .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build());
在日志里搜StrictMode的TAG来查看相关日志。
适用情况:排查主线程不恰当读取文件和使用Socket的情况。
java/** StrictMode is most commonly used to catch accidental disk or network access on the application's main thread, where UI operations are received and animations take place. Keeping disk and network operations off the main thread makes for much smoother, more responsive applications. By keeping your application's main thread responsive, you also prevent ANR dialogs from being shown to users. **/ public final class StrictMode { }
-
根据数量异常的FD的类型来定位到问题代码。
可以通过
ls -l /proc/${pid}/proc
命令来查看某个进程持有的fd数量,pid为进程id。javascriptemulator64_arm64:/ # ls -l /proc/4618/fd/ total 0 lrwx------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 0 -> /dev/null lrwx------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 1 -> /dev/null lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 10 -> /apex/com.android.art/javalib/apache-xml.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 11 -> /system/framework/framework.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 12 -> /system/framework/framework-graphics.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 13 -> /system/framework/ext.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 14 -> /system/framework/telephony-common.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 15 -> /system/framework/voip-common.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 16 -> /system/framework/ims-common.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 17 -> /apex/com.android.i18n/javalib/core-icu4j.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 18 -> /apex/com.android.appsearch/javalib/framework-appsearch.jar lr-x------ 1 u0_a101 u0_a101 64 2024-03-15 04:11 19 -> /apex/com.android.conscrypt/javalib/conscrypt.jar
在这里也给出一条统计FD各自类型的数量比较方便的命令。
bashls -l /proc/${pid}/proc|awk '{print $NF}' | sort | uniq -c
举个实战例子,这次我在排查FD泄漏的时候,发现anon_inode:[eventpoll]和anon_inode:[eventfd]的数量异常,且二者数量相等。
我初步定位到了是因为HandlerThread不正当使用导致的FD泄漏,进而再去排查具体的问题代码。
-
线上监控。
每隔一段时间检查FD数量,当达到警戒值后,将/proc/${pid}/fd下的内容上传至后台分析。
-
dump系统信息
通过
dumpsys window
来检查与window相关的FD泄漏。 -
排查循环打印的日志。
如是否有Socket创建失败。
写在最后
本文主要参考了下面文章,此外,我还结合了自己这次定位FD泄漏的经历。