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走漏问题不怎么常见的原因。

chopin:/ $ ulimit -n
32768

走漏场景

  • 输入输出流,Socket和Cursor等常见场景,下面以输入输出流为例介绍下。

    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成果如下。

    浅谈FD走漏

    注释1处的写法是为了防止fd数量不如预期,这是因为假如FileOutputStream的引用未被持有,Java废物回收器确定其为废物,将其回收,而FileOutputStream的finalize()办法会开释fd资源。

    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。

    浅谈FD走漏

    里面值得注意的有两个点:

    • 每发动100个HandlerThread,anon_inode:[eventfd]anon_inode:[eventpoll] 就会各自多100个,HandlerThread:anon_inode:[eventfd]:anon_inode:[eventpoll] 是1:1:1的关系,所以可凭这个特征定位是否HandlerThread导致的FD走漏。

    • 假如发动完HandlerThread后开释,FD数量是不会增加的,所以用完HandlerThread记住开释,下面两个办法都能达到该目的。

      HandlerThread::quitSafely()//1
      HandlerThread::quit()//2
      

    源码如下

    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。

    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个弹窗,看看作用。

    浅谈FD走漏

    能够看到点击Button后FD数量从100激增到了1300,相当于每个Dialog都会产生差不多12个FD。

    执行下指令看下window相关的数据。

    dumpsys window|grep 'com.example.fddemoapp'
    

    浅谈FD走漏

排查办法

  • StrictMode。

    示例代码如下。

    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的状况。

        /**
        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。

    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各自类型的数量比较便利的指令。

    ls -l /proc/${pid}/proc|awk '{print $NF}' | sort | uniq -c
    

    举个实战比如,这次我在排查FD走漏的时分,发现anon_inode:[eventpoll]和anon_inode:[eventfd]的数量反常,且二者数量相等。

    浅谈FD走漏
    我初步定位到了是因为HandlerThread不正当运用导致的FD走漏,从而再去排查具体的问题代码。

  • 线上监控。

    每隔一段时间查看FD数量,当达到戒备值后,将/proc/${pid}/fd下的内容上传至后台剖析。

  • dump系统信息

    经过dumpsys window 来查看与window相关的FD走漏。

  • 排查循环打印的日志。

    如是否有Socket创立失败。

写在最终

本文首要参考了下面文章,此外,我还结合了自己这次定位FD走漏的经历。

Android FD 文件描述符 走漏总结 –