介绍文件描述符的概念以及工作原理,并通过源码了解 Android 中常见的 FD 泄漏。

一、什么是文件描述符?

文件描述符是在 Linux 文件系统的被使用,由于Android基 于Linux 系统,所以Android也继承了文件描述符系统。我们都知道,在 Linux 中一切皆文件,所以系统在运行时有大量的文件操作,内核为了高效管理已被打开的文件会创建索引,用来指向被打开的文件,这个索引即是文件描述符,其表现形式为一个非负整数。

可以通过命令  ls -la /proc/$pid/fd 查看当前进程文件描述符使用信息。

上图中 箭头前的数组部分是文件描述符,箭头指向的部分是对应的文件信息。

Android系统中可以打开的文件描述符是有上限的,所以分到每一个进程可打开的文件描述符也是有限的。可以通过命令 cat /proc/sys/fs/file-max 查看所有进程允许打开的最大文件描述符数量。

当然也可以查看进程的允许打开的最大文件描述符数量。Linux默认进程最大文件描述符数量是1024,但是较新款的Android设置这个值被改为32768。

可以通过命令 ulimit -n 查看,Linux 默认是1024,比较新款的Android设备大部分已经是大于1024的,例如我用的测试机是:32768。

通过概念性的描述,我们知道系统在打开文件的时候会创建文件操作符,后续就通过文件操作符来操作文件。那么,文件描述符在代码上是怎么实现的呢,让我们来看一下Linux中用来描述进程信息的 task_struct 源码。

struct task_struct
{
// 进程状态
long               state;
// 虚拟内存结构体
struct mm_struct *mm;
// 进程号
pid_t              pid;
// 指向父进程的指针
struct task_struct*parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct* fs;
// 存放该进程打开的文件指针数组
struct files_struct *files;
};

task_struct 是 Linux  内核中描述进程信息的对象,其中files指向一个文件指针数组 ,这个数组中保存了这个进程打开的所有文件指针。 每一个进程会用 files_struct 结构体来记录文件描述符的使用情况,这个 files_struct 结构体为用户打开表,它是进程的私有数据,其定义如下:

/** Open file table structure*/
struct files_struct {/** read mostly part*/atomic_t count;//自动增量bool resize_in_progress;wait_queue_head_t resize_wait;struct fdtable __rcu *fdt; //fdtable类型指针struct fdtable fdtab;  //fdtable变量实例/** written part on a separate cache line in SMP*/spinlock_t file_lock ____cacheline_aligned_in_smp;unsigned int next_fd;unsigned long close_on_exec_init[1];//执行exec时需要关闭的文件描述符初值结合(从主进程中fork出子进程)unsigned long open_fds_init[1];//todo 含义补充unsigned long full_fds_bits_init[1];//todo 含义补充struct file __rcu * fd_array[NR_OPEN_DEFAULT];//默认的文件描述符长度
};

一般情况,“文件描述符”指的就是文件指针数组 files 的索引。

Linux  在2.6.14版本开始通过引入struct fdtable作为file_struct的间接成员,file_struct中会包含一个struct fdtable的变量实例和一个struct fdtable的类型指针。

struct fdtable {unsigned int max_fds;struct file __rcu **fd;      //指向文件对象指针数组的指针unsigned long *close_on_exec;unsigned long *open_fds;     //指向打开文件描述符的指针unsigned long *full_fds_bits;struct rcu_head rcu;
};

在file_struct初始化创建时,fdt指针指向的其实就是当前的的变量fdtab。当打开文件数超过初始设置的大小时,file_struct发生扩容,扩容后fdt指针会指向新分配的fdtable变量。

struct files_struct init_files = {.count      = ATOMIC_INIT(1),.fdt        = &init_files.fdtab,//指向当前fdtable.fdtab      = {.max_fds    = NR_OPEN_DEFAULT,.fd     = &init_files.fd_array[0],//指向files_struct中的fd_array.close_on_exec  = init_files.close_on_exec_init,//指向files_struct中的close_on_exec_init.open_fds   = init_files.open_fds_init,//指向files_struct中的open_fds_init.full_fds_bits  = init_files.full_fds_bits_init,//指向files_struct中的full_fds_bits_init},.file_lock  = __SPIN_LOCK_UNLOCKED(init_files.file_lock),.resize_wait    = __WAIT_QUEUE_HEAD_INITIALIZER(init_files.resize_wait),
};

RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。

RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。

RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。

struct file 处于内核空间,是内核在打开文件时创建,其中保存了文件偏移量,文件的inode等与文件相关的信息,在 Linux  内核中,file结构表示打开的文件描述符,而inode结构表示具体的文件。在文件的所有实例都关闭后,内核释放这个数据结构。

struct file {union {struct llist_node   fu_llist; //用于通用文件对象链表的指针struct rcu_head     fu_rcuhead;//RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制} f_u;struct path     f_path;//path结构体,包含vfsmount:指出该文件的已安装的文件系统,dentry:与文件相关的目录项对象struct inode        *f_inode;   /* cached value */const struct file_operations    *f_op;//文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作会初始化这个f_op字段/** Protects f_ep_links, f_flags.* Must not be taken from IRQ context.*/spinlock_t      f_lock;enum rw_hint        f_write_hint;atomic_long_t       f_count; //引用计数unsigned int        f_flags; //打开文件时候指定的标识,对应系统调用open的int flags参数。驱动程序为了支持非阻塞型操作需要检查这个标志fmode_t         f_mode;//对文件的读写模式,对应系统调用open的mod_t mode参数。如果驱动程序需要这个值,可以直接读取这个字段struct mutex        f_pos_lock;loff_t          f_pos; //目前文件的相对开头的偏移struct fown_struct  f_owner;const struct cred   *f_cred;struct file_ra_state    f_ra;u64         f_version;
#ifdef CONFIG_SECURITYvoid            *f_security;
#endif/* needed for tty driver, and maybe others */void            *private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */struct list_head    f_ep_links;struct list_head    f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */struct address_space    *f_mapping;errseq_t        f_wb_err;errseq_t        f_sb_err; /* for syncfs */
}

整体的数据结构示意图如下:

到这里,文件描述符的基本概念已介绍完毕。

二、文件描述符的工作原理

上文介绍了文件描述符的概念和部分源码,如果要进一步理解文件描述符的工作原理,需要查看由内核维护的三个数据结构。

i-node是 Linux  文件系统中重要的概念,系统通过i-node节点读取磁盘数据。表面上,用户通过文件名打开文件。实际上,系统内部先通过文件名找到对应的inode号码,其次通过inode号码获取inode信息,最后根据inode信息,找到文件数据所在的block,读出数据。

三个表的关系如下:

进程的文件描述符表为进程私有,该表的值是从0开始,在进程创建时会把前三位填入默认值,分别指向 标准输入流,标准输出流,标准错误流,系统总是使用最小的可用值。

正常情况一个进程会从fd[0]读取数据,将输出写入fd[1],将错误写入fd[2]

每一个文件描述符都会对应一个打开文件,同时不同的文件描述符也可以对应同一个打开文件。这里的不同文件描述符既可以是同一个进程下,也可以是不同进程。

每一个打开文件也会对应一个i-node条目,同时不同的文件也可以对应同一个i-node条目。

光看对应关系的结论有点乱,需要梳理每种对应关系的场景,帮助我们加深理解。

问题:如果有两个不同的文件描述符且最终对应一个i-node,这种情况下对应一个打开文件和对应多个打开文件有什么区别呢?

答:如果对一个打开文件,则会共享同一个文件偏移量。

举个例子:

fd1和fd2对应同一个打开文件句柄,fd3指向另外一个文件句柄,他们最终都指向一个i-node。

如果fd1先写入“hello”,fd2再写入“world”,那么文件写入为“helloworld”。

fd2会在fd1偏移之后添加写,fd3对应的偏移量为0,所以直接从开始覆盖写。

三、Android中FD泄漏场景

上文介绍了 Linux 系统中文件描述符的含义以及工作原理,下面我们介绍在Android系统中常见的文件描述符泄漏类型。

3.1 HandlerThread泄漏

HandlerThread是Android提供的带消息队列的异步任务处理类,他实际是一个带有Looper的Thread。正常的使用方法如下:

//初始化
private void init(){//initif(null != mHandlerThread){mHandlerThread = new HandlerThread("fd-test");mHandlerThread.start();mHandler = new Handler(mHandlerThread.getLooper());}
}//释放handlerThread
private void release(){if(null != mHandler){mHandler.removeCallbacksAndMessages(null);mHandler = null;}if(null != mHandlerThread){mHandlerThread.quitSafely();mHandlerThread = null;}
}

HandlerThread在不需要使用的时候,需要调用上述代码中的release方法来释放资源,比如在Activity退出时。另外全局的HandlerThread可能存在被多次赋值的情况,需要做空判断或者先释放再赋值,也需要重点关注。

HandlerThread会泄漏文件描述符的原因是使用了Looper,所以如果普通Thread中使用了Looper,也会有这个问题。下面让我们来分析一下Looper的代码,查看到底是在哪里调用的文件操作。

HandlerThread在run方法中调用Looper.prepare();

public void run() {mTid = Process.myTid();Looper.prepare();synchronized (this) {mLooper = Looper.myLooper();notifyAll();}Process.setThreadPriority(mPriority);onLooperPrepared();Looper.loop();mTid = -1;
}

Looper在构造方法中创建MessageQueue对象。

private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();
}

MessageQueue,也就是我们在Handler学习中经常提到的消息队列,在构造方法中调用了native层的初始化方法。

MessageQueue(boolean quitAllowed) {mQuitAllowed = quitAllowed;mPtr = nativeInit();//native层代码
}

MessageQueue对应native代码,这段代码主要是初始化了一个NativeMessageQueue,然后返回一个long型到Java层。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();if (!nativeMessageQueue) {jniThrowRuntimeException(env, "Unable to allocate native queue");return 0;}nativeMessageQueue->incStrong(env);return reinterpret_cast<jlong>(nativeMessageQueue);
}

NativeMessageQueue初始化方法中会先判断是否存在当前线程的Native层的Looper,如果没有的就创建一个新的Looper并保存。

NativeMessageQueue::NativeMessageQueue() :mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {mLooper = Looper::getForThread();if (mLooper == NULL) {mLooper = new Looper(false);Looper::setForThread(mLooper);}
}

在Looper的构造函数中,我们发现“eventfd”,这个很有文件描述符特征的方法。

Looper::Looper(bool allowNonCallbacks): mAllowNonCallbacks(allowNonCallbacks),mSendingMessage(false),mPolling(false),mEpollRebuildRequired(false),mNextRequestSeq(0),mResponseIndex(0),mNextMessageUptime(LLONG_MAX) {mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));//eventfdLOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));AutoMutex _l(mLock);rebuildEpollLocked();
}

从C++代码注释中可以知道eventfd函数会返回一个新的文件描述符。

/*** [eventfd(2)](http://man7.org/linux/man-pages/man2/eventfd.2.html) creates a file descriptor* for event notification.** Returns a new file descriptor on success, and returns -1 and sets `errno` on failure.*/
int eventfd(unsigned int __initial_value, int __flags);

3.2 IO泄漏

IO操作是Android开发过程中常用的操作,如果没有正确关闭流操作,除了可能会导致内存泄漏,也会导致FD的泄漏。常见的问题代码如下:

private void ioTest(){try {File file = new File(getCacheDir(), "testFdFile");file.createNewFile();FileOutputStream out = new FileOutputStream(file);//do somethingout.close();}catch (Exception e){e.printStackTrace();}
}

如果在流操作过程中发生异常,就有可能导致泄漏。正确的写法应该是在final块中关闭流。

private void ioTest() {FileOutputStream out = null;try {File file = new File(getCacheDir(), "testFdFile");file.createNewFile();out = new FileOutputStream(file);//do somethingout.close();} catch (Exception e) {e.printStackTrace();} finally {if (null != out) {try {out.close();} catch (IOException e) {e.printStackTrace();}}}
}

同样,我们在从源码中寻找流操作是如何创建文件描述符的。首先,查看 FileOutputStream 的构造方法 ,可以发现会初始化一个名为fd的 FileDescriptor 变量,这个 FileDescriptor 对象是Java层对native文件描述符的封装,其中只包含一个int类型的成员变量,这个变量的值就是native层创建的文件描述符的值。

public FileOutputStream(File file, boolean append) throws FileNotFoundException
{//......this.fd = new FileDescriptor();//......open(name, append);//......
}

open方法会直接调用jni方法open0.

/*** Opens a file, with the specified name, for overwriting or appending.* @param name name of file to be opened* @param append whether the file is to be opened in append mode*/
private native void open0(String name, boolean append)throws FileNotFoundException;private void open(String name, boolean append)throws FileNotFoundException {open0(name, append);
}

Tips:  我们在看android源码时常常遇到native方法,通过Android Studio无法跳转查看,可以在 androidxref 网站,通过“Java类名_native方法名”的方法进行搜索。例如,这可以搜索 FileOutputStream_open0 。

接下来,让我们进入native方法查看对应实现。

JNIEXPORT void JNICALL
FileOutputStream_open0(JNIEnv *env, jobject this, jstring path, jboolean append) {fileOpen(env, this, path, fos_fd,O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

在fileOpen方法中,通过handleOpen生成native层的文件描述符(fd),这个fd就是这个所谓对面的文件描述符。

void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{WITH_PLATFORM_STRING(env, path, ps) {FD fd;//......fd = handleOpen(ps, flags, 0666);if (fd != -1) {SET_FD(this, fd, fid);} else {throwFileNotFoundException(env, path);}} END_PLATFORM_STRING(env, ps);
}FD handleOpen(const char *path, int oflag, int mode) {FD fd;RESTARTABLE(open64(path, oflag, mode), fd);//调用open,获取fdif (fd != -1) {//......if (result != -1) {//......} else {close(fd);fd = -1;}}return fd;
}

到这里就结束了吗?

回到开始,FileOutputStream构造方法中初始化了Java层的文件描述符类 FileDescriptor,目前这个对象中的文件描述符的值还是初始的-1,所以目前它还是一个无效的文件描述符,native层完成fd创建后,还需要把fd的值传到 Java层。

我们再来看SET_FD这个宏的定义,在这个宏定义中,通过反射的方式给Java层对象的成员变量赋值。由于上文内容可知,open0是对象的jni方法,所以宏中的this,就是初始创建的FileOutputStream在Java层的对象实例。

#define SET_FD(this, fd, fid) \if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \(*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))

而fid则会在native代码中提前初始化好。

static void FileOutputStream_initIDs(JNIEnv *env) {jclass clazz = (*env)->FindClass(env, "java/io/FileOutputStream");fos_fd = (*env)->GetFieldID(env, clazz, "fd", "Ljava/io/FileDescriptor;");
}

收,到这里FileOutputStream的初始化跟进就完成了,我们已经找到了底层fd初始化的路径。Android的IO操作还有其他的流操作类,大致流程基本类似,这里不再细述。

并不是不关闭就一定会导致文件描述符泄漏,在流对象的析构方法中会调用close方法,所以这个对象被回收时,理论上也是会释放文件描述符。但是最好还是通过代码控制释放逻辑。

3.3 SQLite泄漏

在日常开发中如果使用数据库SQLite管理本地数据,在数据库查询的cursor使用完成后,亦需要调用close方法释放资源,否则也有可能导致内存和文件描述符的泄漏。

public void get() {db = ordersDBHelper.getReadableDatabase();Cursor cursor = db.query(...);while (cursor.moveToNext()) {//......}if(flag){//某种原因导致retrnreturn;}//不调用close,fd就会泄漏cursor.close();
}

按照理解query操作应该会导致文件描述符泄漏,那我们就从query方法的实现开始分析。

然而,在query方法中并没有发现文件描述符相关的代码。

经过测试发现,moveToNext 调用后才会导致文件描述符增长。通过query方法可以获取cursor的实现类SQLiteCursor。

public Cursor query(CursorFactory factory, String[] selectionArgs) {final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);final Cursor cursor;//......if (factory == null) {cursor = new SQLiteCursor(this, mEditTable, query);} else {cursor = factory.newCursor(mDatabase, this, mEditTable, query);}//......
}

在SQLiteCursor的父类找到moveToNext的实现。getCount 是抽象方法,在子类SQLiteCursor实现。

@Override
public final boolean moveToNext() {return moveToPosition(mPos + 1);
}
public final boolean moveToPosition(int position) {// Make sure position isn't past the end of the cursorfinal int count = getCount();if (position >= count) {mPos = count;return false;}//......
}

getCount 方法中对成员变量mCount做判断,如果还是初始值,则会调用fillWindow方法。

@Override
public int getCount() {if (mCount == NO_COUNT) {fillWindow(0);}return mCount;
}
private void fillWindow(int requiredPos) {clearOrCreateWindow(getDatabase().getPath());//......
}

clearOrCreateWindow 实现又回到父类 AbstractWindowedCursor 中。

protected void clearOrCreateWindow(String name) {if (mWindow == null) {mWindow = new CursorWindow(name);} else {mWindow.clear();}
}

在CursorWindow的构造方法中,通过nativeCreate方法调用到native层的初始化。

public CursorWindow(String name, @BytesLong long windowSizeBytes) {//......mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);//......
}

在C++代码中会继续调用一个native层CursorWindow的create方法。

static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) {//......CursorWindow* window;status_t status = CursorWindow::create(name, cursorWindowSize, &window);//......return reinterpret_cast<jlong>(window);
}

在CursorWindow的create方法中,我们可以发现fd创建相关的代码。

status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) {String8 ashmemName("CursorWindow: ");ashmemName.append(name);status_t result;int ashmemFd = ashmem_create_region(ashmemName.string(), size);//......
}

ashmem_create_region 方法最终会调用到open函数打开文件并返回系统创建的文件描述符。这部分代码不在赘述,有兴趣的可以自行查看 。

native完成初始化会把fd信息保存在CursorWindow中并会返回一个指针地址到Java层,Java层可以通过这个指针操作c++层对象从而也能获取对应的文件描述符。

3.4 InputChannel 导致的泄漏

WindowManager.addView

通过WindowManager反复添加view也会导致文件描述符增长,可以通过调用removeView释放之前创建的FD。

private void addView() {View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);//重复调用mWindowManager.addView(windowView, wmParams);
}

WindowManagerImpl中的addView最终会走到ViewRootImpl的setView。

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {//......root = new ViewRootImpl(view.getContext(), display);//......root.setView(view, wparams, panelParentView);
}

setView中会创建InputChannel,并通过Binder机制传到服务端。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {//......//创建inputchannelif ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {mInputChannel = new InputChannel();}//远程服务接口res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);//mInputChannel 作为参数传过去//......if (mInputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}//创建 WindowInputEventReceiver 对象mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}
}

addToDisplay是一个AIDL方法,它的实现类是源码中的Session。最终调用的是 WindowManagerService 的 addWIndow 方法。

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,Rect outStableInsets,DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,outInsetsState, outActiveControls, UserHandle.getUserId(mUid));
}

WMS在 addWindow 方法中创建 InputChannel 用于通讯。

public int addWindow(Session session, IWindow client, int seq,LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,Rect outContentInsets, Rect outStableInsets, Rect outOutsets,DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {//......final boolean openInputChannels = (outInputChannel != null&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);if  (openInputChannels) {win.openInputChannel(outInputChannel);}//......
}

在 openInputChannel 中创建 InputChannel ,并把客户端的传回去。

void openInputChannel(InputChannel outInputChannel) {//......InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);mInputChannel = inputChannels[0];mClientChannel = inputChannels[1];//......
}

InputChannel 的 openInputChannelPair 会调用native的 nativeOpenInputChannelPair ,在native中创建两个带有文件描述符的 socket 。

int socketpair(int domain, int type, int protocol, int sv[2]) {//创建一对匿名的已经连接的套接字int rc = __socketpair(domain, type, protocol, sv);if (rc == 0) {//跟踪文件描述符FDTRACK_CREATE(sv[0]);FDTRACK_CREATE(sv[1]);}return rc;
}

WindowManager 的分析涉及WMS,WMS内容比较多,本文重点关注文件描述符相关的内容。简单的理解,就是进程间通讯会创建socket,所以也会创建文件描述符,而且会在服务端进程和客户端进程各创建一个。另外,如果系统进程文件描述符过多,理论上会造成系统崩溃。

四、如何排查

如果你的应用收到如下这些崩溃堆栈,恭喜你,你的应用存在文件描述符泄漏。

  • abort message 'could not create instance too many files'
  • could not read input file descriptors from parcel
  • socket failed:EMFILE (Too many open files)
  • ...

文件描述符导致的崩溃往往无法通过堆栈直接分析。道理很简单: 出问题的代码在消耗文件描述符同时,正常的代码逻辑可能也同样在创建文件描述符,所以崩溃可能是被正常代码触发了。

4.1 打印当前FD信息

遇到这类问题可以先尝试本体复现,通过命令 ‘ls -la /proc/$pid/fd’ 查看当前进程文件描述符的消耗情况。一般android应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。

4.2 dump系统信息

通过dumpsys window ,查看是否有异常window。用于解决 InputChannel 相关的泄漏问题。

4.3 线上监控

如果是本地无法复现问题,可以尝试添加线上监控代码,定时轮询当前进程使用的FD数量,在达到阈值时,读取当前FD的信息,并传到后台分析,获取FD对应文件信息的代码如下。

if (Build.VERSION.SDK_INT >= VersionCodes.L) {linkTarget = Os.readlink(file.getAbsolutePath());
} else {//通过 readlink 读取文件描述符信息
}

4.4 排查循环打印的日志

除了直接对 FD相关的信息进行分析,还需要关注logcat中是否有频繁打印的信息,例如:socket创建失败。

五、参考文档

  1. Linux 源码
  2. Android源码
  3. i-node介绍
  4. InputChannel通信
  5. Linux 内核文件描述符表的演变

一文帮你搞懂 Android 文件描述符相关推荐

  1. 转:彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑

    转自:https://blog.csdn.net/u010937230/article/details/73303034 前言: 对于任何一个应用来说,无论是PC端应用还是Android应用,存储肯定 ...

  2. 一文帮你搞懂GBK码协议,让你真正理解和搞定它!

    作者:瑞生,来源:瑞生网 微信公众号:芯片之家(ID:chiphome-dy) 关于中文字符的显示,你听到最多的两个名词就是:GB2312和GBK.于是你上网一查,知道了GB2312码有6000多个字 ...

  3. 真香定律!一文带你搞懂Android多线程Handler,成功入职腾讯

    Google 为了帮助 Android 开发者更快更好地开发 App,推出了一系列组件,这些组件被打包成了一个整体,称作 Android Jetpack,它包含的组件如下图所示: 老的 support ...

  4. 深度科普: 一文帮你搞懂协作机器人的前世今生

    给大家说个冷知识-- 协作机器人其实是个「95后」. 最早是在1996年,美国西北大学的两位教授J. Edward Colgate和Michael Peshkin提出了协作机器人概念. J. Edwa ...

  5. 一文彻底搞懂C++文件流, 文件读写,fstream、seekg、seekp等的使用。

    彻底搞懂C++文件流. 首先需要头文件#include< fstream > fstream可以把它理解成一个父类,包含的子类有ifstream和ofstream等, 所以一般直接创建一个 ...

  6. python异步读写文件_一文彻底搞懂python文件读写

    Python文件读写 一,I/O操作 I/O在计算机中是指Input/Output,也就是Stream(流)的输入和输出.这里的输入和输出是相对于内存来说的,Input Stream(输入流)是指数据 ...

  7. 实用口语(第24期):帮你搞懂日期的写法读法

    实用口语(第24期):帮你搞懂日期的写法读法 作者: Emma 来自小组: 托福100分 在往下读之前先做道题吧. Which is not a possible way to read this d ...

  8. 一文带你搞懂C#多线程的5种写法

    一文带你搞懂C#多线程的5种写法 1.简介 超长警告! 在学习本篇文章前你需要学习的相关知识: 线程基本知识 此篇文章简单总结了C#中主要的多线程实现方法,包括: Thread 线程 ThreadPo ...

  9. 一文多图搞懂KITTI数据集下载及解析

    转载自一文多图搞懂KITTI数据集下载及解析-阿里云开发者社区 KITTI Dataset 1.图片下载:点击下载:https://s3.eu-central-1.amazonaws.com/avg- ...

最新文章

  1. 记录每个登陆用户的操作记录
  2. IIS 500错误报service unavailable解决方案之一
  3. CLR Dynamic Languages
  4. make j4什么意思_为什么天天坚持撸铁 肌肉增长不明显
  5. pcl_openmap_OpenMap教程3 –使用MapHandler构建基本的地图应用程序–第2部分
  6. mysql mode_MYSQL中的sql_mode模式
  7. MySQL的五种日期和时间类型
  8. 闭包函数和装饰器(待补充)
  9. windows的dos(cmd)下使用git commit出现error: pathspec readme.md did not match any file known to git
  10. aboboo 上一句 快捷键_Aboboo——英语四六级神器
  11. Ubuntu 命令手册
  12. LQR控制算法的浅析
  13. 4S综合管理系统源码 汽车美容4S店管理服务源码
  14. elementUI---使用按钮切换折叠侧边栏
  15. 用vue开发一个猫眼电影web app
  16. 手机与计算机无线连接的方法,电脑和手机手动添加无线连接的方法
  17. k8s学习-CKA真题-k8s升级(kubeadm、kubelet、kubectl等)
  18. YDOOK :STM32 : 什么是波特率?波特率Baudrate 的定义
  19. 电子货架标签——可自动切换多张画面
  20. Hamilton问题求解-最近邻点法和最近插入法(Python实现)

热门文章

  1. i3 6100 java_i3-4160和i3-6100哪个好 i3 6100与i3 4160区别对比评测详解
  2. 猎上网获IDG资本千万美元投资
  3. 左手坐标系与右手坐标系(转)
  4. 如何将XLSX转换成PDF
  5. 数据分析[1.0]--认识数据分析
  6. 如何选择入门级服务器,CPU
  7. 树莓派3B+安装Ubuntu Mate 16.04
  8. php流光字,各种漂亮的流光字在线制作方法
  9. 浅析多元化互动方式促进抖音强社交属性
  10. Elasticsearch: object 及 nested 数据类型