介紹檔案描述符的概念以及工作原理,並透過原始碼瞭解 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());
} } //釋放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();
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
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就是這個所謂對面的檔案描述符。
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 |
@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 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 |
@Override public
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建立失敗。