介绍文件描述符的概念以及工作原理,并通过源码了解 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 源码。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 |
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 结构体为用户打开表,它是进程的私有数据,其定义如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 |
/*
* 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的类型指针。
|
1
2
3
4
5
6
7
8 |
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变量。
|
1
2
3
4
5
6
7
8
9
10
11
12
13 |
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结构表示具体的文件。在文件的所有实例都关闭后,内核释放这个数据结构。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 |
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_SECURITY
void
*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。正常的使用方法如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 |
//初始化private
void
init(){
//init
if
(
null
!= mHandlerThread){
mHandlerThread =
new
HandlerThread(
"fd-test"
);
mHandlerThread.start();
mHandler =
new
Handler(mHandlerThread.getLooper());
}} //释放handlerThreadprivate
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();
|
1
2
3
4
5
6
7
8
9
10
11
12 |
public
void
run() {
mTid = Process.myTid();
Looper.prepare();
synchronized
(
this
) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -
1
;} |
Looper在构造方法中创建MessageQueue对象。
|
1
2
3
4 |
private
Looper(
boolean
quitAllowed) {
mQueue =
new
MessageQueue(quitAllowed);
mThread = Thread.currentThread();} |
MessageQueue,也就是我们在Handler学习中经常提到的消息队列,在构造方法中调用了native层的初始化方法。
|
1
2
3
4 |
MessageQueue(
boolean
quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
//native层代码} |
MessageQueue对应native代码,这段代码主要是初始化了一个NativeMessageQueue,然后返回一个long型到Java层。
|
1
2
3
4
5
6
7
8
9 |
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并保存。
|
1
2
3
4
5
6
7 |
NativeMessageQueue::NativeMessageQueue() :mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if
(mLooper == NULL) {
mLooper =
new
Looper(
false
);
Looper::setForThread(mLooper);
}} |
在Looper的构造函数中,我们发现“eventfd”,这个很有文件描述符特征的方法。
|
1
2
3
4
5
6
7
8
9
10
11
12 |
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));
//eventfd
LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() <
0
,
"Could not make wake event fd: %s"
, strerror(errno));
AutoMutex _l(mLock);
rebuildEpollLocked();} |
从C++代码注释中可以知道eventfd函数会返回一个新的文件描述符。
|
1
2
3
4
5
6
7 |
/**
* [eventfd(2)]() 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的泄漏。常见的问题代码如下:
|
1
2
3
4
5
6
7
8
9
10
11 |
private
void
ioTest(){
try
{
File file =
new
File(getCacheDir(),
"testFdFile"
);
file.createNewFile();
FileOutputStream out =
new
FileOutputStream(file);
//do something
out.close();
}
catch
(Exception e){
e.printStackTrace();
}} |
如果在流操作过程中发生异常,就有可能导致泄漏。正确的写法应该是在final块中关闭流。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 |
private
void
ioTest() {
FileOutputStream out =
null
;
try
{
File file =
new
File(getCacheDir(),
"testFdFile"
);
file.createNewFile();
out =
new
FileOutputStream(file);
//do something
out.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层创建的文件描述符的值。
|
1
2
3
4
5
6
7
8 |
public
FileOutputStream(File file,
boolean
append)
throws
FileNotFoundException{
//......
this
.fd =
new
FileDescriptor();
//......
open(name, append);
//......} |
open方法会直接调用jni方法open0.
|
1
2
3
4
5
6
7
8
9
10
11
12 |
/**
* 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无法跳转查看,可以在 网站,通过“Java类名_native方法名”的方法进行搜索。例如,这可以搜索 FileOutputStream_open0 。
接下来,让我们进入 方法查看对应实现。
|
1
2
3
4
5 |
JNIEXPORT
void
JNICALLFileOutputStream_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就是这个所谓对面的文件描述符。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 |
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,获取fd
if
(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层的对象实例。
|
1
2
3 |
#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代码中提前初始化好。
|
1
2
3
4 |
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方法释放资源,否则也有可能导致内存和文件描述符的泄漏。
|
1
2
3
4
5
6
7
8
9
10
11
12
13 |
public
void
get() {
db = ordersDBHelper.getReadableDatabase();
Cursor cursor = db.query(...);
while
(cursor.moveToNext()) {
//......
}
if
(flag){
//某种原因导致retrn
return
;
}
//不调用close,fd就会泄漏
cursor.close();} |
按照理解query操作应该会导致文件描述符泄漏,那我们就从query方法的实现开始分析。
然而,在query方法中并没有发现文件描述符相关的代码。
经过测试发现,moveToNext 调用后才会导致文件描述符增长。通过query方法可以获取cursor的实现类SQLiteCursor。
|
1
2
3
4
5
6
7
8
9
10
11 |
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实现。
|
1
2
3
4
5
6
7
8
9
10
11
12
13 |
@Overridepublic
final
boolean
moveToNext() {
return
moveToPosition(mPos +
1
);}public
final
boolean
moveToPosition(
int
position) {
// Make sure position isn't past the end of the cursor
final
int
count = getCount();
if
(position >= count) {
mPos = count;
return
false
;
}
//......} |
getCount 方法中对成员变量mCount做判断,如果还是初始值,则会调用fillWindow方法。
|
1
2
3
4
5
6
7
8
9
10
11 |
@Overridepublic
int
getCount() {
if
(mCount == NO_COUNT) {
fillWindow(
0
);
}
return
mCount;}private
void
fillWindow(
int
requiredPos) {
clearOrCreateWindow(getDatabase().getPath());
//......} |
clearOrCreateWindow 实现又回到父类 AbstractWindowedCursor 中。
|
1
2
3
4
5
6
7 |
protected
void
clearOrCreateWindow(String name) {
if
(mWindow ==
null
) {
mWindow =
new
CursorWindow(name);
}
else
{
mWindow.clear();
}} |
在CursorWindow的构造方法中,通过nativeCreate方法调用到native层的初始化。
|
1
2
3
4
5 |
public
CursorWindow(String name,
@BytesLong
long
windowSizeBytes) {
//......
mWindowPtr = nativeCreate(mName, (
int
) windowSizeBytes);
//......} |
在C++代码中会继续调用一个native层CursorWindow的create方法。
|
1
2
3
4
5
6
7 |
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创建相关的代码。
|
1
2
3
4
5
6
7 |
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。
|
1
2
3
4
5 |
private
void
addView() {
View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window,
null
);
//重复调用
mWindowManager.addView(windowView, wmParams);} |
WindowManagerImpl中的addView最终会走到ViewRootImpl的setView。
|
1
2
3
4
5
6 |
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机制传到服务端。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 |
public
void
setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//......
//创建inputchannel
if
((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方法,它的实现类是源码中的 。最终调用的是 WindowManagerService 的 addWIndow 方法。
|
1
2
3
4
5
6
7
8
9 |
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 用于通讯。
|
1
2
3
4
5
6
7
8
9
10
11
12 |
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 ,并把客户端的传回去。
|
1
2
3
4
5
6
7 |
void
openInputChannel(InputChannel outInputChannel) {
//......
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
mInputChannel = inputChannels[
0
];
mClientChannel = inputChannels[
1
];
//......} |
InputChannel 的 openInputChannelPair 会调用native的 nativeOpenInputChannelPair ,在native中创建两个带有文件描述符的 socket 。
|
1
2
3
4
5
6
7
8
9
10 |
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对应文件信息的代码如下。
|
1
2
3
4
5 |
if
(Build.VERSION.SDK_INT >= VersionCodes.L) {
linkTarget = Os.readlink(file.getAbsolutePath());}
else
{
//通过 readlink 读取文件描述符信息} |
4.4 排查循环打印的日志
除了直接对 FD相关的信息进行分析,还需要关注logcat中是否有频繁打印的信息,例如:socket创建失败。