浅谈FD泄漏

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等常见场景,下面以输入输出流为例介绍下。

    kotlin 复制代码
    class 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资源。

    scala 复制代码
    public 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就会创建两个FDanon_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记得释放,下面两个方法都能达到该目的。

      arduino 复制代码
      HandlerThread::quitSafely()//1
      HandlerThread::quit()//2

    源码如下

    kotlin 复制代码
    class 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。

    kotlin 复制代码
    class 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相关的数据。

    javascript 复制代码
    dumpsys window|grep 'com.example.fddemoapp'    

排查方法

  • StrictMode。

    示例代码如下。

    less 复制代码
    StrictMode.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。

    javascript 复制代码
    emulator64_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各自类型的数量比较方便的命令。

    bash 复制代码
    ls -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泄漏的经历。

Android FD 文件描述符 泄露总结 - 掘金

相关推荐
工业甲酰苯胺44 分钟前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3431 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯3 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey4 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!6 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟7 小时前
Android音频采集
android·音视频
小白也想学C8 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程8 小时前
初级数据结构——树
android·java·数据结构
闲暇部落10 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin