反思|Android 輸入系統 & ANR機制的設計與實現

反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏html

概述

對於Android開發者而言,ANR是一個老生常談的問題,站在面試者的角度,彷佛說出 「不要在主線程作耗時操做」 就算合格了。java

可是,ANR機制究竟是什麼,其背後的原理究竟如何,爲何要設計出這樣的機制?這些問題時時刻刻會縈繞腦海,而想搞清楚這些,就不得不提到Android自身的 輸入系統Input System)。android

Android自身的 輸入系統 又是什麼?一言以蔽之,任何與Android設備的交互——咱們稱之爲 輸入事件,都須要經過 輸入系統 進行管理和分發;這其中最靠近上層,而且最典型的一個小環節就是View事件分發 流程。git

這樣看來,輸入系統 自己確實是一個很是龐大複雜的命題,而且,越靠近底層細節,越容易有一種 只見樹木不見樹林 之感,反覆幾回,直至迷失在細節代碼的較真中,一次學習的努力嘗試付諸東流。github

所以,控制住原理分析的粒度,在宏觀的角度,系統地瞭解輸入系統自己的設計理念,並引伸到實際開發中的ANR現象的原理和解決思路 ,是一個很是不錯的理論與實踐相結合的學習方式,這也正是筆者寫做本文的初衷。面試

本文篇幅較長,思惟導圖以下:安全

1、自頂向下探索

談到Android系統自己,首先,必須將 應用進程系統進程 有一個清晰的認知,前者通常表明開發者依託Android平臺自己創造開發的應用;後者則表明 Android系統自身建立的核心進程。markdown

這裏咱們拋開 應用進程 ,先將視線轉向 系統進程,由於 輸入系統 自己是由後者初始化和管理調度的。數據結構

Android系統在啓動的時候,會初始化zygote進程和由zygote進程fork出來的SystemServer進程;做爲 系統進程 之一,SystemServer進程會提供一系列的系統服務,而接下來要講到的InputManagerService也正是由 SystemServer 提供的。多線程

SystemServer的初始化過程當中,InputManagerService(下稱IMS)和WindowManagerService(下稱WMS)被建立出來;其中WMS自己的建立依賴IMS對象的注入:

// SystemServer.java
private void startOtherServices() {
 // ...
 InputManagerService inputManager = new InputManagerService(context);
 // inputManager做爲WindowManagerService的構造參數
 WindowManagerService wm = WindowManagerService.main(context,inputManager, ...);
}
複製代碼

輸入系統 中,WMS很是重要,其負責管理IMSWindowActivityManager之間的通訊,這裏點到爲止,後文再進行補充,咱們先來看IMS

顧名思義,IMS服務的做用就是負責輸入模塊在Java層級的初始化,並經過JNI調用,在Native層進行更下層輸入子系統相關功能的建立和預處理。

JNI的調用過程當中,IMS建立了NativeInputManager實例,NativeInputManager則在初始化流程中又建立了EventHubInputManager:

NativeInputManager::NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper) : mLooper(looper), mInteractive(true) {
    // ...
    // 建立一個EventHub對象
    sp<EventHub> eventHub = new EventHub();
    // 建立一個InputManager對象
    mInputManager = new InputManager(eventHub, this, this);
}
複製代碼

此時咱們已經處於Native層級。讀者須要注意,對於整個Native層級而言,其向下負責與Linux的設備節點中獲取輸入,向上則與靠近用戶的Java層級相通訊,能夠說是很是重要。而在該層級中,EventHubInputManager又是最核心的兩個角色。

這兩個角色的職責又是什麼呢?首先來講EventHub,它是底層 輸入子系統 中的核心類,負責從物理輸入設備中不斷讀取事件(Event),而後交給InputManager,後者內部封裝了InputReaderInputDispatcher,用來從EventHub中讀取事件和分發事件:

InputManager::InputManager(...) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}
複製代碼

簡單來看,EventHub創建了Linux與輸入設備之間的通訊,InputManager中的InputReaderInputDispatcher負責了輸入事件的讀取和分發,在 輸入系統 中,二者的確很是重要。

這裏借用網上的圖對此進行一個簡單的歸納:

2、EventHub 與 epoll 機制

對於EventHub的具體實現,絕大多數App開發者也許並不須要去花太多時間深刻——簡單瞭解其職責,而後一筆帶過彷佛是筆劃算的買賣。

可是在EventHub的實現細節中筆者發現,其對epoll機制的利用是一個很是經典的學習案例,所以,花時間稍微深刻了解也絕對是一箭雙鵰。

上文說到,EventHub創建了Linux與輸入設備之間的通訊,其實這種描述是不許確的,那麼,EventHub是爲了解決什麼問題而設計的呢,其具體又是如何實現的?

一、多輸入設備與輸入子系統

咱們知道,Android設備能夠同時鏈接多個輸入設備,好比 屏幕鍵盤鼠標 等等,用戶在任意設備上的輸入都會產生一箇中斷,經由Linux內核的中斷處理及設備驅動轉換成一個Event,最終交給用戶空間的應用程序進行處理。

Linux內核提供了一個便於將不一樣設備不一樣數據接口統一轉換的抽象層,只要底層輸入設備驅動程序按照這層抽象接口實現,應用就能夠經過統一接口訪問全部輸入設備,這即是Linux內核的 輸入子系統

那麼 輸入子系統 如何是針對接收到的Event進行的處理呢?這就不得不提到EventHub了,它是底層Event處理的樞紐,其利用了epoll機制,不斷接收到輸入事件Event,而後將其向上層的InputReader傳遞。

二、什麼是epoll機制

這是常見於面試Handler相關知識點時的一道進階題,變種問法是:「既然Handler中的Looper中經過一個死循環不斷輪詢,爲何程序沒有由於無限死循環致使崩潰或者ANR?」

讀者應該知道,Handler簡單的利用了epoll機制,作到了消息隊列的阻塞和喚醒。關於epoll機制,這裏有一篇很是經典的解釋,不瞭解其設計理念的讀者 有必要 瞭解一下:

知乎:epoll或者kqueue的原理是什麼?

參考上文,這裏咱們對epoll機制進行一個簡單的總結:

epoll能夠理解爲event poll,不一樣於忙輪詢和無差異輪詢,在 多個輸入流 的狀況下,epoll只會把哪一個流發生了怎樣的I/O事件通知咱們。此時咱們對這些流的操做都是有意義的。

EventHub中使用epoll的恰到好處——多個物理輸入設備對應了多個不一樣的輸入流,經過epoll機制,在EventHub初始化時,分別建立mEpollFdmINotifyFd;前者用於監聽設備節點是否有設備文件的增刪,後者用於監聽是否有可讀事件,建立管道,讓InputReader來讀取事件:

3、事件的讀取和分發

本章節將對InputReaderInputDispatcher進行系統性的介紹。

一、InputReader:讀取事件

InputReader是什麼?簡單理解InputReader的做用,經過從EventHub獲取事件後,將事件進行對應的處理,而後將事件進行封裝並添加到InputDispatcher的隊列中,最後喚醒InputDispatcher進行下一步的事件分發。

乍得一看,在 輸入系統Native層中,InputReader彷佛平凡無奇,但越是看似樸實無華的事物,在整個流程中每每佔據絕對重要的做用。

首先,EventHub傳過來的Event除了普通的 輸入事件 外,還包含了設備自己的增、刪、掃描 等事件,這些額外的事件處理並無直接交給InputDispatcher去分發,而是在InputReader中進行了處理。

當某個時間發生——多是用戶 按鍵輸入,或者某個 設備插入,亦或 設備屬性被調整epoll_wait()返回並將Event存入。

這以後,InputReader對輸入事件進行了一次讀取,由於不一樣設備對事件的處理邏輯又各自不一樣,所以InputReader內部持有一系列的Mapper對事件進行 匹配 ,若是不匹配則忽略事件,反之則將Event封裝成一個新的NotifyArgs數據對象,準備存入隊列中,即喚醒InputDispatcher進行分發。

巧妙的是,在喚醒InputDispatcher進行分發以前,InputReader在本身的線程中先執行了一個很特殊的 攔截操做 環節。

二、輸入事件的攔截和轉換

讀者知道,在應用開發中,一些特殊的輸入事件是沒法經過普通的方式進行攔截的;好比音量鍵,Power鍵,電話鍵,以及一些特殊的組合鍵,這裏咱們通稱爲 系統按鍵

這點無可厚非,雖然Android系統對於開發者足夠的開放,可是一切都是有限制的,絕大多數的 用戶按鍵 一般能夠被應用攔截處理,可是 系統按鍵 絕對不行——這種限制每每可以給予用戶設備安全最後的保障。

所以,在InputReader喚醒InputDispatcher進行事件分發以前,InputReader在本身的線程中進行了兩輪攔截處理。

首先的第一輪攔截操做就是對 系統按鍵 級別的 輸入事件 進行處理,對於手機而言,這個工做是在PhoneWindowManager中完成;舉例來講,當用戶按了Power(電源)鍵,Android設備自己會切喚醒或睡眠——即亮屏和息屏。

這也正是「在技術論壇中,一般對 系統按鍵 攔截處理的技術方案,基本都是須要修改PhoneWindowManager的源碼」的緣由。

接下來輸入事件進入到第二輪的處理中,若是用戶在Setting->Accessibility中選擇打開某些功能,以 手勢識別 爲例,AndroidAccessbilityManagerService(輔助功能服務) 可能會根據須要轉換成新的Event,好比說兩根手指頭捏動的手勢最終會變成ZoomEvent

須要注意的是,這裏的攔截處理並不會真正將事件 消費 掉,而是經過特殊的方式將事件進行標記(policyFlags),而後在InputDispatcher中處理。

至此,InputReader輸入事件 完整的一輪處理到此結束,這以後,InputReader又進入了新一輪等待。

三、InputDispatcher:分發事件

wake()函數將在Looper中睡眠等待的InputDispatcher喚醒時,InputDispatcher開始新一輪事件的分發。

準確來講,InputDispatcher被喚醒時,wake()函數實際是在InputManagerService的線程中執行的,即整個流程的線程切換順序爲InputReaderThread -> InputManagerServiceThread -> InputDispatcherThread

InputDispatcher的線程負責將接收到的 輸入事件 分發給 目標應用窗口,在這個過程當中,InputDispatcher首先須要對上個環節中標記了須要攔截的 系統按鍵 相關事件進行攔截,被攔截的事件至此再也不向下分發。

這以後,InputDispatcher進入了本文最關鍵的一個環節——調用 findFocusedWindowTargetLocked()獲取當前的 焦點窗口 ,同時檢測目標應用是否有ANR發生。

若是檢測到目標窗口處於正常狀態,即ANR並未發生時,InputDispatcher進入真正的分發程序,將事件對象進行新一輪的封裝,經過SocketPair喚醒目標窗口所在進程的Looper線程,即咱們應用進程中的主線程,後者會讀取相應的鍵值並進行處理。

表面來看,整個分發流程彷佛乾淨簡潔且便於理解,但實際上InputDispatcher整個流程的邏輯十分複雜,試想一次事件分發要橫跨3個線程的流程又怎會簡單?

此外,InputDispatcher還負責了 ANR 的處理,這又致使整個流程的複雜度又上升了一個層級,這個流程咱們在後文的ANR章節中進行更細緻的分析,所以先按住不提。

接下來,咱們來看看整個 輸入事件 的分發流程中, 應用進程 是如何與 系統進程 創建相應的通訊連接的。

四、經過Socket創建通訊

關於 跨進程通訊的創建 這一節,筆者最初打算做爲一個大的章節來說,可是對於整個 輸入系統 而言,其彷佛又只是一個 重要非必需 的知識點。最終,筆者將其放在一個小節中進行簡單的描述,有興趣的讀者能夠在文末的參考連接中查閱更詳盡的資料。

咱們知道,InputReaderInputDispatcher運行在system_server 系統進程 中,而用戶操做的應用都運行在本身的 應用進程 中;這裏就涉及到跨進程通訊,那麼 應用進程 是如何與 系統進程 創建通訊的呢?

讓咱們回到文章最初WindowManagerService(WMS)InputManagerService(IMS)初始化的流程中來,當IMS以及其餘的系統服務初始化完成以後,應用程序開始啓動。

若是一個應用程序有Activity(只有Activity可以接受用戶輸入),那麼它要將本身的Window註冊到WMS中。

在這裏,Android使用了Socket而不是Binder來完成。WMS中經過OpenInputChannelPair生成了兩個SocketFD, 表明一個雙向通道的兩端:向一端寫入數據,另一端即可以讀出;反之,若是一端沒有寫入數據,另一端去讀,則陷入阻塞等待。

最終InputDispatcher中創建了目標應用的Connection對象,表明與遠端應用的窗口創建了連接;一樣,應用進程中的ViewRootImpl建立了WindowInputEventReceiver用於接受InputDispatchor傳過來的事件:

這裏咱們對該次 跨進程通訊創建流程 有了初步的認知,對於Android系統而言,Binder是最普遍的跨進程通訊的應用方式,可是Android系中跨進程通訊就僅僅只用到了Binder嗎?答案是否認的,至少在 輸入系統 中,除了Binder以外,Socket一樣起到了舉足輕重的做用。

那麼新的問題就來了,這裏爲何選擇Socket而不是選擇Binder呢,關於這個問題的解釋,筆者找到了一個很好的版本:

Socket能夠實現異步的通知,且只須要兩個線程參與(Pipe兩端各一個),假設系統有N個應用程序,跟輸入處理相關的線程數目是 N+1 (1Input Dispatcher線程)。然而,若是用Binder實現的話,爲了實現異步接收,每一個應用程序須要兩個線程,一個Binder線程,一個後臺處理線程(不能在Binder線程裏處理輸入,由於這樣太耗時,將會堵塞住發送端的調用線程)。在發送端,一樣須要兩個線程,一個發送線程,一個接收線程來接收應用的完成通知,因此,N個應用程序須要 2(N+1)個線程。相比之下,Socket仍是高效多了。

如今,應用進程 可以收到由InputDispatcher處理完成並分發過來的 輸入事件 了。至此,咱們來到了最熟悉的應用層級事件分發流程。對於這以後 應用層級的事件分發,能夠閱讀下述筆者的另外兩篇文章,本文不贅述。

4、ANR機制的設計與實現

輸入系統 有了更初步總體的認知以後,接下來本文將針對ANR機制進行更深一步的探索。

一般來說,ANR的來源分爲Service、Broadcast、Provider以及Input兩種。

這樣區分的緣由是,首先,前者發生在 應用進程 組件中的ANR問題一般是相對好解決的,若ANR自己容易復現,開發者一般僅須要肯定組件的代碼中是否在 主線程中作了耗時處理;然後者ANR發生的緣由爲 輸入事件 分發超時,包括按鍵和屏幕的觸摸事件,經過閱讀上一章節,讀者知道 輸入系統 中負責處理ANR問題的是處於 系統進程 中的InputDispatcher,其整個流程相比前者而言邏輯更加複雜。

簡單理解了以後,讀者須要知道,「組件類ANR發生緣由一般是因爲 主線程中作了耗時處理」這種說法其實是籠統的,更準確的講,其本質的緣由是 組件任務調度超時,而在設備資源緊湊的狀況下,ANR的發生更可能是綜合性的緣由。

Input類型的ANR相對於Service、Broadcast、Provider,其內部的機制又大相徑庭。

一、第一類原理概述

具體不一樣在哪裏呢,對於Service、Broadcast、Provider組件類的ANR而言,Gityuan這篇文章 中作了一個很是精妙的解釋:

ANR是一套監控Android應用響應是否及時的機制,能夠把發生ANR比做是 引爆炸彈,那麼整個流程包含三部分組成:

  • 埋定時炸彈:中控系統(system_server進程)啓動倒計時,在規定時間內若是目標(應用進程)沒有幹完全部的活,則中控系統會定向炸燬(殺進程)目標。
  • 拆炸彈:在規定的時間內幹完工地的全部活,並及時向中控系統報告完成,請求解除定時炸彈,則倖免於難。
  • 引爆炸彈:中控系統當即封裝現場,抓取快照,蒐集目標執行慢的罪證(traces),便於後續的案件偵破(調試分析),最後是炸燬目標。

將組件的ANR機制比喻爲 定時炸彈 很是貼切,以Service爲例,對於Android系統而言,啓動一個服務其本質是進程間的異步通訊,那麼,如何判斷Service是否啓動成功,若是一直沒有成功,那麼如何處理?

所以Android設計了一個 置之死地然後生 的機制,在嘗試啓動Service時,讓中控系統system_server埋下一個 定時炸彈 ,當Service完成啓動,拆掉炸彈;不然在system_serverActivityManager線程中引爆炸彈,這就是組件類ANR機制的原理:

接下來簡單瞭解一下 輸入系統 流程中ANR機制的原理。

二、第二類原理概述

Input類型的ANR在平常開發中更爲常見且更復雜,好比用戶或者測試反饋,點擊屏幕中的UI元素致使「卡死」。

少數狀況下開發者可以很快定位到問題,但更常見的狀況是,該問題是 隨機難以復現 的,致使該問題的緣由也更具備綜合性,好比低端設備的系統自己資源已很是緊張,或者多線程相互持有彼此須要的資源致使 死鎖 ,亦或其它複雜的狀況,所以處理這類型問題就須要開發者對 輸入系統 中的ANR機制有必定的瞭解。

與組件類ANR不一樣的是,Input類型的超時機制並不是時間到了必定就會爆炸,而是處理後續上報事件的過程纔會去檢測是否該爆炸,因此更像是 掃雷 的過程。

什麼叫作 掃雷 呢,對於 輸入系統 而言,即便某次事件執行時間超過預期的時長,只要用戶後續沒有再生成輸入事件,那麼也不須要ANR

而只有當新一輪的輸入事件到來,此時正在分發事件的窗口(即App應用自己)遲遲沒法釋放資源給新的事件去分發,這時InputDispatcher纔會根據超時時間,動態的判斷是否須要向對應的窗口提示ANR信息。

這也正是用戶在第一次點擊屏幕,即便事件處理超時,也沒有彈出ANR窗口,而當用戶下意識再次點擊屏幕時,屏幕上才提示出了ANR信息的緣由。

因而可知,組件類ANRInput ANR原理上確實有所不一樣;除此以外,前者是在ActivityManager線程中處理的ANR信息,後者則是在InputDispatcher線程中處理的ANR,這裏經過一張圖簡單瞭解一下後者的總體流程:

如今咱們對Input類型的ANR機制有了一個簡單的瞭解,下文將針對其更深刻性的細節實現進行探討。

三、事件分發的異步機制

咱們再次將目光轉回到InputDispatcher的實現細節。

先拋出一個新的問題,對處於system_server進程Native層級的 事件分發 而言,其向下與 應用進程 的通訊的過程應該是同步仍是異步的?

對於讀者而言,不可貴出答案是異步的,由於二者之間雙向通訊的創建是經過SocketPair,而且,由於system_serverInputDispatcher對事件的分發其實是一對多的,若是是同步的,那麼一旦其中一個應用分發超時,那麼InputDispatcher線程天然被卡住,其永遠都不可能進入到下一輪的事件分發中,掃雷 機制更是無從談起。

所以,與應用進程中事件分發不一樣的是,後者咱們一般能夠認爲是在主線程中同步的,而對於整個 輸入系統 而言,由於涉及到 系統進程 與多個 應用進程 之間異步的通訊,所以其內部的實現更爲複雜。

由於事件分發涉及到異步回調機制,所以InputDispatcher須要對事件進行維護和管理,那麼問題就變成了,使用什麼樣的數據結構去維護這些輸入事件比較合適。

四、三個隊列

InputDispatcher的源碼實現中,總體的事件分發流程共使用到3個事件隊列:

  • mInBoundQueue:用於記錄InputReader發送過來的輸入事件;
  • outBoundQueue:用於記錄即將分發給目標應用窗口的輸入事件;
  • waitQueue:用於記錄已分發給目標應用,且應用還沒有處理完成的輸入事件。

下文,筆者經過2輪事件分發的示例,對三個隊列的做用進行簡單的梳理。

4.1 第一輪事件分發

首先InputReader線程經過EventHub監聽到底層的輸入事件上報,並將其放入了mInBoundQueue中,同時喚醒了InputDispatcher線程。

而後InputDispatcher開始了第一輪的事件分發,此時並無正在處理的事件,所以InputDispatchermInBoundQueue隊列頭部取出事件,並重置ANR的計時,並檢查窗口是否就緒,此時窗口準備就緒,將該事件轉移到了outBoundQueue隊列中,由於應用管道對端鏈接正常,所以事件從outBoundQueue取出,而後放入了waitQueue隊列,由於Socket雙向通訊已經創建,接下來就是 應用進程 接收到新的事件,而後對其進行分發。

若是 應用進程 事件分發正常,那麼會經過Socketsystem_server通知完成,則對應的事件最終會從waitQueue隊列中移除。

4.2 第二輪事件分發

若是第一輪事件分發還沒有接收到回調通知,第二輪事件分發抵達又是如何處理的呢?

第二輪事件到達InputDispatcher時,此時InputDispatcher發現有事件正在處理,所以不會從mInBoundQueue取出新的事件,而是直接檢查窗口是否就緒,若未就緒,則進入ANR檢測狀態。

如下幾種狀況會致使進入ANR檢測狀態:

一、目標應用不會空,而目標窗口爲空。說明應用程序在啓動過程當中出現了問題; 二、目標Activity的狀態是Pause,即再也不是Focused的應用; 三、目標窗口還在處理上一個事件。

讀者須要理解,並不是全部「目標窗口還在處理上一個事件」都會拋出ANR,而是須要經過檢測時間,若是未超時,那麼直接停止本輪事件分發,反之,若是事件分發超時,那麼纔會肯定ANR的發生。

這也正是將Input類型的ANR描述爲 掃雷 的緣由:這裏的掃雷是指當前輸入系統中正在處理着某個耗時事件的前提下,後續的每一次input事件都會檢測前一個正在處理的事件是否超時(進入掃雷狀態),檢測當前的時間距離上次輸入事件分發時間點是否超時。若是前一個輸入事件,則會重置ANRtimeout,從而不會爆炸。

至此,輸入系統 檢測到了ANR的發生,並向上層拋出了本次ANR的相關信息。

小結

本文旨在對Android 輸入系統 進行一個系統性的概述,讀者不該將本文做爲惟一的學習資料,而應該經過本文對該知識體系進行初步的瞭解,並根據自身要求進行單個方向細節性的突破。而已經掌握了骨骼架構的讀者而言,更細節性的知識點也不過是待豐富的血肉而已。

本文從立題至發佈,整個流程耗時近1個半月,在這個過程當中,筆者參考了較本文內容數十倍的資料,受益頗深,也深感以 舉重若輕 爲寫文目標之艱難——內容鋪展容易,但經過 簡潔連貫 的語言來對一個龐大複雜的知識體系進行收攏,須要極強的 剋制力 ,在這種嚴苛的要求下,每一句的描述都須要極高的 精確性 ,這對筆者而言是一個挑戰,但真正完成以後,對整個知識體系的理解程度一樣也是極高的。

而這也正是 反思 系列的初衷,但願你能喜歡。

參考 & 擴展閱讀

正如上文所言,輸入系統ANR 自己都是一個很是大的命題,除了寬廣的知識體系,還須要親身去實踐和總結,下文列出若干相關參考資料,讀者可根據自身需求選擇性進行擴展閱讀:

一、完全理解安卓應用無響應機制 @Gityuan
二、Input系統—ANR原理分析 @Gityuan
三、理解Android ANR的觸發原理 @Gityuan

深刻學習ANR機制資料,GityuanANR博客系列絕對是先驅級別的,尤爲是第1篇文章中,其對於 定時炸彈掃雷 的形容,貼切且易理解,這種 舉重若輕 的寫做風格體現了做者自己對整個知識體系的深度掌握;然後兩篇文章則針對兩種類型的ANR分別進行了源碼級別的分析,很是下飯。

四、圖解Android-Android的 Event Input System @漫天塵沙

筆者曾經想寫一個 圖解Android 系列,後來由於種種緣由放棄了,沒想到若干年前已經有先驅進行過了這樣的嘗試,而且,內容質量極高。筆者相信,可以花費很是大精力總結的文章必定不會被埋沒,而這篇文章,註定會成爲經典中的經典。

五、Android Input系列 @Stan_Z

一個筆者最近關注很是優秀的做者,文章很是具備深度,其Input系列針對整個輸入系統進行了更細緻源碼級別的分析,很是值得收藏。

六、Android 信號處理面面觀 之 信號定義、行爲和來源 @rambo2188

若是讀者對「Android系統信號處理的行爲」感興趣,那麼這篇文章絕對不能錯過。

七、Android開發高手課 @張紹文

實戰中的經典之做,該課程每一小結都極具深度,價值不可估量。因或涉及到利益相關,並且推薦了也從張老師那裏拿不到錢,所以本文不加連接並放在最下面(笑)。


關於我

Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 GitHub

若是您以爲文章還差了那麼點東西,也請經過 關注 督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索