浅谈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 文件描述符 泄露总结 - 掘金

相关推荐
恋猫de小郭2 小时前
React Native 鸿蒙 2026 路线发布,为什么它的适配成本那么高?
android·前端·react native
studyForMokey3 小时前
【Android面试】窗口机制专题
android·面试·职场和发展
用户013201436033 小时前
Android 资源管理与常用布局详解|基础入门
android
陆业聪4 小时前
从 OpenClaw 到 Android:Harness Engineering 是怎么让 Agent 变得可用的
android·人工智能·ai编程
stevenzqzq6 小时前
颜色透明度转换技术文档(Android/Compose)
android
巴黎没有摩天轮Li6 小时前
Android JVMTI 接入流程
android
2501_915909067 小时前
iOS 抓包不越狱,代理抓包 和 数据线直连抓包两种实现方式
android·ios·小程序·https·uni-app·iphone·webview
城东米粉儿7 小时前
Android VCL 和 NAL笔记
android
常利兵7 小时前
从0到1,解锁Android WebView混合开发新姿势
android·华为·harmonyos
背包客(wyq)8 小时前
基于Android手机的语音数据采集系统(语音数据自动上传至电脑端)
android·网络