一文幫你搞懂 Android 文件描述符

介紹文件描述符的概念以及工做原理,並經過源碼瞭解 Android 中常見的 FD 泄漏。html

1、什麼是文件描述符?

文件描述符是在 Linux 文件系統的被使用,因爲Android基 於Linux 系統,因此Android也繼承了文件描述符系統。咱們都知道,在 Linux 中一切皆文件,因此係統在運行時有大量的文件操做,內核爲了高效管理已被打開的文件會建立索引,用來指向被打開的文件,這個索引便是文件描述符,其表現形式爲一個非負整數。java

能夠經過命令  ls -la /proc/$pid/fd 查看當前進程文件描述符使用信息。node

一文幫你搞懂 Android 文件描述符

上圖中 箭頭前的數組部分是文件描述符,箭頭指向的部分是對應的文件信息。linux

一文幫你搞懂 Android 文件描述符

Android系統中能夠打開的文件描述符是有上限的,因此分到每個進程可打開的文件描述符也是有限的。能夠經過命令 cat /proc/sys/fs/file-max 查看全部進程容許打開的最大文件描述符數量。android

一文幫你搞懂 Android 文件描述符

固然也能夠查看進程的容許打開的最大文件描述符數量。Linux默認進程最大文件描述符數量是1024,可是較新款的Android設置這個值被改成32768。c++

一文幫你搞懂 Android 文件描述符

能夠經過命令 ulimit -n 查看,Linux 默認是1024,比較新款的Android設備大部分已是大於1024的,例如我用的測試機是:32768。數據庫

經過概念性的描述,咱們知道系統在打開文件的時候會建立文件操做符,後續就經過文件操做符來操做文件。那麼,文件描述符在代碼上是怎麼實現的呢,讓咱們來看一下Linux中用來描述進程信息的 task_struct 源碼。c#

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 的索引。session

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_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 */
}

總體的數據結構示意圖以下:

一文幫你搞懂 Android 文件描述符

到這裏,文件描述符的基本概念已介紹完畢。

2、文件描述符的工做原理

上文介紹了文件描述符的概念和部分源碼,若是要進一步理解文件描述符的工做原理,須要查看由內核維護的三個數據結構。

一文幫你搞懂 Android 文件描述符

i-node是 Linux  文件系統中重要的概念,系統經過i-node節點讀取磁盤數據。表面上,用戶經過文件名打開文件。實際上,系統內部先經過文件名找到對應的inode號碼,其次經過inode號碼獲取inode信息,最後根據inode信息,找到文件數據所在的block,讀出數據。

三個表的關係以下:

一文幫你搞懂 Android 文件描述符

進程的文件描述符表爲進程私有,該表的值是從0開始,在進程建立時會把前三位填入默認值,分別指向 標準輸入流,標準輸出流,標準錯誤流,系統老是使用最小的可用值。

正常狀況一個進程會從fd[0]讀取數據,將輸出寫入fd[1],將錯誤寫入fd[2]

每個文件描述符都會對應一個打開文件,同時不一樣的文件描述符也能夠對應同一個打開文件。這裏的不一樣文件描述符既能夠是同一個進程下,也能夠是不一樣進程。

每個打開文件也會對應一個i-node條目,同時不一樣的文件也能夠對應同一個i-node條目。

光看對應關係的結論有點亂,須要梳理每種對應關係的場景,幫助咱們加深理解。

一文幫你搞懂 Android 文件描述符

問題:若是有兩個不一樣的文件描述符且最終對應一個i-node,這種狀況下對應一個打開文件和對應多個打開文件有什麼區別呢?

答:若是對一個打開文件,則會共享同一個文件偏移量。

舉個例子:

fd1和fd2對應同一個打開文件句柄,fd3指向另一個文件句柄,他們最終都指向一個i-node。

若是fd1先寫入「hello」,fd2再寫入「world」,那麼文件寫入爲「helloworld」。

fd2會在fd1偏移以後添加寫,fd3對應的偏移量爲0,因此直接從開始覆蓋寫。

3、Android中FD泄漏場景

上文介紹了 Linux 系統中文件描述符的含義以及工做原理,下面咱們介紹在Android系統中常見的文件描述符泄漏類型。

3.1 HandlerThread泄漏

HandlerThread是Android提供的帶消息隊列的異步任務處理類,他實際是一個帶有Looper的Thread。正常的使用方法以下:

//初始化
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();

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));//eventfd
    LOG_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 something
        out.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 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層建立的文件描述符的值。

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,獲取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層的對象實例。

#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){
       //某種緣由致使retrn
       return;
    }
    //不調用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 cursor
    final 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) {
    //......
    //建立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方法,它的實現類是源碼中的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,因此也會建立文件描述符,並且會在服務端進程和客戶端進程各建立一個。另外,若是系統進程文件描述符過多,理論上會形成系統崩潰。

4、如何排查

若是你的應用收到以下這些崩潰堆棧,恭喜你,你的應用存在文件描述符泄漏。

  • 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應用的文件描述符能夠分爲幾類,經過對比哪一類文件描述符數量太高,來縮小問題範圍。

一文幫你搞懂 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建立失敗。

5、參考文檔

  1. Linux 源碼
  2. Android源碼
  3. i-node介紹
  4. InputChannel通訊
  5. Linux 內核文件描述符表的演變
相關文章
相關標籤/搜索