本筆記整理自: https://www.gitbook.com/book/tom510230/android_ka_fa_yi_shu_tan_suo/details
參考文章:http://szysky.com/tags/#筆記、http://blog.csdn.net/player_android/article/category/6577498
聯系我:xiadeye@icloud.com
本書是一本Android進階類書籍,采用理論、源碼和實踐相結合的方式來闡述高水準的Android應用開發要點。本書從三個方面來組織內容。
- 介紹Android開發者不容易掌握的一些知識點
- 結合Android源代碼和應用層開發過程,融會貫通,介紹一些比較深入的知識點
- 介紹一些核心技術和Android的性能優化思想
目錄
第1章 Activity的生命周期和啟動模式
第2章 IPC機制
第3章 View的事件體系
第4章 View的工作原理
第5章 理解RemoteViews
第6章 Android的Drawable
第7章 Android動畫深入分析
第8章 理解Window和WindowManager
第9章 四大組件的工作過程
第10章 Android的消息機制
第11章 Android的線程和線程池
第12章 Bitmap的加載和Cache
第13章 綜合技術
第14章 JNI和NDK編程
第15章 Android性能優化
[TOC]
1 Activity的生命周期和啟動模式
1.1 Activity的生命周期全面分析
用戶正常使用情況下的生命周期 & 由于Activity被系統回收或者設備配置改變導致Activity被銷毀重建情況下的生命周期。
1.1.1 典型情況下的生命周期分析
Activity的生命周期和啟動模式

- Activity第一次啟動:onCreate->onStart->onResume。
- Activity切換到后臺( 用戶打開新的Activity或者切換到桌面) ,onPause->onStop(如果新Activity采用了透明主題,則當前Activity不會回調onstop)。
- Activity從后臺到前臺,重新可見,onRestart->onStart->onResume。
- 用戶退出Activity,onPause->onStop->onDestroy。
- onStart開始到onStop之前,Activity可見。onResume到onPause之前,Activity可以接受用戶交互。
- 在新Activity啟動之前,棧頂的Activity需要先onPause后,新Activity才能啟動。所以不能在onPause執行耗時操作。
- onstop中也不可以太耗時,資源回收和釋放可以放在onDestroy中。
1.1.2 異常情況下的生命周期分析
1 系統配置變化導致Activity銷毀重建
例如Activity處于豎屏狀態,如果突然旋轉屏幕,由于系統配置發生了改變,Activity就會被銷
毀并重新創建。
在異常情況下系統會在onStop之前調用onSaveInstanceState來保存狀態。Activity重新創建后,會在onStart之后調用onRestoreInstanceState來恢復之前保存的數據。

保存數據的流程: Activity被意外終止,調用onSaveIntanceState保存數據-> Activity委托Window,Window委托它上面的頂級容器一個ViewGroup( 可能是DecorView) 。然后頂層容器在通知所有子元素來保存數據。
這是一種委托思想,Android中類似的還有:View繪制過程、事件分發等。
系統只在Activity異常終止的時候才會調用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情況不會觸發。
2 資源內存不足導致低優先級的Activity被回收
三種Activity優先級:前臺- 可見非前臺 -后臺,從高到低。
如果一個進程沒有四大組件,那么將很快被系統殺死。因此,后臺工作最好放入service中。
android:configChanges="orientation" 在manifest中指定 configChanges 在系統配置變化后不重新創建Activity,也不會執行 onSaveInstanceState 和onRestoreInstanceState 方法,而是調用 onConfigurationChnaged 方法。
附:系統配置變化項目
configChanges 一般常用三個選項:
- locale 系統語言變化
- keyborardHidden 鍵盤的可訪問性發生了變化,比如用戶調出了鍵盤
- orientation 屏幕方向變化
1.2 Activity的啟動模式
1.2.1 Activity的LaunchMode
Android使用棧來管理Activity。
- standard
每次啟動都會重新創建一個實例,不管這個Activity在棧中是否已經存在。誰啟動了這個Activity,那么Activity就運行在啟動它的那個Activity所在的棧中。
用Application去啟動Activity時會報錯,原因是非Activity的Context沒有任務棧。解決辦法是為待啟動Activity制定FLAG_ACTIVITY_NEW_TASH標志位,這樣就會為它創建一個新的任務棧。 - singleTop
如果新Activity位于任務棧的棧頂,那么此Activity不會被重新創建,同時回調 onNewIntent 方法。onCreate和onStart方法不會被執行。 - singleTask
這是一種單實例模式。如果不存在activity所需要的任務棧,則創建一個新任務棧和新Activity實例;如果存在所需要的任務棧,不存在實例,則新創建一個Activity實例;如果存在所需要的任務棧和實例,則不創建,調用onNewIntent方法。同時使該Activity實例之上的所有Activity出棧。
參考:taskAffinity標識Activity所需要的任務棧 - singleIntance
單實例模式。具有singleTask模式的所有特性,同時具有此模式的Activity只能獨自位于一個任務棧中。
假設兩個任務棧,前臺任務棧為12,后臺任務棧為XY。Y的啟動模式是singleTask。現在請求Y,整個后臺任務棧會被切換到前臺。如圖所示:

設置啟動模式
- manifest中 設置下的 android:launchMode 屬性。
- 啟動Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 。
- 兩種同時存在時,以第二種為準。第一種方式無法直接為Activity添加FLAG_ACTIVITY_CLEAR_TOP標識,第二種方式無法指定singleInstance模式。
- 可以通過命令行 adb shell dumpsys activity 命令查看棧中的Activity信息。
1.2.2 Activity的Flags
這些FLAG可以設定啟動模式、可以影響Activity的運行狀態。
- FLAG_ACTIVITY_NEW_TASK
為Activity指定“singleTask”啟動模式。 - FLAG_ACTIVITY_SINGLE_TOP
為Activity指定“singleTop"啟動模式。 - FLAG_ACTIVITY_CLEAR_TOP
具有此標記位的Activity啟動時,同一個任務棧中位于它上面的Activity都要出棧,一般和FLAG_ACTIVITY_NEW_TASK配合使用。 - FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
如果設置,新的Activity不會在最近啟動的Activity的列表(就是安卓手機里顯示最近打開的Activity那個系統級的UI)中保存。等同于在xml中指定android:exludeFromRecents="true"屬性。
1.3 IntentFilter的匹配規則
Activity調用方式
- 顯示調用 明確指定被啟動對象的組件信息,包括包名和類名
- 隱式調用 不需要明確指定組件信息,需要Intent能夠匹配目標組件中的IntentFilter中所設置的過濾信息。
匹配規則
- IntentFilter中的過濾信息有action、category、data。
- 只有一個Intent同時匹配action類別、category類別、data類別才能成功啟動目標Activity。
- 一個Activity可以有多個intent-filter,一個Intent只要能匹配任何一組intent-filter即可成功啟動對應的Activity。
** action**
action是一個字符串,匹配是指與action的字符串完全一樣,區分大小寫。
一個intent-filter可以有多個aciton,只要Intent中的action能夠和任何一個action相同即可成功匹配。
Intent中如果沒有指定action,那么匹配失敗。
** category**
category是一個字符串。
Intent可以沒有category,但是如果你一旦有category,不管有幾個,每個都必須與intent-filter中的其中一個category相同。
系統在 startActivity 和 startActivityForResult 的時候,會默認為Intent加上 android.intent.category.DEFAULT 這個category,所以為了我們的activity能夠接收隱式調用,就必須在intent-filter中加上 android.intent.category.DEFAULT 這個category。
** data**
data的匹配規則與action一樣,如果intent-filter中定義了data,那么Intent中必須要定義可匹配的data。
intent-filter中data的語法:
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string"/>
Intent中的data有兩部分組成:mimeType和URI。mimeType是指媒體類型,比如
image/jpeg、audio/mpeg4-generic和video/等,可以表示圖片、文本、視頻等不同的媒
體格式。
URI的結構:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
實際例子
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
scheme:URI的模式,比如http、file、content等,默認值是 file 。
host:URI的主機名
port:URI的端口號
path、pathPattern和pathPrefix:這三個參數描述路徑信息。
path、pathPattern可以表示完整的路徑信息,其中pathPattern可以包含通配符 * ,表示0個或者多個任意字符。
pathPrefix只表示路徑的前綴信息。
過濾規則的uri為空時,有默認值content和file,因此intent設置uri的scheme部分必須為content或file。
Intent指定data時,必須調用 setDataAndType 方法, setData 和 setType 會清除另一方的值。
對于service和BroadcastReceiver也是同樣的匹配規則,不過對于service最好使用顯式調用。
隱式調用需注意
當通過隱式調用啟動Activity時,沒找到對應的Activity系統就會拋出 android.content.ActivityNotFoundException 異常,所以需要判斷是否有Activity能夠匹配我們的隱式Intent。
-
采用 PackageManager 的 resloveActivity 方法或Intent 的 resloveActivity 方法
public abstract List<ResolveInfo> queryIntentActivityies(Intent intent,int flags);
public abstract ResolveInfo resloveActivity(Intent intent,int flags);以上的第二個參數使用 MATCH_DEFAULT_ONLY ,這個標志位的含義是僅僅匹配那些在
intent-filter中聲明了 android.intent.category.DEFAULT 這個category的Activity。因為如果把不含這個category的Activity匹配出來了,由于不含DEFAULT這個category的Activity是無法接受隱式Intent的從而導致startActivity失敗。 下面的action和category用來表明這是一個入口Activity,并且會出現在系統的應用列表中,二者缺一不可。
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
2 IPC機制
2.1 Android IPC 簡介
- IPC即Inter-Process Communication,含義為進程間通信或者跨進程通信,是指兩個進程之間進行數據交換的過程。
- 線程是CPU調度的最小單元,是一種有限的系統資源。進程一般指一個執行單元,在PC和移動設備上是指一個程序或者應用。進程與線程是包含與被包含的關系。一個進程可以包含多個線程。最簡單的情況下一個進程只有一個線程,即主線程( 例如Android的UI線程) 。
- 任何操作系統都需要有相應的IPC機制。如Windows上的剪貼板、管道和郵槽;Linux上命名管道、共享內容、信號量等。Android中最有特色的進程間通信方式就是binder,另外還支持socket。contentProvider是Android底層實現的進程間通信。
- 在Android中,IPC的使用場景大概有以下:
- 有些模塊由于特殊原因需要運行在單獨的進程中。
- 通過多進程來獲取多份內存空間。
- 當前應用需要向其他應用獲取數據。
2.2 Android中的多進程模式
2.2.1 開啟多進程模式
在Android中使用多線程只有一種方法:給四大組件在Manifest中指定 android:process 屬性。這個屬性的值就是進程名。這意味著不能在運行時指定一個線程所在的進程。
tips:使用 adb shell ps 或 adb shell ps|grep 包名 查看當前所存在的進程信息。
兩種進程命名方式的區別
- “:remote”
“:”的含義是指在當前的進程名前面附加上當前的包名,完整的進程名為“com.example.c2.remote"。這種進程屬于當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程中。 - "com.example.c2.remote"
這是一種完整的命名方式。這種進程屬于全局進程,其他應用可以通過ShareUID方式和它跑在同一個進程中。
2.2.2 多線程模式的運行機制
Android為每個進程都分配了一個獨立的虛擬機,不同虛擬機在內存分配上有不同的地址空間,導致不同的虛擬機訪問同一個類的對象會產生多份副本。例如不同進程的Activity對靜態變量的修改,對其他進程不會造成任何影響。所有運行在不同進程的四大組件,只要它們之間需要通過內存在共享數據,都會共享失敗。四大組件之間不可能不通過中間層來共享數據。
多進程會帶來以下問題:
- 靜態成員和單例模式完全失效。
- 線程同步鎖機制完全失效。
這兩點都是因為不同進程不在同一個內存空間下,鎖的對象也不是同一個對象。 - SharedPreferences的可靠性下降。
SharedPreferences底層是 通過讀/寫XML文件實現的,并發讀/寫會導致一定幾率的數據丟失。 - Application會多次創建。
由于系統創建新的進程的同時分配獨立虛擬機,其實這就是啟動一個應用的過程。在多進程模式中,不同進程的組件擁有獨立的虛擬機、Application以及內存空間。
多進程相當于兩個不同的應用采用了SharedUID的模式
實現跨進程的方式有很多:
- Intent傳遞數據。
- 共享文件和SharedPreferences。
- 基于Binder的Messenger和AIDL。
- Socket等
2.3 IPC基礎概念介紹
主要介紹 Serializable 、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成對象的序列化過程,我們通過Intent和Binder傳輸數據時就需要Parcelabel和Serializable。還有的時候我們需要對象持久化到存儲設備上或者通過網絡傳輸到其他客戶端,也需要Serializable完成對象持久化。
2.3.1 Serializable接口
Serializable 是Java提供的一個序列化接口( 空接口) ,為對象提供標準的序列化和反序列化操作。只需要一個類去實現 Serializable 接口并聲明一個 serialVersionUID 即可實現序列化。
private static final long serialVersionUID = 8711368828010083044L
serialVersionUID也可以不聲明。如果不手動指定 serialVersionUID 的值,反序列化時如果當前類有所改變( 比如增刪了某些成員變量) ,那么系統就會重新計算當前類的hash值并更新 serialVersionUID 。這個時候當前類的 serialVersionUID 就和序列化數據中的serialVersionUID 不一致,導致反序列化失敗,程序就出現crash。
靜態成員變量屬于類不屬于對象,不參與序列化過程,其次 transient 關鍵字標記的成員變量也不參與序列化過程。
通過重寫writeObject和readObject方法可以改變系統默認的序列化過程。
2.3.2 Parcelable接口
Parcel內部包裝了可序列化的數據,可以在Binder中自由傳輸。序列化過程中需要實現的功能有序列化、反序列化和內容描述。
序列化功能由 writeToParcel 方法完成,最終是通過 Parcel 的一系列writer方法來完成。
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(code);
out.writeString(name);
}
反序列化功能由 CREATOR 來完成,其內部表明了如何創建序列化對象和數組,通過 Parcel 的一系列read方法來完成。
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
protected Book(Parcel in) {
code = in.readInt();
name = in.readString();
}
在Book(Parcel in)方法中,如果有一個成員變量是另一個可序列化對象,在反序列化過程中需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤。
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
內容描述功能由 describeContents 方法完成,幾乎所有情況下都應該返回0,僅當當前對象中存在文件描述符時返回1。
public int describeContents() {
return 0;
}
Serializable 是Java的序列化接口,使用簡單但開銷大,序列化和反序列化過程需要大量I/O操作。而 Parcelable 是Android中的序列化方式,適合在Android平臺使用,效率高但是使用麻煩。 Parcelable 主要在內存序列化上,Parcelable 也可以將對象序列化到存儲設備中或者將對象序列化后通過網絡傳輸,但是稍顯復雜,推薦使用 Serializable 。
2.3.3 Binder
Binder是Android中的一個類,實現了 IBinder 接口。從IPC角度說,Binder是Andoird的一種跨進程通訊方式,Binder還可以理解為一種虛擬物理設備,它的設備驅動是/dev/binder。從Android Framework角度來說,Binder是 ServiceManager 連接各種Manager( ActivityManager· 、 WindowManager )和相應 ManagerService 的橋梁。從Android應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService時,服務端返回一個包含服務端業務調用的Binder對象,通過這個Binder對象,客戶端就可以獲取服務器端提供的服務或者數據( 包括普通服務和基于AIDL的服務)。

Binder通信采用C/S架構,從組件視角來說,包含Client、Server、ServiceManager以及binder驅動,其中ServiceManager用于管理系統中的各種服務。
圖中的Client,Server,Service Manager之間交互都是虛線表示,是由于它們彼此之間不是直接交互的,而是都通過與Binder驅動進行交互的,從而實現IPC通信方式。其中Binder驅動位于內核空間,Client,Server,Service Manager位于用戶空間。Binder驅動和Service Manager可以看做是Android平臺的基礎架構,而Client和Server是Android的應用層,開發人員只需自定義實現client、Server端,借助Android的基本平臺架構便可以直接進行IPC通信。
http://gityuan.com/2015/10/31/binder-prepare/
Android中Binder主要用于 Service ,包括AIDL和Messenger。普通Service的Binder不涉及進程間通信,Messenger的底層其實是AIDL,所以下面通過AIDL分析Binder的工作機制。
由系統根據AIDL文件自動生成.java文件
- Book.java
表示圖書信息的實體類,實現了Parcelable接口。 - Book.aidl
Book類在AIDL中的聲明。 - IBookManager.aidl
定義的管理Book實體的一個接口,包含 getBookList 和 addBook 兩個方法。盡管Book類和IBookManager位于相同的包中,但是在IBookManager仍然要導入Book類。 - IBookManager.java
系統為IBookManager.aidl生產的Binder類,在 gen 目錄下。
IBookManager繼承了 IInterface 接口,所有在Binder中傳輸的接口都需要繼IInterface接口。結構如下:- 聲明了 getBookList 和 addBook 方法,還聲明了兩個整型id分別標識這兩個方法,用于標識在 transact 過程中客戶端請求的到底是哪個方法。
- 聲明了一個內部類 Stub ,這個 Stub 就是一個Binder類,當客戶端和服務端位于同一進程時,方法調用不會走跨進程的 transact 。當二者位于不同進程時,方法調用需要走 transact 過程,這個邏輯有 Stub 的內部代理類 Proxy 來完成。
- 這個接口的核心實現就是它的內部類 Stub 和 Stub 的內部代理類 Proxy 。
Stub和Proxy類的內部方法和定義
- DESCRIPTOR
Binder的唯一標識,一般用Binder的類名表示。 - asInterface(android.os.IBinder obj)
將服務端的Binder對象轉換為客戶端所需的AIDL接口類型的對象,如果C/S位于同一進
程,此方法返回就是服務端的Stub對象本身,否則返回的就是系統封裝后的Stub.proxy對
象。 - asBinder
返回當前Binder對象。 - onTransact
這個方法運行在服務端的Binder線程池中,由客戶端發起跨進程請求時,遠程請求會通過
系統底層封裝后交由此方法來處理。該方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)- 服務端通過code確定客戶端請求的目標方法是什么,
- 接著從data取出目標方法所需的參數,然后執行目標方法。
- 執行完畢后向reply寫入返回值( 如果有返回值) 。
- 如果這個方法返回值為false,那么服務端的請求會失敗,利用這個特性我們可以來做權限驗證。
- Proxy#getBookList 和Proxy#addBook
這兩個方法運行在客戶端,內部實現過程如下:- 首先創建該方法所需要的輸入型對象Parcel對象_data,輸出型Parcel對象_reply和返回值對象List。
- 然后把該方法的參數信息寫入_data( 如果有參數)
- 接著調用transact方法發起RPC( 遠程過程調用) ,同時當前線程掛起
- 然后服務端的onTransact方法會被調用知道RPC過程返回后,當前線程繼續執行,并從_reply中取出RPC過程的返回結果,最后返回_reply中的數據。
AIDL文件不是必須的,之所以提供AIDL文件,是為了方便系統為我們生成IBookManager.java,但我們完全可以自己寫一個。
linkToDeath和unlinkToDeath
如果服務端進程異常終止,我們到服務端的Binder連接斷裂。但是,如果我們不知道Binder連接已經斷裂,那么客戶端功能會受影響。通過linkTODeath我們可以給Binder設置一個死亡代理,當Binder死亡時,我們就會收到通知。
- 聲明一個 DeathRecipient 對象。 DeathRecipient 是一個接口,只有一個方法 binderDied ,當Binder死亡的時候,系統就會回調 binderDied 方法,然后我們就可以重新綁定遠程服務。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
// TODO:這里重新綁定遠程Service
}
} - 在客戶端綁定遠程服務成功后,給binder設置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0); - 另外,可以通過Binder的 isBinderAlive 判斷Binder是否死亡。
2.4 Android中的IPC方式
主要有以下方式:
- Intent中附加extras
- 共享文件
- Binder
- ContentProvider
- Socket
2.4.1 使用Bundle
四大組件中的三大組件( Activity、Service、Receiver) 都支持在Intent中傳遞 Bundle 數據。
Bundle實現了Parcelable接口,因此可以方便的在不同進程間傳輸。當我們在一個進程中啟動了另一個進程的Activity、Service、Receiver,可以再Bundle中附加我們需要傳輸給遠程進程的消息并通過Intent發送出去。被傳輸的數據必須能夠被序列化。
2.4.2 使用文件共享
我們可以序列化一個對象到文件系統中的同時從另一個進程中恢復這個對象。
- 通過 ObjectOutputStream / ObjectInputStream 序列化一個對象到文件中,或者在另一個進程從文件中反序列這個對象。注意:反序列化得到的對象只是內容上和序列化之前的對象一樣,本質是兩個對象。
- 文件并發讀寫會導致讀出的對象可能不是最新的,并發寫的話那就更嚴重了 。所以文件共享方式適合對數據同步要求不高的進程之間進行通信,并且要妥善處理并發讀寫問題。
- SharedPreferences 底層實現采用XML文件來存儲鍵值對。系統對它的讀/寫有一定的緩存策略,即在內存中會有一份 SharedPreferences 文件的緩存,因此在多進程模式下,系統對它的讀/寫變得不可靠,面對高并發讀/寫時 SharedPreferences 有很大幾率丟失數據,因此不建議在IPC中使用 SharedPreferences 。
2.4.3 使用Messenger
Messenger可以在不同進程間傳遞Message對象。是一種輕量級的IPC方案,底層實現是AIDL。它對AIDL進行了封裝,使得我們可以更簡便的進行IPC。
具體使用時,分為服務端和客戶端:
- 服務端:創建一個Service來處理客戶端請求,同時創建一個Handler并通過它來創建一個
Messenger,然后再Service的onBind中返回Messenger對象底層的Binder即可。
private final Messenger mMessenger = new Messenger (new xxxHandler()); - 客戶端:綁定服務端的Sevice,利用服務端返回的IBinder對象來創建一個Messenger,通過這個Messenger就可以向服務端發送消息了,消息類型是 Message 。如果需要服務端響應,則需要創建一個Handler并通過它來創建一個Messenger( 和服務端一樣) ,并通過 Message 的 replyTo 參數傳遞給服務端。服務端通過Message的 replyTo 參數就可以回應客戶端了。
總而言之,就是客戶端和服務端 拿到對方的Messenger來發送 Message 。只不過客戶端通過bindService 而服務端通過 message.replyTo 來獲得對方的Messenger。
Messenger中有一個 Hanlder 以串行的方式處理隊列中的消息。不存在并發執行,因此我們不用考慮線程同步的問題。
2.4.4 使用AIDL
如果有大量的并發請求,使用Messenger就不太適合,同時如果需要跨進程調用服務端的方法,Messenger就無法做到了。這時我們可以使用AIDL。
流程如下:
- 服務端需要創建Service來監聽客戶端請求,然后創建一個AIDL文件,將暴露給客戶端的接口在AIDL文件中聲明,最后在Service中實現這個AIDL接口即可。
- 客戶端首先綁定服務端的Service,綁定成功后,將服務端返回的Binder對象轉成AIDL接口所屬的類型,接著就可以調用AIDL中的方法了。
AIDL支持的數據類型:
- 基本數據類型、String、CharSequence
- List:只支持ArrayList,里面的每個元素必須被AIDL支持
- Map:只支持HashMap,里面的每個元素必須被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
自定義的Parcelable對象和AIDL對象,不管它們與當前的AIDL文件是否位于同一個包,都必須顯式import進來。
如果AIDL文件中使用了自定義的Parcelable對象,就必須新建一個和它同名的AIDL文件,并在其中聲明它為Parcelable類型。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的參數除了基本類型以外都必須表明方向in/out。AIDL接口文件中只支持方法,不支持聲明靜態常量。建議把所有和AIDL相關的類和文件放在同一個包中,方便管理。
void addBook(in Book book);
AIDL方法是在服務端的Binder線程池中執行的,因此當多個客戶端同時連接時,管理數據的集合直接采用 CopyOnWriteArrayList 來進行自動線程同步。類似的還有 ConcurrentHashMap 。
因為客戶端的listener和服務端的listener不是同一個對象,所以 RecmoteCallbackList 是系統專門提供用于刪除跨進程listener的接口,支持管理任意的AIDL接口,因為所有AIDL接口都繼承自 IInterface 接口。
public class RemoteCallbackList<E extends IInterface>
它內部通過一個Map接口來保存所有的AIDL回調,這個Map的key是 IBinder 類型,value是 Callback 類型。當客戶端解除注冊時,遍歷服務端所有listener,找到和客戶端listener具有相同Binder對象的服務端listenr并把它刪掉。
==客戶端RPC的時候線程會被掛起,由于被調用的方法運行在服務端的Binder線程池中,可能很耗時,不能在主線程中去調用服務端的方法。==
權限驗證
默認情況下,我們的遠程服務任何人都可以連接,我們必須加入權限驗證功能,權限驗證失敗則無法調用服務中的方法。通常有兩種驗證方法:
- 在onBind中驗證,驗證不通過返回null
驗證方式比如permission驗證,在AndroidManifest聲明:
<permission
android:name="com.rgy.chapter_2.permisson.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
Android自定義權限和使用權限
public IBinder onBind(Intent intent){
int check = checkCallingOrSelefPermission("com.ryq.chapter_2.permission.ACCESS_BOOK_SERVICE");
if(check == PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
這種方法也適用于Messager。 - 在onTransact中驗證,驗證不通過返回false
可以permission驗證,還可以采用Uid和Pid驗證。
2.4.5 使用ContentProvider
==ContentProvider是四大組件之一,天生就是用來進程間通信。和Messenger一樣,其底層實現是用Binder。==
系統預置了許多ContentProvider,比如通訊錄、日程表等。要RPC訪問這些信息,只需要通過ContentResolver的query、update、insert和delete方法即可。
創建自定義的ContentProvider,只需繼承ContentProvider類并實現 onCreate 、 query 、 update 、 insert 、 getType 六個抽象方法即可。getType用來返回一個Uri請求所對應的MIME類型,剩下四個方法對應于CRUD操作。這六個方法都運行在ContentProvider進程中,除了 onCreate 由系統回調并運行在主線程里,其他五個方法都由外界調用并運行在Binder線程池中。
ContentProvider是通過Uri來區分外界要訪問的數據集合,例如外界訪問ContentProvider中的表,我們需要為它們定義單獨的Uri和Uri_Code。根據Uri_Code,我們就知道要訪問哪個表了。
==query、update、insert、delete四大方法存在多線程并發訪問,因此方法內部要做好線程同步。==若采用SQLite并且只有一個SQLiteDatabase,SQLiteDatabase內部已經做了同步處理。若是多個SQLiteDatabase或是采用List作為底層數據集,就必須做線程同步。
2.4.6 使用Socket
Socket也稱為“套接字”,分為流式套接字和用戶數據報套接字兩種,分別對應于TCP和UDP協議。Socket可以實現計算機網絡中的兩個進程間的通信,當然也可以在本地實現進程間的通信。我們以一個跨進程的聊天程序來演示。
在遠程Service建立一個TCP服務,然后在主界面中連接TCP服務。服務端Service監聽本地端口,客戶端連接指定的端口,建立連接成功后,拿到 Socket 對象就可以向服務端發送消息或者接受服務端發送的消息。
本例的客戶端和服務端源代碼
除了采用TCP套接字,也可以用UDP套接字。實際上socket不僅能實現進程間的通信,還可以實現設備間的通信(只要設備之間的IP地址互相可見)。
2.5 Binder連接池
前面提到AIDL的流程是:首先創建一個service和AIDL接口,接著創建一個類繼承自AIDL接口中的Stub類并實現Stub中的抽象方法,客戶端在Service的onBind方法中拿到這個類的對象,然后綁定這個service,建立連接后就可以通過這個Stub對象進行RPC。
那么如果項目龐大,有多個業務模塊都需要使用AIDL進行IPC,隨著AIDL數量的增加,我們不能無限制地增加Service,我們需要把所有AIDL放在同一個Service中去管理。
- 服務端只有一個Service,把所有AIDL放在一個Service中,不同業務模塊之間不能有耦合
- 服務端提供一個 queryBinder 接口,這個接口能夠根據業務模塊的特征來返回響應的Binder對象給客戶端
- 不同的業務模塊拿到所需的Binder對象就可以進行RPC了
2.6 選用合適的IPC方式

3 View的事件體系
本章介紹View的事件分發和滑動沖突問題的解決方案。
3.1 view的基礎知識
View的位置參數、MotionEvent和TouchSlop對象、VelocityTracker、GestureDetector和Scroller對象。
3.1.1什么是view
View是Android中所有控件的基類,View的本身可以是單個空間,也可以是多個控件組成的一組控件,即ViewGroup,ViewGroup繼承自View,其內部可以有子View,這樣就形成了View樹的結構。
3.1.2 View的位置參數
View的位置主要由它的四個頂點來決定,即它的四個屬性:top、left、right、bottom,分別表示View左上角的坐標點( top,left) 以及右下角的坐標點( right,bottom) 。

同時,我們可以得到View的大小:
width = right - left
height = bottom - top
而這四個參數可以由以下方式獲取:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
Android3.0后,View增加了x、y、translationX和translationY這幾個參數。其中x和y是View左上角的坐標,而translationX和translationY是View左上角相對于容器的偏移量。他們之間的換算關系如下:
x = left + translationX;
y = top + translationY;
top,left表示原始左上角坐標,而x,y表示變化后的左上角坐標。在View沒有平移時,x=left,y=top。==View平移的過程中,top和left不會改變,改變的是x、y、translationX和translationY。==
3.1.3 MotionEvent和TouchSlop
MotionEvent
事件類型
- ACTION_DOWN 手指剛接觸屏幕
- ACTION_MOVE 手指在屏幕上移動
- ACTION_UP 手指從屏幕上松開
點擊事件類型
- 點擊屏幕后離開松開,事件序列為DOWN->UP
- 點擊屏幕滑動一會再松開,事件序列為DOWN->MOVE->...->MOVE->UP
通過MotionEven對象我們可以得到事件發生的x和y坐標,我們可以通過getX/getY和getRawX/getRawY得到。它們的區別是:getX/getY返回的是相對于當前View左上角的x和y坐標,getRawX/getRawY返回的是相對于手機屏幕左上角的x和y坐標。
TouchSloup
TouchSloup是系統所能識別出的被認為是滑動的最小距離,這是一個常量,與設備有關,可通過以下方法獲得:
ViewConfiguration.get(getContext()).getScaledTouchSloup().
當我們處理滑動時,比如滑動距離小于這個值,我們就可以過濾這個事件(系統會默認過濾),從而有更好的用戶體驗。
3.1.4 VelocityTracker、GestureDetector和Scroller
VelocityTracker
速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平放向速度和豎直方向速度。使用方法:
- 在View的onTouchEvent方法中追蹤當前單擊事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event); - 計算速度,獲得水平速度和豎直速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
注意,獲取速度之前必須先計算速度,即調用computeCurrentVelocity方法,這里指的速度是指一段時間內手指滑過的像素數,1000指的是1000毫秒,得到的是1000毫秒內滑過的像素數。速度可正可負:速度 = ( 終點位置 - 起點位置) / 時間段 - 最后,當不需要使用的時候,需要調用clear()方法重置并回收內存:
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。使用方法:
- 創建一個GestureDetector對象并實現OnGestureListener接口,根據需要,也可實現OnDoubleTapListener接口從而監聽雙擊行為:
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕后無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false); - 在目標View的OnTouchEvent方法中添加以下實現:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume; -
實現OnGestureListener和OnDoubleTapListener接口中的方法
其中常用的方法有:onSingleTapUp(單擊)、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)和onDoubleTap( 雙擊)。建議:如果只是監聽滑動相關的,可以自己在onTouchEvent中實現,如果要監聽雙擊這種行為,那么就使用GestureDetector。
Scroller
彈性滑動對象,用于實現View的彈性滑動。其本身無法讓View彈性滑動,需要和View的computeScroll方法配合使用才能完成這個功能。使用方法:
Scroller scroller = new Scroller(mContext);
//緩慢移動到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms內滑向destX,效果就是慢慢滑動
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX,mScroller.getCurrY());
postInvalidate();
}
}
3.2 View的滑動
三種方式實現View滑動
3.2.1 使用scrollTo/scrollBy
scrollBy實際調用了scrollTo,它實現了基于當前位置的相對滑動,而scrollTo則實現了絕對滑動。
==scrollTo和scrollBy只能改變View的內容位置而不能改變View在布局中的位置?;瑒悠屏縨ScrollX和mScrollY的正負與實際滑動方向相反,即從左向右滑動,mScrollX為負值,從上往下滑動mScrollY為負值。==
3.2.2 使用動畫
使用動畫移動View,主要是操作View的translationX和translationY屬性,既可以采用傳統的View動畫,也可以采用屬性動畫,如果使用屬性動畫,為了能夠兼容3.0以下的版本,需要采用開源動畫庫nineolddandroids。 如使用屬性動畫:(View在100ms內向右移動100像素)
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
3.2.3 改變布局屬性
通過改變布局屬性來移動View,即改變LayoutParams。
3.2.4 各種滑動方式的對比
- scrollTo/scrollBy:操作簡單,適合對View內容的滑動;
- 動畫:操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果;
- 改變布局參數:操作稍微復雜,適用于有交互的View。
3.3 彈性滑動
3.3.1 使用Scroller
使用Scroller實現彈性滑動的典型使用方法如下:
Scroller scroller = new Scroller(mContext);
//緩慢移動到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms內滑向destX,效果就是緩慢滑動
mScroller.startSscroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
從上面代碼可以知道,我們首先會構造一個Scroller對象,并調用他的startScroll方法,該方法并沒有讓view實現滑動,只是把參數保存下來,我們來看看startScroll方法的實現就知道了:
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAminationTimeMills();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float)mDuration;
}
可以知道,startScroll方法的幾個參數的含義,startX和startY表示滑動的起點,dx和dy表示的是滑動的距離,而duration表示的是滑動時間,注意,這里的滑動指的是View內容的滑動,在startScroll方法被調用后,馬上調用invalidate方法,這是滑動的開始,invalidate方法會導致View的重繪,在View的draw方法中調用computeScroll方法,computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然后通過scrollTo方法實現滑動,接著又調用postInvalidate方法進行第二次重繪,一直循環,直到computeScrollOffset()方法返回值為false才結束整個滑動過程。 我們可以看看computeScrollOffset方法是如何獲得當前的scrollX和scrollY的:
public boolean computeScrollOffset(){
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime);
if(timePassed < mDuration){
switch(mMode){
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDuratio
nReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(y * mDeltaY);
break;
...
}
}
return true;
}
到這里我們就基本明白了,computeScroll向Scroller獲取當前的scrollX和scrollY其實是通過計算時間流逝的百分比來獲得的,每一次重繪距滑動起始時間會有一個時間間距,通過這個時間間距Scroller就可以得到View當前的滑動位置,然后就可以通過scrollTo方法來完成View的滑動了。
3.3.2 通過動畫
動畫本身就是一種漸近的過程,因此通過動畫來實現的滑動本身就具有彈性。實現也很簡單:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
;
//當然,我們也可以利用動畫來模仿Scroller實現View彈性滑動的過程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();
上面的動畫本質上是沒有作用于任何對象上的,他只是在1000ms內完成了整個動畫過程,利用這個特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,根據比例計算出View所滑動的距離。采用這種方法也可以實現其他動畫效果,我們可以在onAnimationUpdate方法中加入自定義操作。
3.3.3 使用延時策略
延時策略的核心思想是通過發送一系列延時信息從而達到一種漸近式的效果,具體可以通過Hander和View的postDelayed方法,也可以使用線程的sleep方法。 下面以Handler為例:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default : break;
}
}
}
3.4 View的事件分發機制
3.4.1 點擊事件的傳遞規則
點擊事件是MotionEvent。首先我們先看看下面一段偽代碼,通過它我們可以理解到點擊事件的傳遞規則:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
上面代碼主要涉及到以下三個方法:
- public boolean dispatchTouchEvent(MotionEvent ev);
這個方法用來進行事件的分發。如果事件傳遞給當前view,則調用此方法。返回結果表示是否消耗此事件,受onTouchEvent和下級View的dispatchTouchEvent方法影響。 - public boolean onInterceptTouchEvent(MotionEvent ev);
這個方法用來判斷是否攔截事件。在dispatchTouchEvent方法中調用。返回結果表示是否攔截。 - public boolean onTouchEvent(MotionEvent ev);
這個方法用來處理點擊事件。在dispatchTouchEvent方法中調用,返回結果表示是否消耗事件。如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

點擊事件的傳遞規則:對于一個根ViewGroup,點擊事件產生后,首先會傳遞給他,這時候就會調用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要攔截事件,接下來事件就會交給ViewGroup處理,調用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值為false,表示ViewGroup不攔截該事件,這時事件就傳遞給他的子View,接下來子View的dispatchTouchEvent方法,如此反復直到事件被最終處理。
當一個View需要處理事件時,如果它設置了OnTouchListener,那么onTouch方法會被調用,如果onTouch返回false,則當前View的onTouchEvent方法會被調用,返回true則不會被調用,同時,在onTouchEvent方法中如果設置了OnClickListener,那么他的onClick方法會被調用。==由此可見處理事件時的優先級關系: onTouchListener > onTouchEvent >onClickListener==
關于事件傳遞的機制,這里給出一些結論:
- 一個事件系列以down事件開始,中間包含數量不定的move事件,最終以up事件結束。
- 正常情況下,一個事件序列只能由一個View攔截并消耗。
- 某個View攔截了事件后,該事件序列只能由它去處理,并且它的onInterceptTouchEvent
不會再被調用。 - 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不會交給他處理,并且事件將重新交由他的父元素去處理,即父元素的onTouchEvent被調用。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么這個事件將會消失,此時父元素的onTouchEvent并不會被調用,并且當前View可以持續收到后續的事件,最終消失的點擊事件會傳遞給Activity去處理。
- ViewGroup默認不攔截任何事件。
- View沒有onInterceptTouchEvent方法,一旦事件傳遞給它,它的onTouchEvent方法會被調用。
- View的onTouchEvent默認消耗事件,除非他是不可點擊的( clickable和longClickable同時為false) 。View的longClickable屬性默認false,clickable默認屬性分情況(如TextView為false,button為true)。
- View的enable屬性不影響onTouchEvent的默認返回值。
- onClick會發生的前提是當前View是可點擊的,并且收到了down和up事件。
- 事件傳遞過程總是由外向內的,即事件總是先傳遞給父元素,然后由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發過程,但是ACTION_DOWN事件除外。
3.4.2 事件分發的源碼解析
略
3.5 滑動沖突
在界面中,只要內外兩層同時可以滑動,這個時候就會產生滑動沖突。滑動沖突的解決有固定的方法。
3.5.1 常見的滑動沖突場景

- 外部滑動和內部滑動方向不一致;
比如viewpager和listview嵌套,但這種情況下viewpager自身已經對滑動沖突進行了處理。 - 外部滑動方向和內部滑動方向一致;
- 上面兩種情況的嵌套。
只要解決1和2即可。
3.5.2 滑動沖突的處理規則
對于場景一,處理的規則是:當用戶左右( 上下) 滑動時,需要讓外部的View攔截點擊事件,當用戶上下( 左右) 滑動的時候,需要讓內部的View攔截點擊事件。根據滑動的方向判斷誰來攔截事件。
對于場景二,由于滑動方向一致,這時候只能在業務上找到突破點,根據業務需求,規定什么時候讓外部View攔截事件,什么時候由內部View攔截事件。
場景三的情況相對比較復雜,同樣根據需求在業務上找到突破點。
3.5.3 滑動沖突的解決方式
外部攔截法
所謂外部攔截法是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。下面是偽代碼:
public boolean onInterceptTouchEvent (MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
針對不同沖突,只需修改父容器需要當前事件的條件即可。其他不需修改也不能修改。
- ACTION_DOWN:必須返回false。因為如果返回true,后續事件都會被攔截,無法傳遞給子View。
- ACTION_MOVE:根據需要決定是否攔截
- ACTION_UP:必須返回false。如果攔截,那么子View無法接受up事件,無法完成click操作。而如果是父容器需要該事件,那么在ACTION_MOVE時已經進行了攔截,根據上一節的結論3,ACTION_UP不會經過onInterceptTouchEvent方法,直接交給父容器處理。
內部攔截法
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗,否則就交由父容器進行處理。這種方法與Android事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是偽代碼:
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
==除了子元素需要做處理外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。==因此,父元素要做以下修改:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
優化滑動體驗:
mScroller.abortAnimation();
外部攔截法實例:HorizontalScrollViewEx
4 View的工作原理
主要內容
- View的工作原理
- 自定義View的實現方式
- 自定義View的底層工作原理,比如View的測量流程、布局流程、繪制流程
- View常見的回調方法,比如構造方法、onAttach.onVisibilityChanged/onDetach等
4.1 初識ViewRoot和DecorView
ViewRoot的實現是 ViewRootImpl 類,是連接WindowManager和DecorView的紐帶,View的三大流程( mearsure、layout、draw) 均是通過ViewRoot來完成。當Activity對象被創建完畢后,會將DecorView添加到Window中,同時創建 ViewRootImpl 對象,并將ViewRootImpl 對象和DecorView建立連接,源碼如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
View的繪制流程是從ViewRoot的performTraversals開始的

- measure用來測量View的寬高
- layout來確定View在父容器中的位置
- draw負責將View繪制在屏幕上
performTraversals會依次調用 performMeasure 、 performLayout 和performDraw 三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重復父容器的measure過程,如此反復完成了整個View數的遍歷。另外兩個過程同理。
- Measure完成后, 可以通過getMeasuredWidth 、getMeasureHeight 方法來獲取View測量后的寬/高。特殊情況下,測量的寬高不等于最終的寬高,詳見后面。
- Layout過程決定了View的四個頂點的坐標和實際View的寬高,完成后可通過 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四個定點坐標。
DecorView作為頂級View,其實是一個 FrameLayout ,它包含一個豎直方向的 LinearLayout ,這個 LinearLayout 分為標題欄和內容欄兩個部分。

在Activity通過setContextView所設置的布局文件其實就是被加載到內容欄之中的。這個內容欄的id是 R.android.id.content ,通過
ViewGroup content = findViewById(R.android.id.content);
可以得到這個contentView。View層的事件都是先經過DecorView,然后才傳遞到子View。
4.2 理解MeasureSpec
MeasureSpec決定了一個View的尺寸規格。但是父容器會影響View的MeasureSpec的創建過程。系統將View的 LayoutParams 根據父容器所施加的規則轉換成對應的MeasureSpec,然后根據這個MeasureSpec來測量出View的寬高。
4.2.1 MeasureSpec
MeasureSpec代表一個32位int值,高2位代表SpecMode( 測量模式) ,低30位代表SpecSize( 在某個測量模式下的規格大小) 。
SpecMode有三種:
- UNSPECIFIED :父容器不對View進行任何限制,要多大給多大,一般用于系統內部
- EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的 match_parent 和具體數值這兩種模式
- AT_MOST :對應View的默認大小,不同View實現不同,View的大小不能大于父容器的SpecSize,對應 LayoutParams 中的 wrap_content
4.2.2 MeasureSpec和LayoutParams的對應關系
對于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。
View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方法,通過調用子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec,再調用子元素的 measure 方法。

parentSize是指父容器中目前可使用的大小。
- 當View采用固定寬/高時( 即設置固定的dp/px) ,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我們設置的值。
- 當View的寬/高是 match_parent 時,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空間。
- 當View的寬/高是 wrap_content 時,View的MeasureSpec都是AT_MOST模式并且其大小不能超過父容器的剩余空間。
- 父容器的UNSPECIFIED模式,一般用于系統內部多次Measure時,表示一種測量的狀態,一般來說我們不需要關注此模式。
4.3 View的工作流程
4.3.1 measure過程
View的measure過程
直接繼承View的自定義控件需要重寫 onMeasure 方法并設置 wrap_content ( 即specMode是 AT_MOST 模式) 時的自身大小,否則在布局中使用 wrap_content 相當于使用 match_parent 。對于非 wrap_content 的情形,我們沿用系統的測量值即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,給定一個默認值mWidth,mHeight。默認寬高靈活指定
//參考TextView、ImageView的處理方式
//其他情況下沿用系統測量規則即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure過程
ViewGroup是一個抽象類,沒有重寫View的 onMeasure 方法,但是它提供了一個 measureChildren 方法。這是因為不同的ViewGroup子類有不同的布局特性,導致他們的測量細節各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure方法。
measureChildren方法的流程:
- 取出子View的 LayoutParams
- 通過 getChildMeasureSpec 方法來創建子元素的 MeasureSpec
- 將 MeasureSpec 直接傳遞給View的measure方法來進行測量
通過LinearLayout的onMeasure方法里來分析ViewGroup的measure過程:
- LinearLayout在布局中如果使用match_parent或者具體數值,測量過程就和View一致,即高度為specSize
- LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度總和,但不超過它的父容器的剩余空間
- LinearLayout的的最終高度同時也把豎直方向的padding考慮在內
View的measure過程是三大流程中最復雜的一個,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的。
==如果我們想要在Activity啟動的時候就獲取一個View的寬高,怎么操作呢?==因為View的measure過程和Activity的生命周期并不是同步執行,無法保證在Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來獲取View的寬高:
- Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經準備好了,需要注意:它會被調用多次,當Activity的窗口得到焦點和失去焦點均會被調用。 - view.post(runnable)
通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此runnable的時候,View也初始化好了。 - ViewTreeObserver
使用 ViewTreeObserver 的眾多回調可以完成這個功能,比如OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的可見性發生改變時,onGlobalLayout 方法會被回調,這是獲取View寬高的好時機。需要注意的是,伴隨著View樹狀態的改變, onGlobalLayout 會被回調多次。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)
手動對view進行measure。需要根據View的layoutParams分情況處理:- match_parent:
無法measure出具體的寬高,因為不知道父容器的剩余空間,無法測量出View的大小 - 具體的數值( dp/px):
- match_parent:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二進制表示,最大值30個1,在AT_MOST模式下,
// 我們用View理論上能支持的最大值去構造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
4.3.2 layout過程
layout的作用是ViewGroup用來確定子View的位置,當ViewGroup的位置被確定后,它會在onLayout中遍歷所有的子View并調用其layout方法,在 layout 方法中, onLayout 方法又會被調用。
View的 layout 方法確定本身的位置,源碼流程如下:
- setFrame 確定View的四個頂點位置,即確定了View在父容器中的位置
- 調用 onLayout 方法,確定所有子View的位置,和onMeasure一樣,onLayout的具體實現和布局有關,因此View和ViewGroup均沒有真正實現 onLayout 方法。
以LinearLayout的 onLayout 方法為例:
- 遍歷所有子View并調用 setChildFrame 方法來為子元素指定對應的位置
- setChildFrame 方法實際上調用了子View的 layout 方法,形成了遞歸
==View的測量寬高和最終寬高的區別:==
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成于measure過程,最終寬高形成于layout過程。但重寫view的layout方法可以使他們不相等。
4.3.3 draw過程
View的繪制過程遵循如下幾步:
- 繪制背景 drawBackground(canvas)
- 繪制自己 onDraw
- 繪制children dispatchDraw 遍歷所有子View的 draw 方法
- 繪制裝飾 onDrawScrollBars
ViewGroup會默認啟用 setWillNotDraw 為ture,導致系統不會去執行 onDraw ,所以自定義ViewGroup需要通過onDraw來繪制內容時,必須顯式的關閉 WILL_NOT_DRAW 這個優化標記位,即調用 setWillNotDraw(false);
4.4 自定義View
4.4.1 自定義View的分類
繼承View 重寫onDraw方法
通過 onDraw 方法來實現一些不規則的效果,這種效果不方便通過布局的組合方式來達到。這種方式需要自己支持 wrap_content ,并且padding也要去進行處理。
繼承ViewGroup派生特殊的layout
實現自定義的布局方式,需要合適地處理ViewGroup的測量、布局這兩個過程,并同時處理子View的測量和布局過程。
繼承特定的View子類( 如TextView、Button)
擴展某種已有的控件的功能,比較簡單,不需要自己去管理 wrap_content 和padding。
** 繼承特定的ViewGroup子類( 如LinearLayout)**
比較常見,實現幾種view組合一起的效果。與方法二的差別是方法二更接近底層實現。
4.4.2 自定義View須知
- 直接繼承View或ViewGroup的控件, 需要在onmeasure中對wrap_content做特殊處理。指定wrap_content模式下的默認寬/高。
- 直接繼承View的控件,如果不在draw方法中處理padding,那么padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
- 盡量不要用在View中使用Handler,因為沒必要。View內部提供了post系列的方法,完全可以替代Handler的作用。
- View中有線程和動畫,需要在View的onDetachedFromWindow中停止。當View不可見時,也需要停止線程和動畫,否則可能造成內存泄漏。
- View帶有滑動嵌套情形時,需要處理好滑動沖突
4.4.3 自定義View實例
- 繼承View重寫onDraw方法:CircleView
自定義屬性設置方法:
- 在values目錄下創建自定義屬性的XML,如attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
- 在View的構造方法中解析自定義屬性的值并做相應處理,這里我們解析circle_color。
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
- 在布局文件中使用自定義屬性
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
</LinearLayout>
- 繼承ViewGroup派生特殊的layout:HorizontalScrollViewEx
onMeasure方法中,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高做相應處理。然后判斷寬高是不是wrap_content,如果寬是,那么HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和。如果高是wrap_content,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin。
onLayout方法中,在放置子元素時候也要考慮padding和margin。
4.4.4 自定義View的思想
- 掌握基本功,比如View的彈性滑動、滑動沖突、繪制原理等
- 面對新的自定義View時,對其分類并選擇合適的實現思路。