Android面試簡歷亮點來了!驅動核心源碼詳解帶你掌握 Binder 機制

本文設計源碼及邏輯較多,已儘量畫圖輔助,完全理解可能要耗一定精力,建議收藏。

應用程序中執行 getService() 需與 ServiceManager 通過 binder 跨進程通信,此過程中會貫穿 Framework、Natve 層以及 Linux 內核驅動。

binder 驅動的整體分層如上圖,下面先來宏觀的瞭解下 getService() 在整個 Android 系統中的調用棧,ServiceManager 本身的獲取:

與 ServiceManager 進行 IPC 通信:

「本文將主要分析此過程中 binder 驅動具體承擔了哪些工作」,也就是上圖中 IPCThreadState 與 binder 驅動的 ioctl 調用。

binder 驅動中做的工作可以總結爲以下幾步:

  1. 準備數據,根據命令分發給具體的方法去處理
  2. 找到目標進程的相關信息
  3. 將數據一次拷貝到目標進程所映射的物理內存塊
  4. 記錄待處理的任務,喚醒目標線程
  5. 調用線程進入休眠
  6. 目標進程直接拿到數據進行處理,處理完後喚醒調用線程
  7. 調用線程返回處理結果

在源碼中實際會執行到的函數主要包括:

  1. binder_ioctl()
  2. binder_get_thread()
  3. binder_ioctl_write_read()
  4. binder_thread_write()
  5. binder_transaction()
  6. binder_thread_read()

下面按照這些 binder 驅動中的函數,以工作步驟爲脈絡,深入分析驅動中的源碼執行邏輯,徹底搞定 binder 驅動!

1.binder_ioctl()

在 IPCThreadState 中通過系統調用 ioctl 陷入系統內核,調用到 binder_ioctl() 方法:

ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)

binder_ioctl() 方法中會根據 BINDER_WRITE_READ、BINDER_SET_MAX_THREADS 等不同 cmd 轉調到不同的方法去執行,這裏我們只關注 BINDER_WRITE_READ,代碼如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
    int ret;
    //拿到調用進程在 binder_open() 中記錄的 binder_proc
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    binder_lock(__func__);
    //獲取調用線程 binder_thread
    thread = binder_get_thread(proc);
    switch (cmd) {
    case BINDER_WRITE_READ:
      //處理 binder 數據讀寫,binder IPC 通信的核心邏輯
     ret = binder_ioctl_write_read(filp, cmd, arg, thread);
     if (ret)
      goto err;
     break;
    ...
}

之前文章介紹過 binder_open() 方法, binder_open() 方法主要做了兩個工作:

  1. 創建及初始化每個進程獨有一份的、用來存放 binder 相關數據的 binder_proc 結構體
  2. 「將 binder_proc 記錄起來,方便後續使用」

正是通過 file 的 private_data 來記錄的:

static int binder_open(struct inode *nodp, struct file *filp){
    ...
    filp->private_data = proc;
    ...
}

拿到調用進程後,進一步通過 binder_get_thread() 方法拿到調用線程,然後就交給 binder_ioctl_write_read() 方法去執行具體的 binder 數據讀寫了。

可見 binder_ioctl() 方法本身的邏輯非常簡單,將數據 arg 透傳了出去。

下面分別來看 binder_get_thread()、binder_ioctl_write_read() 這兩個方法。

2.binder_get_thread()

static struct binder_thread *binder_get_thread(
                            struct binder_proc *proc){
    struct binder_thread *thread = NULL;
    struct rb_node *parent = NULL;
    //從 proc 中獲取紅黑樹根節點
    struct rb_node **p = &proc->threads.rb_node; 
    //查找 pid 等於當前線程 id 的thread,該紅黑樹以 pid 大小爲序存放
    while (*p) {
        parent = *p;
        thread = rb_entry(parent, struct binder_thread, rb_node);
        //current->pid 是當前調用線程的 id
        if (current->pid < thread->pid) 
            p = &(*p)->rb_left;
        else if (current->pid > thread->pid)
            p = &(*p)->rb_right;
        else
            break;
    }
    
    if (*p == NULL) {//如果沒有找到,則新創建一個
        thread = kzalloc(sizeof(*thread), GFP_KERNEL);
        if (thread == NULL)
            return NULL;
        binder_stats_created(BINDER_STAT_THREAD);
        thread->proc = proc;
        thread->pid = current->pid;
        init_waitqueue_head(&thread->wait); //初始化等待隊列
        INIT_LIST_HEAD(&thread->todo); //初始化待處理隊列
        //加入到 proc 的 threads 紅黑樹中
        rb_link_node(&thread->rb_node, parent, p);  
        rb_insert_color(&thread->rb_node, &proc->threads);
        thread->looper |= BINDER_LOOPER_STATE_NEED_RETURN;
        thread->return_error = BR_OK;
        thread->return_error2 = BR_OK;
    }
    return thread;
}

binder_thread 是用來描述線程的結構體,binder_get_thread() 方法中邏輯也很簡單,首先從調用進程 proc 中查找當前線程是否已被記錄,如果找到就直接返回,否則新建一個返回,並記錄到 proc 中。

也就是說所有調用 binder_ioctl() 的線程,都會被記錄起來。

3.binder_ioctl_write_read

此方法分爲兩部分來看,首先是整體邏輯:

static int binder_ioctl_write_read(struct file *filp,
    unsigned int cmd, unsigned long arg,
    struct binder_thread *thread){
    int ret = 0;
    struct binder_proc *proc = filp->private_data;
    unsigned int size = _IOC_SIZE(cmd);
    //用戶傳下來的數據賦值給 ubuf
    void __user *ubuf = (void __user *)arg; 
    struct binder_write_read bwr;
    //把用戶空間數據 ubuf 拷貝到 bwr
    if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
     ret = -EFAULT;
     goto out;
    }
    暫時忽略處理數據邏輯...
    //將讀寫後的數據寫回給用戶空間
    if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
     ret = -EFAULT;
     goto out;
    }
out:
 return ret;
}

起初看到 copy_from_user() 方法時難以理解,因爲它看起來是將我們要傳輸的數據拷貝到內核空間了,但目前還沒有看到 server 端的任何線索,bwr 跟 server 端沒有映射關係,那後續再將 bwr 傳輸給 server 端的時候又要拷貝,這樣豈不是多次拷貝了?

其實這裏的 copy_from_user() 方法並沒有拷貝要傳輸的數據,而僅是拷貝了持有傳輸數據內存地址的 bwr。後續處理數據時會根據 bwr 信息真正的去拷貝要傳輸的數據。

處理完數據後,會將處理結果體現在 bwr 中,然後返回給用戶空間處理。那是如何處理數據的呢?所謂的處理數據,就是對數據的讀寫而已:

    if (bwr.write_size > 0) {//寫數據
     ret = binder_thread_write(proc, 
             thread,
             bwr.write_buffer, bwr.write_size,
             &bwr.write_consumed);
        trace_binder_write_done(ret);
        if (ret < 0) { //寫失敗
            bwr.read_consumed = 0;
            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
                ret = -EFAULT;
            goto out;
        }
    }
    if (bwr.read_size > 0) {//讀數據
     ret = binder_thread_read(proc, thread, bwr.read_buffer,
            bwr.read_size,
            &bwr.read_consumed,
            filp->f_flags & O_NONBLOCK);
        trace_binder_read_done(ret);
        if (!list_empty(&proc->todo))
            //喚醒等待狀態的線程
            wake_up_interruptible(&proc->wait);
        if (ret < 0) { //讀失敗
            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
                ret = -EFAULT;
            goto out;
        }
    }

可見 binder 驅動內部依賴用戶空間的 binder_write_read 決定是要讀取還是寫入數據:其內部變量 read_size>0 則代表要讀取數據,write_size>0 代表要寫入數據,若都大於 0 則先寫入,後讀取。

至此焦點應該集中在 binder_thread_write() 和 binder_thread_read(),下面分析這兩個方法。

4.binder_thread_write

在上面的 binder_ioctl_write_read() 方法中調用 binder_thread_write() 時傳入了 bwr.write_buffer、bwr.write_size 等,先搞清楚這些參數是什麼。

最開始是在用戶空間 IPCThreadState 的 transact() 中通過 writeTransactionData() 方法創建數據並寫入 mOut 的,writeTransactionData 方法代碼如下:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer){
    binder_transaction_data tr; //到驅動內部後會取出此結構體進行處理
    tr.target.ptr = 0;
    tr.target.handle = handle; //目標 server 的 binder 的句柄
    //請求碼,getService() 服務對應的是 GET_SERVICE_TRANSACTION
    tr.code = code; 
    tr.flags = binderFlags;
    tr.cookie = 0;
    tr.sender_pid = 0;
    tr.sender_euid = 0;
    const status_t err = data.errorCheck(); //驗證數據合理性
    if (err == NO_ERROR) {
        tr.data_size = data.ipcDataSize(); //傳輸數據大小
        tr.data.ptr.buffer = data.ipcData(); //傳輸數據
        tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
        tr.data.ptr.offsets = data.ipcObjects();
    } else {...}
    mOut.writeInt32(cmd); // transact 傳入的 cmd 是 BC_TRANSACTION
    mOut.write(&tr, sizeof(tr)); //打包成 binder_transaction_data
    return NO_ERROR;
}

然後在 IPCThreadState 的 talkWithDriver() 方法中對 write_buffer 賦值:

    bwr.write_buffer = (uintptr_t)mOut.data();

搞清楚了數據的來源,再來看 binder_thread_write() 方法,binder_thread_write() 方法中處理了大量的 BC_XXX 命令,代碼很長,這裏我們只關注當前正在處理的 BC_TRANSACTION 命令,簡化後代碼如下:

static int binder_thread_write(struct binder_proc *proc,
        struct binder_thread *thread,
        binder_uintptr_t binder_buffer, size_t size,
        binder_size_t *consumed){
    uint32_t cmd;
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer; 
    void __user *ptr = buffer + *consumed; //數據起始地址
    void __user *end = buffer + size; //數據結束地址
    //可能有多個命令及對應數據要處理,所以要循環
    while (ptr < end && thread->return_error == BR_OK) { 
        if (get_user(cmd, (uint32_t __user *)ptr)) // 讀取一個 cmd
            return -EFAULT;
        //跳過 cmd 所佔的空間,指向要處理的數據
        ptr += sizeof(uint32_t); 
        switch (cmd) {
            case BC_TRANSACTION:
            case BC_REPLY: {
                 //與 writeTransactionData 中準備的數據結構體對應
                 struct binder_transaction_data tr; 
                 //拷貝到內核空間 tr 中
                 if (copy_from_user(&tr, ptr, sizeof(tr))) 
                    return -EFAULT;
                 //跳過數據所佔空間,指向下一個 cmd
                 ptr += sizeof(tr); 
                 //處理數據
                 binder_transaction(proc, thread, &tr, cmd == BC_REPLY); 
                 break;
            }
            處理其他 BC_XX 命令...
        }
    //被寫入處理消耗的數據量,對應於用戶空間的 bwr.write_consumed
    *consumed = ptr - buffer;

binder_thread_write() 中從 bwr.write_buffer 中取出了 cmd 和 cmd 對應的數據,進一步交給 binder_transaction() 處理,需要注意的是,BC_TRANSACTION、BC_REPLY 這兩個命令都是由 binder_transaction() 處理的。

簡單梳理一下,由 binder_ioctl -> binder_ioctl_write_read -> binder_thread_write ,到目前爲止還只是在準備數據,沒有看到跟目標進程相關的任何處理,都屬於 "準備數據,根據命令分發給具體的方法去處理" 第 1 個工作。

而到此爲止,第 1 個工作便結束,下一步的 binder_transaction() 方法終於要開始後面的工作了。

5.binder_transaction

binder_transaction() 方法中代碼較長,先總結它幹了哪些事:對應開頭列出的工作,此方法中做了非常關鍵的 2-4 步:

  1. 找到目標進程的相關信息
  2. 將數據一次拷貝到目標進程所映射的物理內存塊
  3. 記錄待處理的任務,喚醒目標線程

以這些工作爲線索,將代碼分爲對應的部分來看,首先是「找到目標進程的相關信息」,簡化後代碼如下:

static void binder_transaction(struct binder_proc *proc,
          struct binder_thread *thread,
          struct binder_transaction_data *tr, int reply){
    struct binder_transaction *t; //用於描述本次 server 端要進行的 transaction
    struct binder_work *tcomplete; //用於描述當前調用線程未完成的 transaction
    binder_size_t *offp, *off_end;
    struct binder_proc *target_proc; //目標進程
    struct binder_thread *target_thread = NULL; //目標線程
    struct binder_node *target_node = NULL; //目標 binder 節點
    struct list_head *target_list; //目標 TODO 隊列
    wait_queue_head_t *target_wait; //目標等待隊列
    if(reply){ 
        in_reply_to = thread->transaction_stack;
        ...處理 BC_REPLY,暫不關注
    }else{ 
        //處理 BC_TRANSACTION
        if (tr->target.handle) { //handle 不爲 0
            struct binder_ref *ref;
            //根據 handle 找到目標 binder 實體節點的引用
            ref = binder_get_ref(proc, tr->target.handle);
            target_node = ref->node; //拿到目標 binder 節點
        } else { 
            // handle 爲 0 則代表目標 binder 是 service manager
            // 對於本次調用來說目標就是 service manager
            target_node = binder_context_mgr_node;
        }
    }
    target_proc = target_node->proc; //拿到目標進程
    if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) {
     struct binder_transaction *tmp;
     tmp = thread->transaction_stack;
     while (tmp) {
            if (tmp->from && tmp->from->proc == target_proc)
                target_thread = tmp->from; //拿到目標線程
            tmp = tmp->from_parent;
     }
    }
    target_list = &target_thread->todo; //拿到目標 TODO 隊列
    target_wait = &target_thread->wait; //拿到目標等待隊列

binder_transaction、binder_work 等結構體在上一篇中有介紹,上面代碼中也詳細註釋了它們的含義。比較關鍵的是 binder_get_ref() 方法,它是如何找到目標 binder 的呢?這裏暫不延伸,下文再做分析。

繼續看 binder_transaction() 方法的第 2 個工作,「將數據一次拷貝到目標進程所映射的物理內存塊」

    t = kzalloc(sizeof(*t), GFP_KERNEL); //創建用於描述本次 server 端要進行的 transaction
    tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL); //創建用於描述當前調用線程未完成的 transaction
    if (!reply && !(tr->flags & TF_ONE_WAY)) //將信息記錄到 t 中:
        t->from = thread; //記錄調用線程
    else
        t->from = NULL;
    t->sender_euid = task_euid(proc->tsk);
    t->to_proc = target_proc; //記錄目標進程
    t->to_thread = target_thread; //記錄目標線程
    t->code = tr->code; //記錄請求碼,getService() 對應的是 GET_SERVICE_TRANSACTION
    t->flags = tr->flags;
    //實際申請目標進程所映射的物理內存,準備接收要傳輸的數據
    t->buffer = binder_alloc_buf(target_proc, tr->data_size,
        tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
    //申請到 t->buffer 後,從用戶空間將數據拷貝進來,這裏就是一次拷貝數據的地方!!
    if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
        tr->data.ptr.buffer, tr->data_size)) {
        return_error = BR_FAILED_REPLY;
        goto err_copy_data_failed;
    }

爲什麼在拷貝之前要先申請物理內存呢?之前介紹 binder_mmap() 方法時詳細分析過,雖然 binder_mmap() 直接映射了 (1M-8K) 的虛擬內存,但卻只申請了 1 頁的物理頁面,等到實際使用時再動態申請。也就是說,在 binder_ioctl() 實際傳輸數據的時候,再通過 binder_alloc_buf() 方法去申請物理內存。

至此已經將要傳輸的數據拷貝到目標進程,目標進程可以直接讀取到了,接下來要做的就是將目標進程要處理的任務記錄起來,然後喚醒目標進程,這樣在目標進程被喚醒後,才能知道要處理什麼任務。

最後來看 binder_transaction() 方法的第 3 個工作,「記錄待處理的任務,喚醒目標線程」

 if (reply) { //如果是處理 BC_REPLY,pop 出來棧頂記錄的 transaction(實際上是刪除鏈表頭元素)
  binder_pop_transaction(target_thread, in_reply_to);
 } else if (!(t->flags & TF_ONE_WAY)) {
        //如果不是 oneway,將 server 端要處理的 transaction 記錄到當前調用線程
  t->need_reply = 1;
  t->from_parent = thread->transaction_stack;
  thread->transaction_stack = t;
 } else {
  ...暫不關注 oneway 的情況
 }
 t->work.type = BINDER_WORK_TRANSACTION;
    list_add_tail(&t->work.entry, target_list); //加入目標的處理隊列中
    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; //設置調用線程待處理的任務類型
    list_add_tail(&tcomplete->entry, &thread->todo); //記錄調用線程待處理的任務
    if (target_wait)
        wake_up_interruptible(target_wait); //喚醒目標線程

再次梳理一下,至此已經完成了前四個工作:

  1. 準備數據,根據命令分發給具體的方法去處理
  2. 找到目標進程的相關信息
  3. 將數據一次拷貝到目標進程所映射的物理內存塊
  4. 記錄待處理的任務,喚醒目標線程

其中第 1 個工作涉及到的方法爲 binder_ioctl() -> binder_get_thread() -> binder_ioctl_write_read() -> binder_thread_write() ,主要是一些數據的準備和方法轉跳,沒做什麼實質的事情。而 binder_transaction() 方法中做了非常重要的 2-4 工作。

剩下的工作還有:

  1. 調用線程進入休眠
  2. 目標進程直接拿到數據進行處理,處理完後喚醒調用線程
  3. 調用線程返回處理結果

可以想到,5 和 6 其實沒有時序上的限制,而是並行處理的。下面先來看第 5 個工作:調用線程是如何進入休眠等待服務端執行結果的。

6.binder_thread_read

在喚醒目標線程後,調用線程就執行完 binder_thread_write() 寫完了數據,返回到 binder_ioctl_write_read() 方法中,接着執行 binder_thread_read() 方法。

而調用線程的休眠就是在此方法中觸發的,下面將 binder_thread_read() 分爲兩部分來看,首先是是否阻塞當前線程的判斷邏輯:

static int binder_thread_read(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed, int non_block){
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer; //bwr.read_buffer
    void __user *ptr = buffer + *consumed; //數據起始地址
    void __user *end = buffer + size; //數據結束地址
    if (*consumed == 0) {
        if (put_user(BR_NOOP, (uint32_t __user *)ptr))
            return -EFAULT;
        ptr += sizeof(uint32_t);
    }
    //是否要準備睡眠當前線程
    wait_for_proc_work = thread->transaction_stack == NULL &&
            list_empty(&thread->todo);
    if (wait_for_proc_work) {
        if (non_block) { //non_block 爲 false
            if (!binder_has_proc_work(proc, thread))
                ret = -EAGAIN;
        } else
         ret = wait_event_freezable_exclusive(proc->wait, 
                        binder_has_proc_work(proc, thread));
    } else {
        if (non_block) { //non_block 爲 false
            if (!binder_has_thread_work(thread))
                ret = -EAGAIN;
        } else
            ret = wait_event_freezable(thread->wait, 
                        binder_has_thread_work(thread));
    }

consumed 即用戶空間的 bwr.read_consumed,這裏是 0 ,所以將一個 BR_NOOP 加到了 ptr 中。

怎麼理解 wait_for_proc_work 條件呢?在 binder_transaction() 方法中將 server 端要處理的 transaction 記錄到了當前調用線程 thread->transaction_stack 中;將當前調用線程待處理的任務記錄到了 thread->todo 中。

所以這裏的 thread->transaction_stack 和 thread->todo 都不爲空,wait_for_proc_work 爲 false,代表不準備阻塞當前線程。

但 wait_for_proc_work 並不是決定是否睡眠的最終條件,接着往下看,其中 non_block 恆爲 false,那是否要睡眠當前線程就取決於 binder_has_thread_work() 的返回值,binder_has_thread_work() 方法如下:

static int binder_has_thread_work(struct binder_thread *thread){
    return !list_empty(&thread->todo) || thread->return_error != BR_OK ||
        (thread->looper & BINDER_LOOPER_STATE_NEED_RETURN);
}

thread->todo 不爲空,所以 binder_has_thread_work() 返回 true,當前調用線程不進入休眠,繼續往下執行。你可能會有疑問,不是說調用線程的休眠就是在 binder_thread_read() 方法中觸發的嗎?確實是,只不過不是本次,先接着分析 binder_thread_read() 繼續往下要執行的邏輯:

struct binder_work *w;
w = list_first_entry(&thread->todo, struct binder_work,entry);
switch (w->type) {
    case BINDER_WORK_TRANSACTION_COMPLETE: {
        cmd = BR_TRANSACTION_COMPLETE;
        if (put_user(cmd, (uint32_t __user *)ptr))
            return -EFAULT;
        ptr += sizeof(uint32_t);
        binder_stat_br(proc, thread, cmd);
        list_del(&w->entry); //刪除 binder_work 在 thread->todo 中的引用
        kfree(w);
    }
    case BINDER_WORK_NODE{...}
    case BINDER_WORK_DEAD_BINDER{...}
    ...

在上面 binder_transaction() 方法最後,將 BINDER_WORK_TRANSACTION_COMPLETE 類型的 binder_work 加入到 thread->todo 中。而這裏就是對這個 binder_work 進行處理,將一個 BR_TRANSACTION_COMPLETE 命令加到了 ptr 中。

梳理一下目前的邏輯,至此已經順序執行完 binder_thread_write()、binder_thread_read() 方法,並且在 binder_thread_read() 中往用戶空間傳輸了兩個命令:BR_NOOP 和 BR_TRANSACTION_COMPLETE。

本次 binder_ioctl() 調用就執行完了,然後會回到 IPCThreadState 中,這裏簡單概括一下後續執行的邏輯:

mIn 中有 BR_NOOP 和 BR_TRANSACTION_COMPLETE 兩個命令,首先處理 BR_NOOP 命令,此命令什麼也沒做,由於 talkWithDriver() 處於 while 循環中,會再一次進入 talkWithDriver(),但因爲此時 mIn 中還有數據沒讀完,不會調用 binder_ioctl()。

然後處理 BR_TRANSACTION_COMPLETE 命令,如果是 oneway 就直接結束本次 IPC 調用,否則再一次進入 talkWithDriver(),第二次進入 talkWithDriver 時,bwr.write_size = 0,bwr.read_size > 0,所以會第二次調用 binder_ioctl() 方法。第二次執行 binder_ioctl() 時,bwr.write_size = 0,bwr.read_size > 0,所以不會再執行 binder_thread_write() 方法,而只執行 binder_thread_read() 方法。

第二次執行 binder_thread_read() 時,thread->todo 已經被處理爲空,但是 thread->transaction_stack 還不爲空,wait_for_proc_work 仍然爲 false,但最終決定是否要休眠的條件成立了: binder_has_thread_work(thread) 返回 false,由此當前調用線程通過 wait_event_freezable() 進入休眠。

最後

至此還剩下兩個工作:

  1. 目標進程直接拿到數據進行處理,處理完後喚醒調用線程
  2. 調用線程返回處理結果

但是已經不用再看代碼了,因爲上述方法已經覆蓋了剩下的工作。對於 getService() 來說,目標進程就是 Service Manager。

最後上圖來概括 binder 驅動所承擔的工作。調用進程邏輯:

Service Manager 端邏輯:

本節完整的分析了一次 IPC 調用中 binder 驅動內部具體的執行邏輯,此部分也是 binder 機制中最難的,而將最難的部分掌握後,可以極大的提高信心。

面試前做好準備戰!

接下來將分享面試的一個複習路線,如果你也在準備面試但是不知道怎麼高效複習,可以參考一下我的複習路線,有任何問題也歡迎一起互相交流,加油吧!

這裏給大家提供一個方向,進行體系化的學習:

1、看視頻進行系統學習

前幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰鬥機,也正因爲Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的。我差的是系統知識,差的結構框架和思路,所以通過視頻來學習,效果更好,也更全面。關於視頻學習,個人可以推薦去B站進行學習,B站上有很多學習視頻,唯一的缺點就是免費的容易過時。

另外,我自己也珍藏了好幾套視頻,有需要的我也可以分享給你。

2、進行系統梳理知識,提升儲備

客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。

系統學習方向:

  • 架構師築基必備技能:深入Java泛型+註解深入淺出+併發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO

  • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化

  • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化

  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack

  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發

  • 微信小程序:小程序介紹+UI開發+API操作+微信對接

  • Hybrid 開發與Flutter:Html5項目實戰+Flutter進階

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

3、讀源碼,看實戰筆記,學習大神思路

“編程語言是程序員的表達的方式,而架構是程序員對世界的認知”。所以,程序員要想快速認知並學習架構,讀源碼是必不可少的。閱讀源碼,是解決問題 + 理解事物,更重要的:看到源碼背後的想法;程序員說:讀萬行源碼,行萬種實踐。

主要內含微信 MMKV 源碼、AsyncTask 源碼、Volley 源碼、Retrofit源碼、OkHttp 源碼等等。

4、面試前夕,刷題衝刺

面試的前一週時間內,就可以開始刷題衝刺了。請記住,刷題的時候,技術的優先,算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎麼會問。

關於面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:

還有耗時一年多整理的一系列Android學習資源:Android源碼解析、Android第三方庫源碼筆記、Android進階架構師七大專題學習、歷年BAT面試題解析包、Android大佬學習筆記等等。

以上這些內容均免費分享給大家,需要完整版的朋友,點這裏可以看到全部內容。或者點擊 【這裏】 查看獲取方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章