本筆記整理自: 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
聯(lián)系我:xiadeye@icloud.com
本書是一本Android進(jìn)階類書籍,采用理論玻蝌、源碼和實(shí)踐相結(jié)合的方式來闡述高水準(zhǔn)的Android應(yīng)用開發(fā)要點(diǎn)价脾。本書從三個(gè)方面來組織內(nèi)容颈畸。
- 介紹Android開發(fā)者不容易掌握的一些知識(shí)點(diǎn)
- 結(jié)合Android源代碼和應(yīng)用層開發(fā)過程峻厚,融會(huì)貫通,介紹一些比較深入的知識(shí)點(diǎn)
- 介紹一些核心技術(shù)和Android的性能優(yōu)化思想
目錄
第1章 Activity的生命周期和啟動(dòng)模式
第2章 IPC機(jī)制
第3章 View的事件體系
第4章 View的工作原理
第5章 理解RemoteViews
第6章 Android的Drawable
第7章 Android動(dòng)畫深入分析
第8章 理解Window和WindowManager
第9章 四大組件的工作過程
第10章 Android的消息機(jī)制
第11章 Android的線程和線程池
第12章 Bitmap的加載和Cache
第13章 綜合技術(shù)
第14章 JNI和NDK編程
第15章 Android性能優(yōu)化
[TOC]
1 Activity的生命周期和啟動(dòng)模式
1.1 Activity的生命周期全面分析
用戶正常使用情況下的生命周期 & 由于Activity被系統(tǒng)回收或者設(shè)備配置改變導(dǎo)致Activity被銷毀重建情況下的生命周期焕毫。
1.1.1 典型情況下的生命周期分析
Activity的生命周期和啟動(dòng)模式
- Activity第一次啟動(dòng):onCreate->onStart->onResume。
- Activity切換到后臺(tái)( 用戶打開新的Activity或者切換到桌面) 驶乾,onPause->onStop(如果新Activity采用了透明主題邑飒,則當(dāng)前Activity不會(huì)回調(diào)onstop)。
- Activity從后臺(tái)到前臺(tái)级乐,重新可見疙咸,onRestart->onStart->onResume。
- 用戶退出Activity风科,onPause->onStop->onDestroy撒轮。
- onStart開始到onStop之前,Activity可見贼穆。onResume到onPause之前题山,Activity可以接受用戶交互。
- 在新Activity啟動(dòng)之前扮惦,棧頂?shù)腁ctivity需要先onPause后臀蛛,新Activity才能啟動(dòng)亲桦。所以不能在onPause執(zhí)行耗時(shí)操作崖蜜。
- onstop中也不可以太耗時(shí),資源回收和釋放可以放在onDestroy中客峭。
1.1.2 異常情況下的生命周期分析
1 系統(tǒng)配置變化導(dǎo)致Activity銷毀重建
例如Activity處于豎屏狀態(tài)豫领,如果突然旋轉(zhuǎn)屏幕,由于系統(tǒng)配置發(fā)生了改變舔琅,Activity就會(huì)被銷
毀并重新創(chuàng)建等恐。
在異常情況下系統(tǒng)會(huì)在onStop之前調(diào)用onSaveInstanceState來保存狀態(tài)。Activity重新創(chuàng)建后,會(huì)在onStart之后調(diào)用onRestoreInstanceState來恢復(fù)之前保存的數(shù)據(jù)课蔬。
保存數(shù)據(jù)的流程: Activity被意外終止囱稽,調(diào)用onSaveIntanceState保存數(shù)據(jù)-> Activity委托Window,Window委托它上面的頂級容器一個(gè)ViewGroup( 可能是DecorView) 二跋。然后頂層容器在通知所有子元素來保存數(shù)據(jù)战惊。
這是一種委托思想,Android中類似的還有:View繪制過程扎即、事件分發(fā)等吞获。
系統(tǒng)只在Activity異常終止的時(shí)候才會(huì)調(diào)用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情況不會(huì)觸發(fā)谚鄙。
2 資源內(nèi)存不足導(dǎo)致低優(yōu)先級的Activity被回收
三種Activity優(yōu)先級:前臺(tái)- 可見非前臺(tái) -后臺(tái)各拷,從高到低。
如果一個(gè)進(jìn)程沒有四大組件闷营,那么將很快被系統(tǒng)殺死烤黍。因此,后臺(tái)工作最好放入service中傻盟。
android:configChanges="orientation" 在manifest中指定 configChanges 在系統(tǒng)配置變化后不重新創(chuàng)建Activity蚊荣,也不會(huì)執(zhí)行 onSaveInstanceState 和onRestoreInstanceState 方法,而是調(diào)用 onConfigurationChnaged 方法莫杈。
附:系統(tǒng)配置變化項(xiàng)目
configChanges 一般常用三個(gè)選項(xiàng):
- locale 系統(tǒng)語言變化
- keyborardHidden 鍵盤的可訪問性發(fā)生了變化互例,比如用戶調(diào)出了鍵盤
- orientation 屏幕方向變化
1.2 Activity的啟動(dòng)模式
1.2.1 Activity的LaunchMode
Android使用棧來管理Activity。
- standard
每次啟動(dòng)都會(huì)重新創(chuàng)建一個(gè)實(shí)例筝闹,不管這個(gè)Activity在棧中是否已經(jīng)存在媳叨。誰啟動(dòng)了這個(gè)Activity,那么Activity就運(yùn)行在啟動(dòng)它的那個(gè)Activity所在的棧中关顷。
用Application去啟動(dòng)Activity時(shí)會(huì)報(bào)錯(cuò)糊秆,原因是非Activity的Context沒有任務(wù)棧。解決辦法是為待啟動(dòng)Activity制定FLAG_ACTIVITY_NEW_TASH標(biāo)志位议双,這樣就會(huì)為它創(chuàng)建一個(gè)新的任務(wù)棧痘番。 - singleTop
如果新Activity位于任務(wù)棧的棧頂,那么此Activity不會(huì)被重新創(chuàng)建平痰,同時(shí)回調(diào) onNewIntent 方法汞舱。onCreate和onStart方法不會(huì)被執(zhí)行。 - singleTask
這是一種單實(shí)例模式宗雇。如果不存在activity所需要的任務(wù)棧昂芜,則創(chuàng)建一個(gè)新任務(wù)棧和新Activity實(shí)例;如果存在所需要的任務(wù)棧赔蒲,不存在實(shí)例泌神,則新創(chuàng)建一個(gè)Activity實(shí)例良漱;如果存在所需要的任務(wù)棧和實(shí)例,則不創(chuàng)建欢际,調(diào)用onNewIntent方法母市。同時(shí)使該Activity實(shí)例之上的所有Activity出棧。
參考:taskAffinity標(biāo)識(shí)Activity所需要的任務(wù)棧 - singleIntance
單實(shí)例模式损趋。具有singleTask模式的所有特性窒篱,同時(shí)具有此模式的Activity只能獨(dú)自位于一個(gè)任務(wù)棧中。
假設(shè)兩個(gè)任務(wù)棧舶沿,前臺(tái)任務(wù)棧為12墙杯,后臺(tái)任務(wù)棧為XY。Y的啟動(dòng)模式是singleTask±ǖ矗現(xiàn)在請求Y高镐,整個(gè)后臺(tái)任務(wù)棧會(huì)被切換到前臺(tái)。如圖所示:
設(shè)置啟動(dòng)模式
- manifest中 設(shè)置下的 android:launchMode 屬性畸冲。
- 啟動(dòng)Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 程腹。
- 兩種同時(shí)存在時(shí)变丧,以第二種為準(zhǔn)。第一種方式無法直接為Activity添加FLAG_ACTIVITY_CLEAR_TOP標(biāo)識(shí),第二種方式無法指定singleInstance模式启搂。
- 可以通過命令行 adb shell dumpsys activity 命令查看棧中的Activity信息渠驼。
1.2.2 Activity的Flags
這些FLAG可以設(shè)定啟動(dòng)模式疫鹊、可以影響Activity的運(yùn)行狀態(tài)杉武。
- FLAG_ACTIVITY_NEW_TASK
為Activity指定“singleTask”啟動(dòng)模式。 - FLAG_ACTIVITY_SINGLE_TOP
為Activity指定“singleTop"啟動(dòng)模式褪子。 - FLAG_ACTIVITY_CLEAR_TOP
具有此標(biāo)記位的Activity啟動(dòng)時(shí)量淌,同一個(gè)任務(wù)棧中位于它上面的Activity都要出棧,一般和FLAG_ACTIVITY_NEW_TASK配合使用嫌褪。 - FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
如果設(shè)置呀枢,新的Activity不會(huì)在最近啟動(dòng)的Activity的列表(就是安卓手機(jī)里顯示最近打開的Activity那個(gè)系統(tǒng)級的UI)中保存。等同于在xml中指定android:exludeFromRecents="true"屬性笼痛。
1.3 IntentFilter的匹配規(guī)則
Activity調(diào)用方式
- 顯示調(diào)用 明確指定被啟動(dòng)對象的組件信息裙秋,包括包名和類名
- 隱式調(diào)用 不需要明確指定組件信息,需要Intent能夠匹配目標(biāo)組件中的IntentFilter中所設(shè)置的過濾信息缨伊。
匹配規(guī)則
- IntentFilter中的過濾信息有action摘刑、category、data倘核。
- 只有一個(gè)Intent同時(shí)匹配action類別泣侮、category類別即彪、data類別才能成功啟動(dòng)目標(biāo)Activity紧唱。
- 一個(gè)Activity可以有多個(gè)intent-filter活尊,一個(gè)Intent只要能匹配任何一組intent-filter即可成功啟動(dòng)對應(yīng)的Activity。
** action**
action是一個(gè)字符串,匹配是指與action的字符串完全一樣,區(qū)分大小寫漏益。
一個(gè)intent-filter可以有多個(gè)aciton蛹锰,只要Intent中的action能夠和任何一個(gè)action相同即可成功匹配。
Intent中如果沒有指定action绰疤,那么匹配失敗铜犬。
** category**
category是一個(gè)字符串。
Intent可以沒有category轻庆,但是如果你一旦有category癣猾,不管有幾個(gè),每個(gè)都必須與intent-filter中的其中一個(gè)category相同余爆。
系統(tǒng)在 startActivity 和 startActivityForResult 的時(shí)候纷宇,會(huì)默認(rèn)為Intent加上 android.intent.category.DEFAULT 這個(gè)category,所以為了我們的activity能夠接收隱式調(diào)用蛾方,就必須在intent-filter中加上 android.intent.category.DEFAULT 這個(gè)category像捶。
** data**
data的匹配規(guī)則與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的結(jié)構(gòu):
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
實(shí)際例子
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
scheme:URI的模式醉途,比如http矾瑰、file、content等隘擎,默認(rèn)值是 file 殴穴。
host:URI的主機(jī)名
port:URI的端口號(hào)
path、pathPattern和pathPrefix:這三個(gè)參數(shù)描述路徑信息货葬。
path采幌、pathPattern可以表示完整的路徑信息,其中pathPattern可以包含通配符 * 震桶,表示0個(gè)或者多個(gè)任意字符休傍。
pathPrefix只表示路徑的前綴信息。
過濾規(guī)則的uri為空時(shí)蹲姐,有默認(rèn)值content和file磨取,因此intent設(shè)置uri的scheme部分必須為content或file人柿。
Intent指定data時(shí),必須調(diào)用 setDataAndType 方法忙厌, setData 和 setType 會(huì)清除另一方的值凫岖。
對于service和BroadcastReceiver也是同樣的匹配規(guī)則,不過對于service最好使用顯式調(diào)用逢净。
隱式調(diào)用需注意
當(dāng)通過隱式調(diào)用啟動(dòng)Activity時(shí)哥放,沒找到對應(yīng)的Activity系統(tǒng)就會(huì)拋出 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);以上的第二個(gè)參數(shù)使用 MATCH_DEFAULT_ONLY 甥雕,這個(gè)標(biāo)志位的含義是僅僅匹配那些在
intent-filter中聲明了 android.intent.category.DEFAULT 這個(gè)category的Activity。因?yàn)槿绻巡缓@個(gè)category的Activity匹配出來了胀茵,由于不含DEFAULT這個(gè)category的Activity是無法接受隱式Intent的從而導(dǎo)致startActivity失敗犀农。 下面的action和category用來表明這是一個(gè)入口Activity,并且會(huì)出現(xiàn)在系統(tǒng)的應(yīng)用列表中宰掉,二者缺一不可呵哨。
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
2 IPC機(jī)制
2.1 Android IPC 簡介
- IPC即Inter-Process Communication,含義為進(jìn)程間通信或者跨進(jìn)程通信轨奄,是指兩個(gè)進(jìn)程之間進(jìn)行數(shù)據(jù)交換的過程孟害。
- 線程是CPU調(diào)度的最小單元,是一種有限的系統(tǒng)資源挪拟。進(jìn)程一般指一個(gè)執(zhí)行單元挨务,在PC和移動(dòng)設(shè)備上是指一個(gè)程序或者應(yīng)用。進(jìn)程與線程是包含與被包含的關(guān)系玉组。一個(gè)進(jìn)程可以包含多個(gè)線程谎柄。最簡單的情況下一個(gè)進(jìn)程只有一個(gè)線程,即主線程( 例如Android的UI線程) 惯雳。
- 任何操作系統(tǒng)都需要有相應(yīng)的IPC機(jī)制朝巫。如Windows上的剪貼板、管道和郵槽石景;Linux上命名管道劈猿、共享內(nèi)容、信號(hào)量等潮孽。Android中最有特色的進(jìn)程間通信方式就是binder揪荣,另外還支持socket。contentProvider是Android底層實(shí)現(xiàn)的進(jìn)程間通信往史。
- 在Android中仗颈,IPC的使用場景大概有以下:
- 有些模塊由于特殊原因需要運(yùn)行在單獨(dú)的進(jìn)程中。
- 通過多進(jìn)程來獲取多份內(nèi)存空間椎例。
- 當(dāng)前應(yīng)用需要向其他應(yīng)用獲取數(shù)據(jù)挨决。
2.2 Android中的多進(jìn)程模式
2.2.1 開啟多進(jìn)程模式
在Android中使用多線程只有一種方法:給四大組件在Manifest中指定 android:process 屬性请祖。這個(gè)屬性的值就是進(jìn)程名。這意味著不能在運(yùn)行時(shí)指定一個(gè)線程所在的進(jìn)程凰棉。
tips:使用 adb shell ps 或 adb shell ps|grep 包名 查看當(dāng)前所存在的進(jìn)程信息损拢。
兩種進(jìn)程命名方式的區(qū)別
- “:remote”
“:”的含義是指在當(dāng)前的進(jìn)程名前面附加上當(dāng)前的包名陌粹,完整的進(jìn)程名為“com.example.c2.remote"撒犀。這種進(jìn)程屬于當(dāng)前應(yīng)用的私有進(jìn)程,其他應(yīng)用的組件不可以和它跑在同一個(gè)進(jìn)程中掏秩。 - "com.example.c2.remote"
這是一種完整的命名方式或舞。這種進(jìn)程屬于全局進(jìn)程,其他應(yīng)用可以通過ShareUID方式和它跑在同一個(gè)進(jìn)程中蒙幻。
2.2.2 多線程模式的運(yùn)行機(jī)制
Android為每個(gè)進(jìn)程都分配了一個(gè)獨(dú)立的虛擬機(jī)映凳,不同虛擬機(jī)在內(nèi)存分配上有不同的地址空間,導(dǎo)致不同的虛擬機(jī)訪問同一個(gè)類的對象會(huì)產(chǎn)生多份副本邮破。例如不同進(jìn)程的Activity對靜態(tài)變量的修改诈豌,對其他進(jìn)程不會(huì)造成任何影響。所有運(yùn)行在不同進(jìn)程的四大組件抒和,只要它們之間需要通過內(nèi)存在共享數(shù)據(jù)矫渔,都會(huì)共享失敗。四大組件之間不可能不通過中間層來共享數(shù)據(jù)摧莽。
多進(jìn)程會(huì)帶來以下問題:
- 靜態(tài)成員和單例模式完全失效庙洼。
- 線程同步鎖機(jī)制完全失效。
這兩點(diǎn)都是因?yàn)椴煌M(jìn)程不在同一個(gè)內(nèi)存空間下镊辕,鎖的對象也不是同一個(gè)對象油够。 - SharedPreferences的可靠性下降。
SharedPreferences底層是 通過讀/寫XML文件實(shí)現(xiàn)的征懈,并發(fā)讀/寫會(huì)導(dǎo)致一定幾率的數(shù)據(jù)丟失石咬。 - Application會(huì)多次創(chuàng)建。
由于系統(tǒng)創(chuàng)建新的進(jìn)程的同時(shí)分配獨(dú)立虛擬機(jī)卖哎,其實(shí)這就是啟動(dòng)一個(gè)應(yīng)用的過程碌补。在多進(jìn)程模式中,不同進(jìn)程的組件擁有獨(dú)立的虛擬機(jī)棉饶、Application以及內(nèi)存空間厦章。
多進(jìn)程相當(dāng)于兩個(gè)不同的應(yīng)用采用了SharedUID的模式
實(shí)現(xiàn)跨進(jìn)程的方式有很多:
- Intent傳遞數(shù)據(jù)。
- 共享文件和SharedPreferences照藻。
- 基于Binder的Messenger和AIDL袜啃。
- Socket等
2.3 IPC基礎(chǔ)概念介紹
主要介紹 Serializable 、 Parcelable 幸缕、 Binder 群发。Serializable和Parcelable接口可以完成對象的序列化過程晰韵,我們通過Intent和Binder傳輸數(shù)據(jù)時(shí)就需要Parcelabel和Serializable。還有的時(shí)候我們需要對象持久化到存儲(chǔ)設(shè)備上或者通過網(wǎng)絡(luò)傳輸?shù)狡渌蛻舳耸旒耍残枰猄erializable完成對象持久化雪猪。
2.3.1 Serializable接口
Serializable 是Java提供的一個(gè)序列化接口( 空接口) ,為對象提供標(biāo)準(zhǔn)的序列化和反序列化操作起愈。只需要一個(gè)類去實(shí)現(xiàn) Serializable 接口并聲明一個(gè) serialVersionUID 即可實(shí)現(xiàn)序列化只恨。
private static final long serialVersionUID = 8711368828010083044L
serialVersionUID也可以不聲明。如果不手動(dòng)指定 serialVersionUID 的值抬虽,反序列化時(shí)如果當(dāng)前類有所改變( 比如增刪了某些成員變量) 官觅,那么系統(tǒng)就會(huì)重新計(jì)算當(dāng)前類的hash值并更新 serialVersionUID 。這個(gè)時(shí)候當(dāng)前類的 serialVersionUID 就和序列化數(shù)據(jù)中的serialVersionUID 不一致阐污,導(dǎo)致反序列化失敗休涤,程序就出現(xiàn)crash。
靜態(tài)成員變量屬于類不屬于對象笛辟,不參與序列化過程功氨,其次 transient 關(guān)鍵字標(biāo)記的成員變量也不參與序列化過程。
通過重寫writeObject和readObject方法可以改變系統(tǒng)默認(rèn)的序列化過程手幢。
2.3.2 Parcelable接口
Parcel內(nèi)部包裝了可序列化的數(shù)據(jù)捷凄,可以在Binder中自由傳輸。序列化過程中需要實(shí)現(xiàn)的功能有序列化弯菊、反序列化和內(nèi)容描述纵势。
序列化功能由 writeToParcel 方法完成,最終是通過 Parcel 的一系列writer方法來完成。
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(code);
out.writeString(name);
}
反序列化功能由 CREATOR 來完成管钳,其內(nèi)部表明了如何創(chuàng)建序列化對象和數(shù)組钦铁,通過 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)方法中才漆,如果有一個(gè)成員變量是另一個(gè)可序列化對象牛曹,在反序列化過程中需要傳遞當(dāng)前線程的上下文類加載器,否則會(huì)報(bào)無法找到類的錯(cuò)誤醇滥。
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
內(nèi)容描述功能由 describeContents 方法完成黎比,幾乎所有情況下都應(yīng)該返回0,僅當(dāng)當(dāng)前對象中存在文件描述符時(shí)返回1鸳玩。
public int describeContents() {
return 0;
}
Serializable 是Java的序列化接口阅虫,使用簡單但開銷大,序列化和反序列化過程需要大量I/O操作不跟。而 Parcelable 是Android中的序列化方式颓帝,適合在Android平臺(tái)使用,效率高但是使用麻煩。 Parcelable 主要在內(nèi)存序列化上购城,Parcelable 也可以將對象序列化到存儲(chǔ)設(shè)備中或者將對象序列化后通過網(wǎng)絡(luò)傳輸吕座,但是稍顯復(fù)雜,推薦使用 Serializable 瘪板。
2.3.3 Binder
Binder是Android中的一個(gè)類吴趴,實(shí)現(xiàn)了 IBinder 接口。從IPC角度說侮攀,Binder是Andoird的一種跨進(jìn)程通訊方式锣枝,Binder還可以理解為一種虛擬物理設(shè)備,它的設(shè)備驅(qū)動(dòng)是/dev/binder魏身。從Android Framework角度來說吼鱼,Binder是 ServiceManager 連接各種Manager( ActivityManager· 烤宙、 WindowManager )和相應(yīng) ManagerService 的橋梁。從Android應(yīng)用層來說碍扔,Binder是客戶端和服務(wù)端進(jìn)行通信的媒介回季,當(dāng)bindService時(shí)家制,服務(wù)端返回一個(gè)包含服務(wù)端業(yè)務(wù)調(diào)用的Binder對象,通過這個(gè)Binder對象泡一,客戶端就可以獲取服務(wù)器端提供的服務(wù)或者數(shù)據(jù)( 包括普通服務(wù)和基于AIDL的服務(wù))颤殴。
Binder通信采用C/S架構(gòu),從組件視角來說鼻忠,包含Client涵但、Server、ServiceManager以及binder驅(qū)動(dòng)帖蔓,其中ServiceManager用于管理系統(tǒng)中的各種服務(wù)矮瘟。
圖中的Client,Server,Service Manager之間交互都是虛線表示,是由于它們彼此之間不是直接交互的塑娇,而是都通過與Binder驅(qū)動(dòng)進(jìn)行交互的澈侠,從而實(shí)現(xiàn)IPC通信方式。其中Binder驅(qū)動(dòng)位于內(nèi)核空間埋酬,Client,Server,Service Manager位于用戶空間哨啃。Binder驅(qū)動(dòng)和Service Manager可以看做是Android平臺(tái)的基礎(chǔ)架構(gòu),而Client和Server是Android的應(yīng)用層写妥,開發(fā)人員只需自定義實(shí)現(xiàn)client拳球、Server端,借助Android的基本平臺(tái)架構(gòu)便可以直接進(jìn)行IPC通信珍特。
http://gityuan.com/2015/10/31/binder-prepare/
Android中Binder主要用于 Service 祝峻,包括AIDL和Messenger。普通Service的Binder不涉及進(jìn)程間通信,Messenger的底層其實(shí)是AIDL呼猪,所以下面通過AIDL分析Binder的工作機(jī)制画畅。
由系統(tǒng)根據(jù)AIDL文件自動(dòng)生成.java文件
- Book.java
表示圖書信息的實(shí)體類,實(shí)現(xiàn)了Parcelable接口宋距。 - Book.aidl
Book類在AIDL中的聲明轴踱。 - IBookManager.aidl
定義的管理Book實(shí)體的一個(gè)接口,包含 getBookList 和 addBook 兩個(gè)方法谚赎。盡管Book類和IBookManager位于相同的包中淫僻,但是在IBookManager仍然要導(dǎo)入Book類。 - IBookManager.java
系統(tǒng)為IBookManager.aidl生產(chǎn)的Binder類壶唤,在 gen 目錄下雳灵。
IBookManager繼承了 IInterface 接口,所有在Binder中傳輸?shù)慕涌诙夹枰^IInterface接口闸盔。結(jié)構(gòu)如下:- 聲明了 getBookList 和 addBook 方法悯辙,還聲明了兩個(gè)整型id分別標(biāo)識(shí)這兩個(gè)方法,用于標(biāo)識(shí)在 transact 過程中客戶端請求的到底是哪個(gè)方法迎吵。
- 聲明了一個(gè)內(nèi)部類 Stub 躲撰,這個(gè) Stub 就是一個(gè)Binder類,當(dāng)客戶端和服務(wù)端位于同一進(jìn)程時(shí)击费,方法調(diào)用不會(huì)走跨進(jìn)程的 transact 拢蛋。當(dāng)二者位于不同進(jìn)程時(shí),方法調(diào)用需要走 transact 過程蔫巩,這個(gè)邏輯有 Stub 的內(nèi)部代理類 Proxy 來完成谆棱。
- 這個(gè)接口的核心實(shí)現(xiàn)就是它的內(nèi)部類 Stub 和 Stub 的內(nèi)部代理類 Proxy 。
Stub和Proxy類的內(nèi)部方法和定義
- DESCRIPTOR
Binder的唯一標(biāo)識(shí)圆仔,一般用Binder的類名表示垃瞧。 - asInterface(android.os.IBinder obj)
將服務(wù)端的Binder對象轉(zhuǎn)換為客戶端所需的AIDL接口類型的對象,如果C/S位于同一進(jìn)
程荧缘,此方法返回就是服務(wù)端的Stub對象本身皆警,否則返回的就是系統(tǒng)封裝后的Stub.proxy對
象。 - asBinder
返回當(dāng)前Binder對象截粗。 - onTransact
這個(gè)方法運(yùn)行在服務(wù)端的Binder線程池中信姓,由客戶端發(fā)起跨進(jìn)程請求時(shí),遠(yuǎn)程請求會(huì)通過
系統(tǒng)底層封裝后交由此方法來處理绸罗。該方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)- 服務(wù)端通過code確定客戶端請求的目標(biāo)方法是什么意推,
- 接著從data取出目標(biāo)方法所需的參數(shù),然后執(zhí)行目標(biāo)方法珊蟀。
- 執(zhí)行完畢后向reply寫入返回值( 如果有返回值) 菊值。
- 如果這個(gè)方法返回值為false外驱,那么服務(wù)端的請求會(huì)失敗,利用這個(gè)特性我們可以來做權(quán)限驗(yàn)證腻窒。
- Proxy#getBookList 和Proxy#addBook
這兩個(gè)方法運(yùn)行在客戶端昵宇,內(nèi)部實(shí)現(xiàn)過程如下:- 首先創(chuàng)建該方法所需要的輸入型對象Parcel對象_data,輸出型Parcel對象_reply和返回值對象List儿子。
- 然后把該方法的參數(shù)信息寫入_data( 如果有參數(shù))
- 接著調(diào)用transact方法發(fā)起RPC( 遠(yuǎn)程過程調(diào)用) 瓦哎,同時(shí)當(dāng)前線程掛起
- 然后服務(wù)端的onTransact方法會(huì)被調(diào)用知道RPC過程返回后,當(dāng)前線程繼續(xù)執(zhí)行柔逼,并從_reply中取出RPC過程的返回結(jié)果蒋譬,最后返回_reply中的數(shù)據(jù)。
AIDL文件不是必須的愉适,之所以提供AIDL文件犯助,是為了方便系統(tǒng)為我們生成IBookManager.java,但我們完全可以自己寫一個(gè)维咸。
linkToDeath和unlinkToDeath
如果服務(wù)端進(jìn)程異常終止剂买,我們到服務(wù)端的Binder連接斷裂。但是腰湾,如果我們不知道Binder連接已經(jīng)斷裂雷恃,那么客戶端功能會(huì)受影響疆股。通過linkTODeath我們可以給Binder設(shè)置一個(gè)死亡代理费坊,當(dāng)Binder死亡時(shí),我們就會(huì)收到通知旬痹。
- 聲明一個(gè) DeathRecipient 對象附井。 DeathRecipient 是一個(gè)接口,只有一個(gè)方法 binderDied 两残,當(dāng)Binder死亡的時(shí)候永毅,系統(tǒng)就會(huì)回調(diào) binderDied 方法,然后我們就可以重新綁定遠(yuǎn)程服務(wù)人弓。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
// TODO:這里重新綁定遠(yuǎn)程Service
}
} - 在客戶端綁定遠(yuǎn)程服務(wù)成功后沼死,給binder設(shè)置死亡代理:
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 數(shù)據(jù)健芭。
Bundle實(shí)現(xiàn)了Parcelable接口县钥,因此可以方便的在不同進(jìn)程間傳輸。當(dāng)我們在一個(gè)進(jìn)程中啟動(dòng)了另一個(gè)進(jìn)程的Activity慈迈、Service若贮、Receiver,可以再Bundle中附加我們需要傳輸給遠(yuǎn)程進(jìn)程的消息并通過Intent發(fā)送出去。被傳輸?shù)臄?shù)據(jù)必須能夠被序列化谴麦。
2.4.2 使用文件共享
我們可以序列化一個(gè)對象到文件系統(tǒng)中的同時(shí)從另一個(gè)進(jìn)程中恢復(fù)這個(gè)對象蠢沿。
- 通過 ObjectOutputStream / ObjectInputStream 序列化一個(gè)對象到文件中,或者在另一個(gè)進(jìn)程從文件中反序列這個(gè)對象匾效。注意:反序列化得到的對象只是內(nèi)容上和序列化之前的對象一樣搏予,本質(zhì)是兩個(gè)對象。
- 文件并發(fā)讀寫會(huì)導(dǎo)致讀出的對象可能不是最新的弧轧,并發(fā)寫的話那就更嚴(yán)重了 雪侥。所以文件共享方式適合對數(shù)據(jù)同步要求不高的進(jìn)程之間進(jìn)行通信,并且要妥善處理并發(fā)讀寫問題精绎。
- SharedPreferences 底層實(shí)現(xiàn)采用XML文件來存儲(chǔ)鍵值對速缨。系統(tǒng)對它的讀/寫有一定的緩存策略,即在內(nèi)存中會(huì)有一份 SharedPreferences 文件的緩存代乃,因此在多進(jìn)程模式下旬牲,系統(tǒng)對它的讀/寫變得不可靠,面對高并發(fā)讀/寫時(shí) SharedPreferences 有很大幾率丟失數(shù)據(jù)搁吓,因此不建議在IPC中使用 SharedPreferences 原茅。
2.4.3 使用Messenger
Messenger可以在不同進(jìn)程間傳遞Message對象。是一種輕量級的IPC方案堕仔,底層實(shí)現(xiàn)是AIDL擂橘。它對AIDL進(jìn)行了封裝,使得我們可以更簡便的進(jìn)行IPC摩骨。
具體使用時(shí)通贞,分為服務(wù)端和客戶端:
- 服務(wù)端:創(chuàng)建一個(gè)Service來處理客戶端請求,同時(shí)創(chuàng)建一個(gè)Handler并通過它來創(chuàng)建一個(gè)
Messenger恼五,然后再Service的onBind中返回Messenger對象底層的Binder即可昌罩。
private final Messenger mMessenger = new Messenger (new xxxHandler()); - 客戶端:綁定服務(wù)端的Sevice,利用服務(wù)端返回的IBinder對象來創(chuàng)建一個(gè)Messenger灾馒,通過這個(gè)Messenger就可以向服務(wù)端發(fā)送消息了茎用,消息類型是 Message 。如果需要服務(wù)端響應(yīng)睬罗,則需要?jiǎng)?chuàng)建一個(gè)Handler并通過它來創(chuàng)建一個(gè)Messenger( 和服務(wù)端一樣) 轨功,并通過 Message 的 replyTo 參數(shù)傳遞給服務(wù)端。服務(wù)端通過Message的 replyTo 參數(shù)就可以回應(yīng)客戶端了傅物。
總而言之夯辖,就是客戶端和服務(wù)端 拿到對方的Messenger來發(fā)送 Message 。只不過客戶端通過bindService 而服務(wù)端通過 message.replyTo 來獲得對方的Messenger董饰。
Messenger中有一個(gè) Hanlder 以串行的方式處理隊(duì)列中的消息蒿褂。不存在并發(fā)執(zhí)行圆米,因此我們不用考慮線程同步的問題。
2.4.4 使用AIDL
如果有大量的并發(fā)請求啄栓,使用Messenger就不太適合娄帖,同時(shí)如果需要跨進(jìn)程調(diào)用服務(wù)端的方法,Messenger就無法做到了昙楚。這時(shí)我們可以使用AIDL近速。
流程如下:
- 服務(wù)端需要?jiǎng)?chuàng)建Service來監(jiān)聽客戶端請求,然后創(chuàng)建一個(gè)AIDL文件堪旧,將暴露給客戶端的接口在AIDL文件中聲明削葱,最后在Service中實(shí)現(xiàn)這個(gè)AIDL接口即可。
- 客戶端首先綁定服務(wù)端的Service淳梦,綁定成功后析砸,將服務(wù)端返回的Binder對象轉(zhuǎn)成AIDL接口所屬的類型,接著就可以調(diào)用AIDL中的方法了爆袍。
AIDL支持的數(shù)據(jù)類型:
- 基本數(shù)據(jù)類型首繁、String、CharSequence
- List:只支持ArrayList陨囊,里面的每個(gè)元素必須被AIDL支持
- Map:只支持HashMap弦疮,里面的每個(gè)元素必須被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
自定義的Parcelable對象和AIDL對象,不管它們與當(dāng)前的AIDL文件是否位于同一個(gè)包蜘醋,都必須顯式import進(jìn)來胁塞。
如果AIDL文件中使用了自定義的Parcelable對象,就必須新建一個(gè)和它同名的AIDL文件堂湖,并在其中聲明它為Parcelable類型闲先。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的參數(shù)除了基本類型以外都必須表明方向in/out。AIDL接口文件中只支持方法无蜂,不支持聲明靜態(tài)常量。建議把所有和AIDL相關(guān)的類和文件放在同一個(gè)包中蒙谓,方便管理斥季。
void addBook(in Book book);
AIDL方法是在服務(wù)端的Binder線程池中執(zhí)行的,因此當(dāng)多個(gè)客戶端同時(shí)連接時(shí)累驮,管理數(shù)據(jù)的集合直接采用 CopyOnWriteArrayList 來進(jìn)行自動(dòng)線程同步酣倾。類似的還有 ConcurrentHashMap 。
因?yàn)榭蛻舳说膌istener和服務(wù)端的listener不是同一個(gè)對象谤专,所以 RecmoteCallbackList 是系統(tǒng)專門提供用于刪除跨進(jìn)程listener的接口躁锡,支持管理任意的AIDL接口,因?yàn)樗蠥IDL接口都繼承自 IInterface 接口置侍。
public class RemoteCallbackList<E extends IInterface>
它內(nèi)部通過一個(gè)Map接口來保存所有的AIDL回調(diào)映之,這個(gè)Map的key是 IBinder 類型拦焚,value是 Callback 類型。當(dāng)客戶端解除注冊時(shí)杠输,遍歷服務(wù)端所有l(wèi)istener赎败,找到和客戶端listener具有相同Binder對象的服務(wù)端listenr并把它刪掉。
==客戶端RPC的時(shí)候線程會(huì)被掛起蠢甲,由于被調(diào)用的方法運(yùn)行在服務(wù)端的Binder線程池中僵刮,可能很耗時(shí),不能在主線程中去調(diào)用服務(wù)端的方法鹦牛。==
權(quán)限驗(yàn)證
默認(rèn)情況下搞糕,我們的遠(yuǎn)程服務(wù)任何人都可以連接,我們必須加入權(quán)限驗(yàn)證功能曼追,權(quán)限驗(yàn)證失敗則無法調(diào)用服務(wù)中的方法寞宫。通常有兩種驗(yàn)證方法:
- 在onBind中驗(yàn)證,驗(yàn)證不通過返回null
驗(yàn)證方式比如permission驗(yàn)證拉鹃,在AndroidManifest聲明:
<permission
android:name="com.rgy.chapter_2.permisson.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
Android自定義權(quán)限和使用權(quán)限
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中驗(yàn)證,驗(yàn)證不通過返回false
可以permission驗(yàn)證膏燕,還可以采用Uid和Pid驗(yàn)證钥屈。
2.4.5 使用ContentProvider
==ContentProvider是四大組件之一,天生就是用來進(jìn)程間通信坝辫。和Messenger一樣助析,其底層實(shí)現(xiàn)是用Binder。==
系統(tǒng)預(yù)置了許多ContentProvider莽红,比如通訊錄嘱兼、日程表等。要RPC訪問這些信息及舍,只需要通過ContentResolver的query未辆、update、insert和delete方法即可锯玛。
創(chuàng)建自定義的ContentProvider咐柜,只需繼承ContentProvider類并實(shí)現(xiàn) onCreate 、 query 攘残、 update 拙友、 insert 、 getType 六個(gè)抽象方法即可歼郭。getType用來返回一個(gè)Uri請求所對應(yīng)的MIME類型遗契,剩下四個(gè)方法對應(yīng)于CRUD操作。這六個(gè)方法都運(yùn)行在ContentProvider進(jìn)程中病曾,除了 onCreate 由系統(tǒng)回調(diào)并運(yùn)行在主線程里牍蜂,其他五個(gè)方法都由外界調(diào)用并運(yùn)行在Binder線程池中漾根。
ContentProvider是通過Uri來區(qū)分外界要訪問的數(shù)據(jù)集合,例如外界訪問ContentProvider中的表捷兰,我們需要為它們定義單獨(dú)的Uri和Uri_Code立叛。根據(jù)Uri_Code,我們就知道要訪問哪個(gè)表了贡茅。
==query秘蛇、update、insert顶考、delete四大方法存在多線程并發(fā)訪問赁还,因此方法內(nèi)部要做好線程同步。==若采用SQLite并且只有一個(gè)SQLiteDatabase驹沿,SQLiteDatabase內(nèi)部已經(jīng)做了同步處理艘策。若是多個(gè)SQLiteDatabase或是采用List作為底層數(shù)據(jù)集,就必須做線程同步渊季。
2.4.6 使用Socket
Socket也稱為“套接字”朋蔫,分為流式套接字和用戶數(shù)據(jù)報(bào)套接字兩種,分別對應(yīng)于TCP和UDP協(xié)議却汉。Socket可以實(shí)現(xiàn)計(jì)算機(jī)網(wǎng)絡(luò)中的兩個(gè)進(jìn)程間的通信驯妄,當(dāng)然也可以在本地實(shí)現(xiàn)進(jìn)程間的通信。我們以一個(gè)跨進(jìn)程的聊天程序來演示合砂。
在遠(yuǎn)程Service建立一個(gè)TCP服務(wù)青扔,然后在主界面中連接TCP服務(wù)。服務(wù)端Service監(jiān)聽本地端口翩伪,客戶端連接指定的端口微猖,建立連接成功后,拿到 Socket 對象就可以向服務(wù)端發(fā)送消息或者接受服務(wù)端發(fā)送的消息缘屹。
本例的客戶端和服務(wù)端源代碼
除了采用TCP套接字凛剥,也可以用UDP套接字。實(shí)際上socket不僅能實(shí)現(xiàn)進(jìn)程間的通信囊颅,還可以實(shí)現(xiàn)設(shè)備間的通信(只要設(shè)備之間的IP地址互相可見)当悔。
2.5 Binder連接池
前面提到AIDL的流程是:首先創(chuàng)建一個(gè)service和AIDL接口,接著創(chuàng)建一個(gè)類繼承自AIDL接口中的Stub類并實(shí)現(xiàn)Stub中的抽象方法踢代,客戶端在Service的onBind方法中拿到這個(gè)類的對象,然后綁定這個(gè)service嗅骄,建立連接后就可以通過這個(gè)Stub對象進(jìn)行RPC胳挎。
那么如果項(xiàng)目龐大,有多個(gè)業(yè)務(wù)模塊都需要使用AIDL進(jìn)行IPC溺森,隨著AIDL數(shù)量的增加慕爬,我們不能無限制地增加Service窑眯,我們需要把所有AIDL放在同一個(gè)Service中去管理。
- 服務(wù)端只有一個(gè)Service医窿,把所有AIDL放在一個(gè)Service中磅甩,不同業(yè)務(wù)模塊之間不能有耦合
- 服務(wù)端提供一個(gè) queryBinder 接口,這個(gè)接口能夠根據(jù)業(yè)務(wù)模塊的特征來返回響應(yīng)的Binder對象給客戶端
- 不同的業(yè)務(wù)模塊拿到所需的Binder對象就可以進(jìn)行RPC了
2.6 選用合適的IPC方式
3 View的事件體系
本章介紹View的事件分發(fā)和滑動(dòng)沖突問題的解決方案姥卢。
3.1 view的基礎(chǔ)知識(shí)
View的位置參數(shù)卷要、MotionEvent和TouchSlop對象、VelocityTracker独榴、GestureDetector和Scroller對象僧叉。
3.1.1什么是view
View是Android中所有控件的基類,View的本身可以是單個(gè)空間棺榔,也可以是多個(gè)控件組成的一組控件瓶堕,即ViewGroup,ViewGroup繼承自View症歇,其內(nèi)部可以有子View郎笆,這樣就形成了View樹的結(jié)構(gòu)。
3.1.2 View的位置參數(shù)
View的位置主要由它的四個(gè)頂點(diǎn)來決定忘晤,即它的四個(gè)屬性:top宛蚓、left、right德频、bottom苍息,分別表示View左上角的坐標(biāo)點(diǎn)( top,left) 以及右下角的坐標(biāo)點(diǎn)( right壹置,bottom) 竞思。
同時(shí),我們可以得到View的大谐ぁ:
width = right - left
height = bottom - top
而這四個(gè)參數(shù)可以由以下方式獲雀桥纭:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
Android3.0后,View增加了x难咕、y课梳、translationX和translationY這幾個(gè)參數(shù)。其中x和y是View左上角的坐標(biāo)余佃,而translationX和translationY是View左上角相對于容器的偏移量暮刃。他們之間的換算關(guān)系如下:
x = left + translationX;
y = top + translationY;
top,left表示原始左上角坐標(biāo),而x,y表示變化后的左上角坐標(biāo)爆土。在View沒有平移時(shí)椭懊,x=left,y=top。==View平移的過程中步势,top和left不會(huì)改變氧猬,改變的是x背犯、y、translationX和translationY盅抚。==
3.1.3 MotionEvent和TouchSlop
MotionEvent
事件類型
- ACTION_DOWN 手指剛接觸屏幕
- ACTION_MOVE 手指在屏幕上移動(dòng)
- ACTION_UP 手指從屏幕上松開
點(diǎn)擊事件類型
- 點(diǎn)擊屏幕后離開松開漠魏,事件序列為DOWN->UP
- 點(diǎn)擊屏幕滑動(dòng)一會(huì)再松開,事件序列為DOWN->MOVE->...->MOVE->UP
通過MotionEven對象我們可以得到事件發(fā)生的x和y坐標(biāo)妄均,我們可以通過getX/getY和getRawX/getRawY得到柱锹。它們的區(qū)別是:getX/getY返回的是相對于當(dāng)前View左上角的x和y坐標(biāo),getRawX/getRawY返回的是相對于手機(jī)屏幕左上角的x和y坐標(biāo)丛晦。
TouchSloup
TouchSloup是系統(tǒng)所能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離奕纫,這是一個(gè)常量,與設(shè)備有關(guān)烫沙,可通過以下方法獲得:
ViewConfiguration.get(getContext()).getScaledTouchSloup().
當(dāng)我們處理滑動(dòng)時(shí)匹层,比如滑動(dòng)距離小于這個(gè)值,我們就可以過濾這個(gè)事件(系統(tǒng)會(huì)默認(rèn)過濾)锌蓄,從而有更好的用戶體驗(yàn)升筏。
3.1.4 VelocityTracker、GestureDetector和Scroller
VelocityTracker
速度追蹤瘸爽,用于追蹤手指在滑動(dòng)過程中的速度您访,包括水平放向速度和豎直方向速度。使用方法:
- 在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event); - 計(jì)算速度剪决,獲得水平速度和豎直速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
注意灵汪,獲取速度之前必須先計(jì)算速度,即調(diào)用computeCurrentVelocity方法柑潦,這里指的速度是指一段時(shí)間內(nèi)手指滑過的像素?cái)?shù)享言,1000指的是1000毫秒,得到的是1000毫秒內(nèi)滑過的像素?cái)?shù)渗鬼。速度可正可負(fù):速度 = ( 終點(diǎn)位置 - 起點(diǎn)位置) / 時(shí)間段 - 最后览露,當(dāng)不需要使用的時(shí)候,需要調(diào)用clear()方法重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手勢檢測譬胎,用于輔助檢測用戶的單擊差牛、滑動(dòng)、長按堰乔、雙擊等行為偏化。使用方法:
- 創(chuàng)建一個(gè)GestureDetector對象并實(shí)現(xiàn)OnGestureListener接口,根據(jù)需要镐侯,也可實(shí)現(xiàn)OnDoubleTapListener接口從而監(jiān)聽雙擊行為:
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕后無法拖動(dòng)的現(xiàn)象
mGestureDetector.setIsLongpressEnabled(false); - 在目標(biāo)View的OnTouchEvent方法中添加以下實(shí)現(xiàn):
boolean consume = mGestureDetector.onTouchEvent(event);
return consume; -
實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener接口中的方法
其中常用的方法有:onSingleTapUp(單擊)夹孔、onFling(快速滑動(dòng))、onScroll(拖動(dòng))析孽、onLongPress(長按)和onDoubleTap( 雙擊)搭伤。建議:如果只是監(jiān)聽滑動(dòng)相關(guān)的,可以自己在onTouchEvent中實(shí)現(xiàn)袜瞬,如果要監(jiān)聽雙擊這種行為怜俐,那么就使用GestureDetector。
Scroller
彈性滑動(dòng)對象邓尤,用于實(shí)現(xiàn)View的彈性滑動(dòng)拍鲤。其本身無法讓View彈性滑動(dòng),需要和View的computeScroll方法配合使用才能完成這個(gè)功能汞扎。使用方法:
Scroller scroller = new Scroller(mContext);
//緩慢移動(dòng)到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms內(nèi)滑向destX,效果就是慢慢滑動(dòng)
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX,mScroller.getCurrY());
postInvalidate();
}
}
3.2 View的滑動(dòng)
三種方式實(shí)現(xiàn)View滑動(dòng)
3.2.1 使用scrollTo/scrollBy
scrollBy實(shí)際調(diào)用了scrollTo季稳,它實(shí)現(xiàn)了基于當(dāng)前位置的相對滑動(dòng),而scrollTo則實(shí)現(xiàn)了絕對滑動(dòng)澈魄。
==scrollTo和scrollBy只能改變View的內(nèi)容位置而不能改變View在布局中的位置景鼠。滑動(dòng)偏移量mScrollX和mScrollY的正負(fù)與實(shí)際滑動(dòng)方向相反痹扇,即從左向右滑動(dòng)铛漓,mScrollX為負(fù)值,從上往下滑動(dòng)mScrollY為負(fù)值鲫构。==
3.2.2 使用動(dòng)畫
使用動(dòng)畫移動(dòng)View浓恶,主要是操作View的translationX和translationY屬性,既可以采用傳統(tǒng)的View動(dòng)畫结笨,也可以采用屬性動(dòng)畫包晰,如果使用屬性動(dòng)畫,為了能夠兼容3.0以下的版本炕吸,需要采用開源動(dòng)畫庫nineolddandroids伐憾。 如使用屬性動(dòng)畫:(View在100ms內(nèi)向右移動(dòng)100像素)
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
3.2.3 改變布局屬性
通過改變布局屬性來移動(dòng)View,即改變LayoutParams算途。
3.2.4 各種滑動(dòng)方式的對比
- scrollTo/scrollBy:操作簡單塞耕,適合對View內(nèi)容的滑動(dòng);
- 動(dòng)畫:操作簡單嘴瓤,主要適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果扫外;
- 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View廓脆。
3.3 彈性滑動(dòng)
3.3.1 使用Scroller
使用Scroller實(shí)現(xiàn)彈性滑動(dòng)的典型使用方法如下:
Scroller scroller = new Scroller(mContext);
//緩慢移動(dòng)到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms內(nèi)滑向destX筛谚,效果就是緩慢滑動(dòng)
mScroller.startSscroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
從上面代碼可以知道,我們首先會(huì)構(gòu)造一個(gè)Scroller對象停忿,并調(diào)用他的startScroll方法驾讲,該方法并沒有讓view實(shí)現(xiàn)滑動(dòng),只是把參數(shù)保存下來,我們來看看startScroll方法的實(shí)現(xiàn)就知道了:
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方法的幾個(gè)參數(shù)的含義时迫,startX和startY表示滑動(dòng)的起點(diǎn),dx和dy表示的是滑動(dòng)的距離谓晌,而duration表示的是滑動(dòng)時(shí)間掠拳,注意,這里的滑動(dòng)指的是View內(nèi)容的滑動(dòng)纸肉,在startScroll方法被調(diào)用后溺欧,馬上調(diào)用invalidate方法,這是滑動(dòng)的開始柏肪,invalidate方法會(huì)導(dǎo)致View的重繪姐刁,在View的draw方法中調(diào)用computeScroll方法,computeScroll又會(huì)去向Scroller獲取當(dāng)前的scrollX和scrollY烦味;然后通過scrollTo方法實(shí)現(xiàn)滑動(dòng)聂使,接著又調(diào)用postInvalidate方法進(jìn)行第二次重繪,一直循環(huán)拐叉,直到computeScrollOffset()方法返回值為false才結(jié)束整個(gè)滑動(dòng)過程岩遗。 我們可以看看computeScrollOffset方法是如何獲得當(dāng)前的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獲取當(dāng)前的scrollX和scrollY其實(shí)是通過計(jì)算時(shí)間流逝的百分比來獲得的凤瘦,每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間距宿礁,通過這個(gè)時(shí)間間距Scroller就可以得到View當(dāng)前的滑動(dòng)位置,然后就可以通過scrollTo方法來完成View的滑動(dòng)了蔬芥。
3.3.2 通過動(dòng)畫
動(dòng)畫本身就是一種漸近的過程梆靖,因此通過動(dòng)畫來實(shí)現(xiàn)的滑動(dòng)本身就具有彈性。實(shí)現(xiàn)也很簡單:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
;
//當(dāng)然笔诵,我們也可以利用動(dòng)畫來模仿Scroller實(shí)現(xiàn)View彈性滑動(dòng)的過程:
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();
上面的動(dòng)畫本質(zhì)上是沒有作用于任何對象上的返吻,他只是在1000ms內(nèi)完成了整個(gè)動(dòng)畫過程,利用這個(gè)特性乎婿,我們就可以在動(dòng)畫的每一幀到來時(shí)獲取動(dòng)畫完成的比例测僵,根據(jù)比例計(jì)算出View所滑動(dòng)的距離。采用這種方法也可以實(shí)現(xiàn)其他動(dòng)畫效果谢翎,我們可以在onAnimationUpdate方法中加入自定義操作捍靠。
3.3.3 使用延時(shí)策略
延時(shí)策略的核心思想是通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過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的事件分發(fā)機(jī)制
3.4.1 點(diǎn)擊事件的傳遞規(guī)則
點(diǎn)擊事件是MotionEvent。首先我們先看看下面一段偽代碼褒侧,通過它我們可以理解到點(diǎn)擊事件的傳遞規(guī)則:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
上面代碼主要涉及到以下三個(gè)方法:
- public boolean dispatchTouchEvent(MotionEvent ev);
這個(gè)方法用來進(jìn)行事件的分發(fā)良风。如果事件傳遞給當(dāng)前view谊迄,則調(diào)用此方法。返回結(jié)果表示是否消耗此事件烟央,受onTouchEvent和下級View的dispatchTouchEvent方法影響统诺。 - public boolean onInterceptTouchEvent(MotionEvent ev);
這個(gè)方法用來判斷是否攔截事件。在dispatchTouchEvent方法中調(diào)用吊档。返回結(jié)果表示是否攔截篙议。 - public boolean onTouchEvent(MotionEvent ev);
這個(gè)方法用來處理點(diǎn)擊事件。在dispatchTouchEvent方法中調(diào)用怠硼,返回結(jié)果表示是否消耗事件稍走。如果不消耗乏苦,則在同一個(gè)事件序列中兽泄,當(dāng)前View無法再次接收到事件遭殉。
點(diǎn)擊事件的傳遞規(guī)則:對于一個(gè)根ViewGroup裹匙,點(diǎn)擊事件產(chǎn)生后源武,首先會(huì)傳遞給他斯入,這時(shí)候就會(huì)調(diào)用他的dispatchTouchEvent方法晰搀,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要攔截事件嵌溢,接下來事件就會(huì)交給ViewGroup處理眯牧,調(diào)用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值為false赖草,表示ViewGroup不攔截該事件学少,這時(shí)事件就傳遞給他的子View,接下來子View的dispatchTouchEvent方法秧骑,如此反復(fù)直到事件被最終處理版确。
當(dāng)一個(gè)View需要處理事件時(shí),如果它設(shè)置了OnTouchListener乎折,那么onTouch方法會(huì)被調(diào)用绒疗,如果onTouch返回false,則當(dāng)前View的onTouchEvent方法會(huì)被調(diào)用骂澄,返回true則不會(huì)被調(diào)用吓蘑,同時(shí),在onTouchEvent方法中如果設(shè)置了OnClickListener坟冲,那么他的onClick方法會(huì)被調(diào)用磨镶。==由此可見處理事件時(shí)的優(yōu)先級關(guān)系: onTouchListener > onTouchEvent >onClickListener==
關(guān)于事件傳遞的機(jī)制,這里給出一些結(jié)論:
- 一個(gè)事件系列以down事件開始樱衷,中間包含數(shù)量不定的move事件棋嘲,最終以up事件結(jié)束。
- 正常情況下矩桂,一個(gè)事件序列只能由一個(gè)View攔截并消耗沸移。
- 某個(gè)View攔截了事件后痪伦,該事件序列只能由它去處理,并且它的onInterceptTouchEvent
不會(huì)再被調(diào)用雹锣。 - 某個(gè)View一旦開始處理事件网沾,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不會(huì)交給他處理蕊爵,并且事件將重新交由他的父元素去處理辉哥,即父元素的onTouchEvent被調(diào)用。
- 如果View不消耗ACTION_DOWN以外的其他事件攒射,那么這個(gè)事件將會(huì)消失醋旦,此時(shí)父元素的onTouchEvent并不會(huì)被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件会放,最終消失的點(diǎn)擊事件會(huì)傳遞給Activity去處理饲齐。
- ViewGroup默認(rèn)不攔截任何事件。
- View沒有onInterceptTouchEvent方法咧最,一旦事件傳遞給它捂人,它的onTouchEvent方法會(huì)被調(diào)用。
- View的onTouchEvent默認(rèn)消耗事件矢沿,除非他是不可點(diǎn)擊的( clickable和longClickable同時(shí)為false) 滥搭。View的longClickable屬性默認(rèn)false,clickable默認(rèn)屬性分情況(如TextView為false捣鲸,button為true)瑟匆。
- View的enable屬性不影響onTouchEvent的默認(rèn)返回值。
- onClick會(huì)發(fā)生的前提是當(dāng)前View是可點(diǎn)擊的摄狱,并且收到了down和up事件脓诡。
- 事件傳遞過程總是由外向內(nèi)的,即事件總是先傳遞給父元素媒役,然后由父元素分發(fā)給子View祝谚,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的分發(fā)過程,但是ACTION_DOWN事件除外酣衷。
3.4.2 事件分發(fā)的源碼解析
略
3.5 滑動(dòng)沖突
在界面中交惯,只要內(nèi)外兩層同時(shí)可以滑動(dòng),這個(gè)時(shí)候就會(huì)產(chǎn)生滑動(dòng)沖突穿仪∠滑動(dòng)沖突的解決有固定的方法。
3.5.1 常見的滑動(dòng)沖突場景
- 外部滑動(dòng)和內(nèi)部滑動(dòng)方向不一致啊片;
比如viewpager和listview嵌套只锻,但這種情況下viewpager自身已經(jīng)對滑動(dòng)沖突進(jìn)行了處理。 - 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致紫谷;
- 上面兩種情況的嵌套齐饮。
只要解決1和2即可捐寥。
3.5.2 滑動(dòng)沖突的處理規(guī)則
對于場景一,處理的規(guī)則是:當(dāng)用戶左右( 上下) 滑動(dòng)時(shí)祖驱,需要讓外部的View攔截點(diǎn)擊事件握恳,當(dāng)用戶上下( 左右) 滑動(dòng)的時(shí)候,需要讓內(nèi)部的View攔截點(diǎn)擊事件捺僻。根據(jù)滑動(dòng)的方向判斷誰來攔截事件乡洼。
對于場景二,由于滑動(dòng)方向一致匕坯,這時(shí)候只能在業(yè)務(wù)上找到突破點(diǎn)束昵,根據(jù)業(yè)務(wù)需求,規(guī)定什么時(shí)候讓外部View攔截事件醒颖,什么時(shí)候由內(nèi)部View攔截事件妻怎。
場景三的情況相對比較復(fù)雜,同樣根據(jù)需求在業(yè)務(wù)上找到突破點(diǎn)泞歉。
3.5.3 滑動(dòng)沖突的解決方式
外部攔截法
所謂外部攔截法是指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截匿辩,否則就不攔截腰耙。下面是偽代碼:
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 (父容器需要當(dāng)前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
針對不同沖突,只需修改父容器需要當(dāng)前事件的條件即可铲球。其他不需修改也不能修改挺庞。
- ACTION_DOWN:必須返回false。因?yàn)槿绻祷豻rue稼病,后續(xù)事件都會(huì)被攔截选侨,無法傳遞給子View。
- ACTION_MOVE:根據(jù)需要決定是否攔截
- ACTION_UP:必須返回false然走。如果攔截援制,那么子View無法接受up事件,無法完成click操作芍瑞。而如果是父容器需要該事件晨仑,那么在ACTION_MOVE時(shí)已經(jīng)進(jìn)行了攔截,根據(jù)上一節(jié)的結(jié)論3拆檬,ACTION_UP不會(huì)經(jīng)過onInterceptTouchEvent方法洪己,直接交給父容器處理。
內(nèi)部攔截法
內(nèi)部攔截法是指父容器不攔截任何事件竟贯,所有的事件都傳遞給子元素答捕,如果子元素需要此事件就直接消耗,否則就交由父容器進(jìn)行處理屑那。這種方法與Android事件分發(fā)機(jī)制不一致拱镐,需要配合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 (父容器需要此類點(diǎn)擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
==除了子元素需要做處理外,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其他事件痢站,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí)磷箕,父元素才能繼續(xù)攔截所需的事件。==因此阵难,父元素要做以下修改:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
優(yōu)化滑動(dòng)體驗(yàn):
mScroller.abortAnimation();
外部攔截法實(shí)例:HorizontalScrollViewEx
4 View的工作原理
主要內(nèi)容
- View的工作原理
- 自定義View的實(shí)現(xiàn)方式
- 自定義View的底層工作原理岳枷,比如View的測量流程、布局流程呜叫、繪制流程
- View常見的回調(diào)方法空繁,比如構(gòu)造方法、onAttach.onVisibilityChanged/onDetach等
4.1 初識(shí)ViewRoot和DecorView
ViewRoot的實(shí)現(xiàn)是 ViewRootImpl 類朱庆,是連接WindowManager和DecorView的紐帶盛泡,View的三大流程( mearsure、layout娱颊、draw) 均是通過ViewRoot來完成傲诵。當(dāng)Activity對象被創(chuàng)建完畢后,會(huì)將DecorView添加到Window中箱硕,同時(shí)創(chuàng)建 ViewRootImpl 對象拴竹,并將ViewRootImpl 對象和DecorView建立連接,源碼如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
View的繪制流程是從ViewRoot的performTraversals開始的
- measure用來測量View的寬高
- layout來確定View在父容器中的位置
- draw負(fù)責(zé)將View繪制在屏幕上
performTraversals會(huì)依次調(diào)用 performMeasure 剧罩、 performLayout 和performDraw 三個(gè)方法栓拜,這三個(gè)方法分別完成頂級View的measure、layout和draw這三大流程惠昔。其中 performMeasure 中會(huì)調(diào)用 measure 方法幕与,在 measure 方法中又會(huì)調(diào)用 onMeasure 方法,在 onMeasure 方法中則會(huì)對所有子元素進(jìn)行measure過程镇防,這樣就完成了一次measure過程啦鸣;子元素會(huì)重復(fù)父容器的measure過程,如此反復(fù)完成了整個(gè)View數(shù)的遍歷营罢。另外兩個(gè)過程同理赏陵。
- Measure完成后, 可以通過getMeasuredWidth 、getMeasureHeight 方法來獲取View測量后的寬/高饲漾。特殊情況下蝙搔,測量的寬高不等于最終的寬高,詳見后面考传。
- Layout過程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際View的寬高吃型,完成后可通過 getTop 、 getBotton 僚楞、 getLeft 和 getRight 拿到View的四個(gè)定點(diǎn)坐標(biāo)勤晚。
DecorView作為頂級View枉层,其實(shí)是一個(gè) FrameLayout ,它包含一個(gè)豎直方向的 LinearLayout 赐写,這個(gè) LinearLayout 分為標(biāo)題欄和內(nèi)容欄兩個(gè)部分鸟蜡。
在Activity通過setContextView所設(shè)置的布局文件其實(shí)就是被加載到內(nèi)容欄之中的。這個(gè)內(nèi)容欄的id是 R.android.id.content 挺邀,通過
ViewGroup content = findViewById(R.android.id.content);
可以得到這個(gè)contentView揉忘。View層的事件都是先經(jīng)過DecorView,然后才傳遞到子View端铛。
4.2 理解MeasureSpec
MeasureSpec決定了一個(gè)View的尺寸規(guī)格泣矛。但是父容器會(huì)影響View的MeasureSpec的創(chuàng)建過程。系統(tǒng)將View的 LayoutParams 根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的MeasureSpec禾蚕,然后根據(jù)這個(gè)MeasureSpec來測量出View的寬高您朽。
4.2.1 MeasureSpec
MeasureSpec代表一個(gè)32位int值,高2位代表SpecMode( 測量模式) 换淆,低30位代表SpecSize( 在某個(gè)測量模式下的規(guī)格大谢┳堋) 。
SpecMode有三種:
- UNSPECIFIED :父容器不對View進(jìn)行任何限制倍试,要多大給多大魂奥,一般用于系統(tǒng)內(nèi)部
- EXACTLY:父容器檢測到View所需要的精確大小,這時(shí)候View的最終大小就是SpecSize所指定的值易猫,對應(yīng)LayoutParams中的 match_parent 和具體數(shù)值這兩種模式
- AT_MOST :對應(yīng)View的默認(rèn)大小,不同View實(shí)現(xiàn)不同具壮,View的大小不能大于父容器的SpecSize准颓,對應(yīng) LayoutParams 中的 wrap_content
4.2.2 MeasureSpec和LayoutParams的對應(yīng)關(guān)系
對于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定棺妓。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定攘已。
View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方法怜跑,通過調(diào)用子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec样勃,再調(diào)用子元素的 measure 方法。
parentSize是指父容器中目前可使用的大小性芬。
- 當(dāng)View采用固定寬/高時(shí)( 即設(shè)置固定的dp/px) ,不管父容器的MeasureSpec是什么峡眶,View的MeasureSpec都是EXACTLY模式,并且大小遵循我們設(shè)置的值植锉。
- 當(dāng)View的寬/高是 match_parent 時(shí)辫樱,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空間。
- 當(dāng)View的寬/高是 wrap_content 時(shí)俊庇,View的MeasureSpec都是AT_MOST模式并且其大小不能超過父容器的剩余空間狮暑。
- 父容器的UNSPECIFIED模式鸡挠,一般用于系統(tǒng)內(nèi)部多次Measure時(shí),表示一種測量的狀態(tài)搬男,一般來說我們不需要關(guān)注此模式拣展。
4.3 View的工作流程
4.3.1 measure過程
View的measure過程
直接繼承View的自定義控件需要重寫 onMeasure 方法并設(shè)置 wrap_content ( 即specMode是 AT_MOST 模式) 時(shí)的自身大小,否則在布局中使用 wrap_content 相當(dāng)于使用 match_parent 缔逛。對于非 wrap_content 的情形备埃,我們沿用系統(tǒng)的測量值即可。
@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 模式下译株,給定一個(gè)默認(rèn)值mWidth,mHeight瓜喇。默認(rèn)寬高靈活指定
//參考TextView、ImageView的處理方式
//其他情況下沿用系統(tǒng)測量規(guī)則即可
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是一個(gè)抽象類歉糜,沒有重寫View的 onMeasure 方法乘寒,但是它提供了一個(gè) measureChildren 方法。這是因?yàn)椴煌腣iewGroup子類有不同的布局特性匪补,導(dǎo)致他們的測量細(xì)節(jié)各不相同伞辛,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實(shí)現(xiàn) onMeasure方法。
measureChildren方法的流程:
- 取出子View的 LayoutParams
- 通過 getChildMeasureSpec 方法來創(chuàng)建子元素的 MeasureSpec
- 將 MeasureSpec 直接傳遞給View的measure方法來進(jìn)行測量
通過LinearLayout的onMeasure方法里來分析ViewGroup的measure過程:
- LinearLayout在布局中如果使用match_parent或者具體數(shù)值夯缺,測量過程就和View一致蚤氏,即高度為specSize
- LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度總和踊兜,但不超過它的父容器的剩余空間
- LinearLayout的的最終高度同時(shí)也把豎直方向的padding考慮在內(nèi)
View的measure過程是三大流程中最復(fù)雜的一個(gè)竿滨,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高捏境。在某些情況下于游,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準(zhǔn)確的垫言。
==如果我們想要在Activity啟動(dòng)的時(shí)候就獲取一個(gè)View的寬高贰剥,怎么操作呢?==因?yàn)閂iew的measure過程和Activity的生命周期并不是同步執(zhí)行筷频,無法保證在Activity的 onCreate蚌成、onStart、onResume 時(shí)某個(gè)View就已經(jīng)測量完畢凛捏。所以有以下四種方式來獲取View的寬高:
- Activity/View#onWindowFocusChanged
onWindowFocusChanged這個(gè)方法的含義是:VieW已經(jīng)初始化完畢了担忧,寬高已經(jīng)準(zhǔn)備好了,需要注意:它會(huì)被調(diào)用多次葵袭,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)均會(huì)被調(diào)用涵妥。 - view.post(runnable)
通過post將一個(gè)runnable投遞到消息隊(duì)列的尾部,當(dāng)Looper調(diào)用此runnable的時(shí)候,View也初始化好了蓬网。 - ViewTreeObserver
使用 ViewTreeObserver 的眾多回調(diào)可以完成這個(gè)功能窒所,比如OnGlobalLayoutListener 這個(gè)接口,當(dāng)View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時(shí)帆锋,onGlobalLayout 方法會(huì)被回調(diào)吵取,這是獲取View寬高的好時(shí)機(jī)。需要注意的是锯厢,伴隨著View樹狀態(tài)的改變皮官, onGlobalLayout 會(huì)被回調(diào)多次。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)
手動(dòng)對view進(jìn)行measure实辑。需要根據(jù)View的layoutParams分情況處理:- match_parent:
無法measure出具體的寬高捺氢,因?yàn)椴恢栏溉萜鞯氖S嗫臻g,無法測量出View的大小 - 具體的數(shù)值( 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位二進(jìn)制表示剪撬,最大值30個(gè)1摄乒,在AT_MOST模式下,
// 我們用View理論上能支持的最大值去構(gòu)造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
4.3.2 layout過程
layout的作用是ViewGroup用來確定子View的位置残黑,當(dāng)ViewGroup的位置被確定后馍佑,它會(huì)在onLayout中遍歷所有的子View并調(diào)用其layout方法,在 layout 方法中梨水, onLayout 方法又會(huì)被調(diào)用寞奸。
View的 layout 方法確定本身的位置迄薄,源碼流程如下:
- setFrame 確定View的四個(gè)頂點(diǎn)位置诗眨,即確定了View在父容器中的位置
- 調(diào)用 onLayout 方法鳍鸵,確定所有子View的位置,和onMeasure一樣奇徒,onLayout的具體實(shí)現(xiàn)和布局有關(guān)歇终,因此View和ViewGroup均沒有真正實(shí)現(xiàn) onLayout 方法。
以LinearLayout的 onLayout 方法為例:
- 遍歷所有子View并調(diào)用 setChildFrame 方法來為子元素指定對應(yīng)的位置
- setChildFrame 方法實(shí)際上調(diào)用了子View的 layout 方法逼龟,形成了遞歸
==View的測量寬高和最終寬高的區(qū)別:==
在View的默認(rèn)實(shí)現(xiàn)中,View的測量寬高和最終寬高相等追葡,只不過測量寬高形成于measure過程腺律,最終寬高形成于layout過程。但重寫view的layout方法可以使他們不相等宜肉。
4.3.3 draw過程
View的繪制過程遵循如下幾步:
- 繪制背景 drawBackground(canvas)
- 繪制自己 onDraw
- 繪制children dispatchDraw 遍歷所有子View的 draw 方法
- 繪制裝飾 onDrawScrollBars
ViewGroup會(huì)默認(rèn)啟用 setWillNotDraw 為ture匀钧,導(dǎo)致系統(tǒng)不會(huì)去執(zhí)行 onDraw ,所以自定義ViewGroup需要通過onDraw來繪制內(nèi)容時(shí)谬返,必須顯式的關(guān)閉 WILL_NOT_DRAW 這個(gè)優(yōu)化標(biāo)記位之斯,即調(diào)用 setWillNotDraw(false);
4.4 自定義View
4.4.1 自定義View的分類
繼承View 重寫onDraw方法
通過 onDraw 方法來實(shí)現(xiàn)一些不規(guī)則的效果,這種效果不方便通過布局的組合方式來達(dá)到遣铝。這種方式需要自己支持 wrap_content 佑刷,并且padding也要去進(jìn)行處理莉擒。
繼承ViewGroup派生特殊的layout
實(shí)現(xiàn)自定義的布局方式,需要合適地處理ViewGroup的測量瘫絮、布局這兩個(gè)過程涨冀,并同時(shí)處理子View的測量和布局過程。
繼承特定的View子類( 如TextView麦萤、Button)
擴(kuò)展某種已有的控件的功能鹿鳖,比較簡單,不需要自己去管理 wrap_content 和padding壮莹。
** 繼承特定的ViewGroup子類( 如LinearLayout)**
比較常見翅帜,實(shí)現(xiàn)幾種view組合一起的效果。與方法二的差別是方法二更接近底層實(shí)現(xiàn)命满。
4.4.2 自定義View須知
- 直接繼承View或ViewGroup的控件涝滴, 需要在onmeasure中對wrap_content做特殊處理。指定wrap_content模式下的默認(rèn)寬/高周荐。
- 直接繼承View的控件狭莱,如果不在draw方法中處理padding,那么padding屬性就無法起作用概作。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響腋妙,不然padding和子元素的margin無效。
- 盡量不要用在View中使用Handler讯榕,因?yàn)闆]必要骤素。View內(nèi)部提供了post系列的方法,完全可以替代Handler的作用愚屁。
- View中有線程和動(dòng)畫济竹,需要在View的onDetachedFromWindow中停止。當(dāng)View不可見時(shí)霎槐,也需要停止線程和動(dòng)畫送浊,否則可能造成內(nèi)存泄漏。
- View帶有滑動(dòng)嵌套情形時(shí)丘跌,需要處理好滑動(dòng)沖突
4.4.3 自定義View實(shí)例
- 繼承View重寫onDraw方法:CircleView
自定義屬性設(shè)置方法:
- 在values目錄下創(chuàng)建自定義屬性的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的構(gòu)造方法中解析自定義屬性的值并做相應(yīng)處理闭树,這里我們解析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方法中,首先判斷是否有子元素报辱,沒有的話根據(jù)LayoutParams中的寬高做相應(yīng)處理与殃。然后判斷寬高是不是wrap_content,如果寬是,那么HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和幅疼。如果高是wrap_content米奸,HorizontalScrollViewEx的高度就是第一個(gè)子元素的高度。同時(shí)要處理padding和margin衣屏。
onLayout方法中躏升,在放置子元素時(shí)候也要考慮padding和margin。
4.4.4 自定義View的思想
- 掌握基本功狼忱,比如View的彈性滑動(dòng)膨疏、滑動(dòng)沖突、繪制原理等
- 面對新的自定義View時(shí)钻弄,對其分類并選擇合適的實(shí)現(xiàn)思路佃却。