原文地址:https://www.cnblogs.com/jooy/articles/8926144.html
本筆記整理自: 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/6577498javascript
本書是一本Android進階類書籍,採用理論、源碼和實踐相結合的方式來闡述高水準的Android應用開發要點。本書從三個方面來組織內容。
1. 介紹Android開發者不容易掌握的一些知識點
2. 結合Android源代碼和應用層開發過程,融會貫通,介紹一些比較深刻的知識點
3. 介紹一些核心技術和Android的性能優化思想php
目錄
第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性能優化css
- 1.1 Activity的生命週期全面分析
- 1.2 Activity的啓動模式
- 1.3 IntentFilter的匹配規則
- 2.1 Android IPC 簡介
- 2.2 Android中的多進程模式
- 2.3 IPC基礎概念介紹
- 2.4 Android中的IPC方式
- 2.5 Binder鏈接池
- 2.6 選用合適的IPC方式
- 3.1 view的基礎知識
- 3.2 View的滑動
- 3.3 彈性滑動
- 3.4 View的事件分發機制
- 3.5 滑動衝突
- 4.1 初識ViewRoot和DecorView
- 4.2 理解MeasureSpec
- 4.3 View的工做流程
- 4.4 自定義View
- 5.1 RemoteViews的應用
- 5.2 RemoteViews的內部機制
- 5.3 RemoteViews的意義
- 6.1 Drawable簡介
- 6.2 Drawable的分類
- 6.3 自定義Drawable
- 7.1 View動畫
- 7.2 View動畫的特殊使用場景
- 7.3 屬性動畫
- 7.4 使用動畫的注意事項
- 8.1 Window和WindowManager
- 8.2 Window的內部機制
- 8.3 Window的建立過程
- 9.1 四大組件的運行狀態
- 9.2 Activity的工做過程
- 9.3 Service的工做過程
- 9.4 BroadcastReceiver的工做過程
- 9.5 ContentProvider的工做機制
- 10.1 Android的消息機制概述
- 10.2 Android的消息機制分析
- 10.3 主線程的消息循環
- 11.1 主線程和子線程
- 11.2 Android中的線程形態
- 11.3 Android線程池
- 12.1 Bitmap的高效加載
- 12.2 Android中的緩存策略
- 12.3 ImageLoader的使用
- 13.1 使用CrashHandler來獲取應用的crash信息
- 13.2 使用multidex來解決方法數越界
- 13.3 Android動態加載技術
- 13.4 反編譯初步
- 14.1 JNI的開發流程
- 14.2 NDK的開發流程
- 14.3 JNI的數據類型和類型簽名
- 14.4 JNI調用Java方法的流程
- 15.1 Android的性能優化方法
- 15.2 內存泄漏分析工具MAT
- 15.3 提升程序的可維護性
1 Activity的生命週期和啓動模式
1.1 Activity的生命週期全面分析
用戶正常使用狀況下的生命週期 & 因爲Activity被系統回收或者設備配置改變致使Activity被銷燬重建狀況下的生命週期。html
1.1.1 典型狀況下的生命週期分析
Activity的生命週期和啓動模式
1. Activity第一次啓動:onCreate->onStart->onResume。
2. Activity切換到後臺( 用戶打開新的Activity或者切換到桌面) ,onPause->onStop(若是新Activity採用了透明主題,則當前Activity不會回調onstop)。
3. Activity從後臺到前臺,從新可見,onRestart->onStart->onResume。
4. 用戶退出Activity,onPause->onStop->onDestroy。
5. onStart開始到onStop以前,Activity可見。onResume到onPause以前,Activity能夠接受用戶交互。
6. 在新Activity啓動以前,棧頂的Activity須要先onPause後,新Activity才能啓動。因此不能在onPause執行耗時操做。
7. onstop中也不能夠太耗時,資源回收和釋放能夠放在onDestroy中。java
1.1.2 異常狀況下的生命週期分析
1 系統配置變化致使Activity銷燬重建android
例如Activity處於豎屏狀態,若是忽然旋轉屏幕,因爲系統配置發生了改變,Activity就會被銷
毀並從新建立。
在異常狀況下系統會在onStop以前調用onSaveInstanceState來保存狀態。Activity從新建立後,會在onStart以後調用onRestoreInstanceState來恢復以前保存的數據。
保存數據的流程: Activity被意外終止,調用onSaveIntanceState保存數據-> Activity委託Window,Window委託它上面的頂級容器一個ViewGroup( 多是DecorView) 。而後頂層容器在通知全部子元素來保存數據。nginx
這是一種委託思想,Android中相似的還有:View繪製過程、事件分發等。c++
系統只在Activity異常終止的時候纔會調用 onSaveInstanceState 和onRestoreInstanceState 方法。其餘狀況不會觸發。git
2 資源內存不足致使低優先級的Activity被回收
三種Activity優先級:前臺- 可見非前臺 -後臺,從高到低。
若是一個進程沒有四大組件,那麼將很快被系統殺死。所以,後臺工做最好放入service中。github
android:configChanges=「orientation」 在manifest中指定 configChanges 在系統配置變化後不從新建立Activity,也不會執行 onSaveInstanceState 和onRestoreInstanceState 方法,而是調用 onConfigurationChnaged 方法。
附:系統配置變化項目
configChanges 通常經常使用三個選項:
1. locale 系統語言變化
2. keyborardHidden 鍵盤的可訪問性發生了變化,好比用戶調出了鍵盤
3. orientation 屏幕方向變化
1.2 Activity的啓動模式
1.2.1 Activity的LaunchMode
Android使用棧來管理Activity。
1. standard
每次啓動都會從新建立一個實例,無論這個Activity在棧中是否已經存在。誰啓動了這個Activity,那麼Activity就運行在啓動它的那個Activity所在的棧中。
用Application去啓動Activity時會報錯,緣由是非Activity的Context沒有任務棧。解決辦法是爲待啓動Activity制定FLAGACTIVITYNEW_TASH標誌位,這樣就會爲它建立一個新的任務棧。
2. singleTop
若是新Activity位於任務棧的棧頂,那麼此Activity不會被從新建立,同時回調 onNewIntent 方法。onCreate和onStart方法不會被執行。
3. singleTask
這是一種單實例模式。若是不存在activity所須要的任務棧,則建立一個新任務棧和新Activity實例;若是存在所須要的任務棧,不存在實例,則新建立一個Activity實例;若是存在所須要的任務棧和實例,則不建立,調用onNewIntent方法。同時使該Activity實例之上的全部Activity出棧。
參考:taskAffinity標識Activity所須要的任務棧
4. singleIntance
單實例模式。具備singleTask模式的全部特性,同時具備此模式的Activity只能獨自位於一個任務棧中。
假設兩個任務棧,前臺任務棧爲12,後臺任務棧爲XY。Y的啓動模式是singleTask。如今請求Y,整個後臺任務棧會被切換到前臺。如圖所示:
設置啓動模式
1. manifest中 設置下的 android:launchMode 屬性。
2. 啓動Activity的 intent.addFlags(Intent.FLAGACTIVITYNEW_TASK); 。
3. 兩種同時存在時,以第二種爲準。第一種方式沒法直接爲Activity添加FLAGACTIVITYCLEAR_TOP標識,第二種方式沒法指定singleInstance模式。
4. 能夠經過命令行 adb shell dumpsys activity 命令查看棧中的Activity信息。
1.2.2 Activity的Flags
這些FLAG能夠設定啓動模式、能夠影響Activity的運行狀態。
-
FLAGACTIVITYNEW_TASK
爲Activity指定「singleTask」啓動模式。
-
FLAGACTIVITYSINGLE_TOP
爲Activity指定「singleTop"啓動模式。
-
FLAGACTIVITYCLEAR_TOP
具備此標記位的Activity啓動時,同一個任務棧中位於它上面的Activity都要出棧,通常和FLAGACTIVITYNEW_TASK配合使用。
-
FLAGACTIVITYEXCLUDEFROMRECENTS
若是設置,新的Activity不會在最近啓動的Activity的列表(就是安卓手機裏顯示最近打開的Activity那個系統級的UI)中保存。等同於在xml中指定android:exludeFromRecents="true"屬性。
1.3 IntentFilter的匹配規則
Activity調用方式
1. 顯示調用 明確指定被啓動對象的組件信息,包括包名和類名
2. 隱式調用 不須要明確指定組件信息,須要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 queryIntentActivityies(Intent intent,int flags);
public abstract ResolveInfo resloveActivity(Intent intent,int flags);以上的第二個參數使用 MATCHDEFAULTONLY ,這個標誌位的含義是僅僅匹配那些在
intent-filter中聲明瞭 android.intent.category.DEFAULT 這個category的Activity。由於若是把不含這個category的Activity匹配出來了,因爲不含DEFAULT這個category的Activity是沒法接受隱式Intent的從而致使startActivity失敗。
-
下面的action和category用來代表這是一個入口Activity,而且會出如今系統的應用列表中,兩者缺一不可。
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 包名 查看當前所存在的進程信息。
兩種進程命名方式的區別
1. 「:remote」
「:」的含義是指在當前的進程名前面附加上當前的包名,完整的進程名爲「com.example.c2.remote"。這種進程屬於當前應用的私有進程,其餘應用的組件不能夠和它跑在同一個進程中。
2. 「com.example.c2.remote」
這是一種完整的命名方式。這種進程屬於全局進程,其餘應用能夠經過ShareUID方式和它跑在同一個進程中。
2.2.2 多線程模式的運行機制
Android爲每一個進程都分配了一個獨立的虛擬機,不一樣虛擬機在內存分配上有不一樣的地址空間,致使不一樣的虛擬機訪問同一個類的對象會產生多份副本。例如不一樣進程的Activity對靜態變量的修改,對其餘進程不會形成任何影響。全部運行在不一樣進程的四大組件,只要它們之間須要經過內存在共享數據,都會共享失敗。四大組件之間不可能不經過中間層來共享數據。
多進程會帶來如下問題:
1. 靜態成員和單例模式徹底失效。
2. 線程同步鎖機制徹底失效。
這兩點都是由於不一樣進程不在同一個內存空間下,鎖的對象也不是同一個對象。
3. SharedPreferences的可靠性降低。
SharedPreferences底層是 經過讀/寫XML文件實現的,併發讀/寫會致使必定概率的數據丟失。
4. Application會屢次建立。
因爲系統建立新的進程的同時分配獨立虛擬機,其實這就是啓動一個應用的過程。在多進程模式中,不一樣進程的組件擁有獨立的虛擬機、Application以及內存空間。
多進程至關於兩個不一樣的應用採用了SharedUID的模式
實現跨進程的方式有不少:
1. Intent傳遞數據。
2. 共享文件和SharedPreferences。
3. 基於Binder的Messenger和AIDL。
4. 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文件
1. Book.java
表示圖書信息的實體類,實現了Parcelable接口。
2. Book.aidl
Book類在AIDL中的聲明。
3. IBookManager.aidl
定義的管理Book實體的一個接口,包含 getBookList 和 addBook 兩個方法。儘管Book類和IBookManager位於相同的包中,可是在IBookManager仍然要導入Book類。
4. 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類的內部方法和定義
1. DESCRIPTOR
Binder的惟一標識,通常用Binder的類名錶示。
2. asInterface(android.os.IBinder obj)
將服務端的Binder對象轉換爲客戶端所需的AIDL接口類型的對象,若是C/S位於同一進
程,此方法返回就是服務端的Stub對象自己,不然返回的就是系統封裝後的Stub.proxy對
象。
3. asBinder
返回當前Binder對象。
4. onTransact
這個方法運行在服務端的Binder線程池中,由客戶端發起跨進程請求時,遠程請求會經過
系統底層封裝後交由此方法來處理。該方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)
1. 服務端經過code肯定客戶端請求的目標方法是什麼,
2. 接着從data取出目標方法所需的參數,而後執行目標方法。
3. 執行完畢後向reply寫入返回值( 若是有返回值) 。
4. 若是這個方法返回值爲false,那麼服務端的請求會失敗,利用這個特性咱們能夠來作權限驗證。
5. Proxy#getBookList 和Proxy#addBook
這兩個方法運行在客戶端,內部實現過程以下:
1. 首先建立該方法所須要的輸入型對象Parcel對象data,輸出型Parcel對象reply和返回值對象List。
2. 而後把該方法的參數信息寫入_data( 若是有參數)
3. 接着調用transact方法發起RPC( 遠程過程調用) ,同時當前線程掛起
4. 而後服務端的onTransact方法會被調用知道RPC過程返回後,當前線程繼續執行,並從reply中取出RPC過程的返回結果,最後返回reply中的數據。
AIDL文件不是必須的,之因此提供AIDL文件,是爲了方便系統爲咱們生成IBookManager.java,但咱們徹底能夠本身寫一個。
linkToDeath和unlinkToDeath
若是服務端進程異常終止,咱們到服務端的Binder鏈接斷裂。可是,若是咱們不知道Binder鏈接已經斷裂,那麼客戶端功能會受影響。經過linkTODeath咱們能夠給Binder設置一個死亡代理,當Binder死亡時,咱們就會收到通知。
1. 聲明一個 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
}
}
2. 在客戶端綁定遠程服務成功後,給binder設置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
3. 另外,能夠經過Binder的 isBinderAlive 判斷Binder是否死亡。
2.4 Android中的IPC方式
主要有如下方式:
1. Intent中附加extras
2. 共享文件
3. Binder
4. ContentProvider
5. Socket
2.4.1 使用Bundle
四大組件中的三大組件( Activity、Service、Receiver) 都支持在Intent中傳遞 Bundle 數據。
Bundle實現了Parcelable接口,所以能夠方便的在不一樣進程間傳輸。當咱們在一個進程中啓動了另外一個進程的Activity、Service、Receiver,能夠再Bundle中附加咱們須要傳輸給遠程進程的消息並經過Intent發送出去。被傳輸的數據必須可以被序列化。
2.4.2 使用文件共享
咱們能夠序列化一個對象到文件系統中的同時從另外一個進程中恢復這個對象。
1. 經過 ObjectOutputStream / ObjectInputStream 序列化一個對象到文件中,或者在另外一個進程從文件中反序列這個對象。注意:反序列化獲得的對象只是內容上和序列化以前的對象同樣,本質是兩個對象。
2. 文件併發讀寫會致使讀出的對象可能不是最新的,併發寫的話那就更嚴重了 。因此文件共享方式適合對數據同步要求不高的進程之間進行通訊,而且要妥善處理併發讀寫問題。
3. SharedPreferences 底層實現採用XML文件來存儲鍵值對。系統對它的讀/寫有必定的緩存策略,即在內存中會有一份 SharedPreferences 文件的緩存,所以在多進程模式下,系統對它的讀/寫變得不可靠,面對高併發讀/寫時 SharedPreferences 有很大概率丟失數據,所以不建議在IPC中使用 SharedPreferences 。
2.4.3 使用Messenger
Messenger能夠在不一樣進程間傳遞Message對象。是一種輕量級的IPC方案,底層實現是AIDL。它對AIDL進行了封裝,使得咱們能夠更簡便的進行IPC。
具體使用時,分爲服務端和客戶端:
1. 服務端:建立一個Service來處理客戶端請求,同時建立一個Handler並經過它來建立一個
Messenger,而後再Service的onBind中返回Messenger對象底層的Binder便可。
private final Messenger mMessenger = new Messenger (new xxxHandler());
2. 客戶端:綁定服務端的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。
流程以下:
1. 服務端須要建立Service來監聽客戶端請求,而後建立一個AIDL文件,將暴露給客戶端的接口在AIDL文件中聲明,最後在Service中實現這個AIDL接口便可。
2. 客戶端首先綁定服務端的Service,綁定成功後,將服務端返回的Binder對象轉成AIDL接口所屬的類型,接着就能夠調用AIDL中的方法了。
AIDL支持的數據類型:
1. 基本數據類型、String、CharSequence
2. List:只支持ArrayList,裏面的每一個元素必須被AIDL支持
3. Map:只支持HashMap,裏面的每一個元素必須被AIDL支持
4. Parcelable
5. 全部的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線程池中,可能很耗時,不能在主線程中去調用服務端的方法。==
權限驗證
默認狀況下,咱們的遠程服務任何人均可以鏈接,咱們必須加入權限驗證功能,權限驗證失敗則沒法調用服務中的方法。一般有兩種驗證方法:
1. 在onBind中驗證,驗證不經過返回null
驗證方式好比permission驗證,在AndroidManifest聲明:
<permission android:name="com.rgy.chapter2.permisson.ACCESSBOOK_SERVICE" android:protectionLevel="normal"/>
public IBinder onBind(Intent intent){ int check = checkCallingOrSelefPermission("com.ryq.chapter2.permission.ACCESSBOOK_SERVICE"); if(check == PackageManager.PERMISSION_DENIED){ return null; } return mBinder; }
這種方法也適用於Messager。
2. 在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和UriCode。根據UriCode,咱們就知道要訪問哪一個表了。
==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在佈局中的位置。滑動偏移量mScrollX和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 MESSAGESCROLLTO = 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 MESSAGESCROLLTO: mCount ++ ; if (mCount <= FRAME_COUNT){ float fraction = mCount / (float) FRAME_COUNT; int scrollX = (int) (fraction * 100); mButton1.scrollTo(scrollX,0); mHandelr.sendEmptyMessageDelayed(MESSAGESCROLLTO , 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==
關於事件傳遞的機制,這裏給出一些結論:
1. 一個事件系列以down事件開始,中間包含數量不定的move事件,最終以up事件結束。
2. 正常狀況下,一個事件序列只能由一個View攔截並消耗。
3. 某個View攔截了事件後,該事件序列只能由它去處理,而且它的onInterceptTouchEvent
不會再被調用。
4. 某個View一旦開始處理事件,若是它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那麼同一事件序列中的其餘事件都不會交給他處理,而且事件將從新交由他的父元素去處理,即父元素的onTouchEvent被調用。
5. 若是View不消耗ACTION_DOWN之外的其餘事件,那麼這個事件將會消失,此時父元素的onTouchEvent並不會被調用,而且當前View能夠持續收到後續的事件,最終消失的點擊事件會傳遞給Activity去處理。
6. ViewGroup默認不攔截任何事件。
7. View沒有onInterceptTouchEvent方法,一旦事件傳遞給它,它的onTouchEvent方法會被調用。
8. View的onTouchEvent默認消耗事件,除非他是不可點擊的( clickable和longClickable同時爲false) 。View的longClickable屬性默認false,clickable默認屬性分狀況(如TextView爲false,button爲true)。
9. View的enable屬性不影響onTouchEvent的默認返回值。
10. onClick會發生的前提是當前View是可點擊的,而且收到了down和up事件。
11. 事件傳遞過程老是由外向內的,即事件老是先傳遞給父元素,而後由父元素分發給子View,經過requestDisallowInterceptTouchEvent方法能夠在子元素中干預父元素的分發過程,可是ACTION_DOWN事件除外。
3.4.2 事件分發的源碼解析
略
3.5 滑動衝突
在界面中,只要內外兩層同時能夠滑動,這個時候就會產生滑動衝突。滑動衝突的解決有固定的方法。
3.5.1 常見的滑動衝突場景
1. 外部滑動和內部滑動方向不一致;
好比viewpager和listview嵌套,但這種狀況下viewpager自身已經對滑動衝突進行了處理。
2. 外部滑動方向和內部滑動方向一致;
3. 上面兩種狀況的嵌套。
只要解決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:根據須要決定是否攔截
-
ACTIONUP:必須返回false。若是攔截,那麼子View沒法接受up事件,沒法完成click操做。而若是是父容器須要該事件,那麼在ACTIONMOVE時已經進行了攔截,根據上一節的結論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開始的
1. measure用來測量View的寬高
2. layout來肯定View在父容器中的位置
3. 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 分爲標題欄和內容欄兩個部分。
!
<div align="center"> <img src="http://images2015.cnblogs.com/blog/500720/201609/500720-20160925174505236-1295369287.png" width = "150" height = "200" alt="圖片" align=center /> </div>
在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 和具體數值這兩種模式
-
ATMOST :對應View的默認大小,不一樣View實現不一樣,View的大小不能大於父容器的SpecSize,對應 LayoutParams 中的 wrapcontent
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的寬/高是 wrapcontent 時,View的MeasureSpec都是ATMOST模式而且其大小不能超過父容器的剩餘空間。
- 父容器的UNSPECIFIED模式,通常用於系統內部屢次Measure時,表示一種測量的狀態,通常來講咱們不須要關注此模式。
4.3 View的工做流程
4.3.1 measure過程
View的measure過程
直接繼承View的自定義控件須要重寫 onMeasure 方法並設置 wrapcontent ( 即specMode是 ATMOST 模式) 時的自身大小,不然在佈局中使用 wrapcontent 至關於使用 matchparent 。對於非 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方法的流程:
1. 取出子View的 LayoutParams
2. 經過 getChildMeasureSpec 方法來建立子元素的 MeasureSpec
3. 將 MeasureSpec 直接傳遞給View的measure方法來進行測量
經過LinearLayout的onMeasure方法裏來分析ViewGroup的measure過程:
1. LinearLayout在佈局中若是使用match_parent或者具體數值,測量過程就和View一致,即高度爲specSize
2. LinearLayout在佈局中若是使用wrap_content,那麼它的高度就是全部子元素所佔用的高度總和,但不超過它的父容器的剩餘空間
3. LinearLayout的的最終高度同時也把豎直方向的padding考慮在內
View的measure過程是三大流程中最複雜的一個,measure完成之後,經過 getMeasuredWidth/Height 方法就能夠正確獲取到View的測量後寬/高。在某些狀況下,系統可能須要屢次measure才能肯定最終的測量寬/高,因此在onMeasure中拿到的寬/高極可能不是準確的。
==若是咱們想要在Activity啓動的時候就獲取一個View的寬高,怎麼操做呢?==由於View的measure過程和Activity的生命週期並非同步執行,沒法保證在Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。因此有如下四種方式來獲取View的寬高:
1. Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經準備好了,須要注意:它會被調用屢次,當Activity的窗口獲得焦點和失去焦點均會被調用。
2. view.post(runnable)
經過post將一個runnable投遞到消息隊列的尾部,當Looper調用此runnable的時候,View也初始化好了。
3. ViewTreeObserver
使用 ViewTreeObserver 的衆多回調能夠完成這個功能,好比OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的可見性發生改變時,onGlobalLayout 方法會被回調,這是獲取View寬高的好時機。須要注意的是,伴隨着View樹狀態的改變, onGlobalLayout 會被回調屢次。
4. view.measure(int widthMeasureSpec,int heightMeasureSpec)
手動對view進行measure。須要根據View的layoutParams分狀況處理:
- match_parent:
沒法measure出具體的寬高,由於不知道父容器的剩餘空間,沒法測量出View的大小-
具體的數值( dp/px):
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 方法肯定自己的位置,源碼流程以下:
1. setFrame 肯定View的四個頂點位置,即肯定了View在父容器中的位置
2. 調用 onLayout 方法,肯定全部子View的位置,和onMeasure同樣,onLayout的具體實現和佈局有關,所以View和ViewGroup均沒有真正實現 onLayout 方法。
以LinearLayout的 onLayout 方法爲例:
1. 遍歷全部子View並調用 setChildFrame 方法來爲子元素指定對應的位置
2. setChildFrame 方法實際上調用了子View的 layout 方法,造成了遞歸
==View的測量寬高和最終寬高的區別:==
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高造成於measure過程,最終寬高造成於layout過程。但重寫view的layout方法能夠使他們不相等。
4.3.3 draw過程
View的繪製過程遵循以下幾步:
1. 繪製背景 drawBackground(canvas)
2. 繪製本身 onDraw
3. 繪製children dispatchDraw 遍歷全部子View的 draw 方法
4. 繪製裝飾 onDrawScrollBars
ViewGroup會默認啓用 setWillNotDraw 爲ture,致使系統不會去執行 onDraw ,因此自定義ViewGroup須要經過onDraw來繪製內容時,必須顯式的關閉 WILLNOTDRAW 這個優化標記位,即調用 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中對wrapcontent作特殊處理。指定wrapcontent模式下的默認寬/高。
- 直接繼承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
自定義屬性設置方法:
1. 在values目錄下建立自定義屬性的XML,如attrs.xml。
- 在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.CircleViewcirclecolor, Color.RED);
a.recycle();
init();
}
- 在佈局文件中使用自定義屬性
width="matchparent" android:layoutheight="matchparent" android:background="#ffffff" android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id=「@+id/circleView1」
android:layoutwidth=「wrapcontent」
android:layout_height=「100dp」
android:layout_margin=「20dp」
android:background=「#000000」
android:padding=「20dp」
app:circlecolor=「@color/lightgreen」 />
Android中的命名空間
- 繼承ViewGroup派生特殊的layout:HorizontalScrollViewEx
onMeasure方法中,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高作相應處理。而後判斷寬高是否是wrapcontent,若是寬是,那麼HorizontalScrollViewEx的寬就是全部全部子元素的寬度之和。若是高是wrapcontent,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin。
onLayout方法中,在放置子元素時候也要考慮padding和margin。
4.4.4 自定義View的思想
-
掌握基本功,好比View的彈性滑動、滑動衝突、繪製原理等
-
面對新的自定義View時,對其分類並選擇合適的實現思路。
5 理解RemoteViews
什麼是遠程view呢?它和遠程service同樣,RemoteViews能夠在其餘進程中顯示。咱們能夠跨進程更新它的界面。在Android中,主要有兩種場景:通知欄和桌面小部件。
本章先簡單介紹通知欄和桌面小部件應用,接着分析RemoteViews內部機制,最後分析RemoteViews的意義並給出一個實例。
5.1 RemoteViews的應用
通知欄主要是經過NotificationManager的notify方法實現。桌面小部件是經過APPWidgetProvider來實現。APPWidgetProvider本質是一個廣播。RemoteViews運行在系統的SystemServer進程。
5.1.1 RemoteViews在通知欄的應用
咱們用到自定義通知,首先要提供一個佈局文件,而後經過RemoteViews來加載,能夠自定義通知的樣式。更新view時,經過RemoteViews提供的一系列方法。若是給一個控件加點擊事件,要使用PendingIntent。
5.1.2 RemoteViews在桌面小部件的應用
AppWidgetProvider是實現桌面小部件的類,本質是一個BroadcastReceiver。開發步驟以下:
1. 定義小部件界面 代碼
2. 定義小部件配置信息 代碼
3. 定義小部件實現類,繼承AppWidgetProvider 代碼
上面的例子實現了一個簡單地桌面小部件,在小部件上顯示一張圖片,點擊後會旋轉一週。
4. 在AndroidManifest.mxl中聲明小部件
receiver android:name=「.MyAppWidgetProvider」 >
<intent-filter> <action android:name="com.ryg.chapter_5.action.CLICK" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> </receiver>
第一個action用於識別小部件的單擊,第二個action做爲小部件的標識必須存在。
AppWidgetProvider除了onUpdate方法,還有一系列方法。這些方法會自動被onReceive方法調用。當廣播到來之後,AppWidgetProvider會自動根據廣播的action經過onReceive方法分發廣播。
-
onEnable:該小部件第一次添加到桌面時調用,添加屢次只在第一次調用
-
onUpdate:小部件被添加或者每次小部件更新時調用,更新時機由updatePeriodMillis指定,每一個週期小部件都會自動更新一次。
-
onDeleted:每刪除一次桌面小部件都會調用一次
-
onDisabled:最後一個該類型的桌面小部件被刪除時調用
-
onReceive:內置方法,用於分發具體事件給以上方法
5.1.3 PendingIntent概述
PendingIntent表示一種處於待定的狀態的intent。典型場景是RemoteViews添加單擊事件。經過send和cancel方法來發送和取消待定intent。
PendingIntent支持三種待定意圖:
static PendingIntent | getActivity(Context context, int requestCode, Intent intent, int flags)得到一個PendingIntent,效果至關於Context.startActivity(Intent) |
static PendingIntent | getService(Context context, int requestCode, Intent intent, int flags)得到一個PendingIntent,效果至關於Context.startService(Intent) |
static PendingIntent | getBroadcast(Context context, int requestCode, Intent intent, int flags)得到一個PendingIntent,效果至關於Context.sendBroadcast(Intent) |
其中requestCode多數狀況下設爲0便可,requestCode會影響flags的效果。
PendingIntent的匹配規則:
若是兩個PendingIntent,它們內部的Intent相同且requestCode也相同,那這兩個PendingIntent就是相同的。
Intent的匹配規則:
若是兩個intent的ComponentName和intent-filter相同,那麼這兩個intent相同。Extras不參與匹配過程。
flags參數的含義
-
FLAGONESHOT
當前的PendingIntent只能被使用一次,而後就會被自動cancel,若是後續還有相同的PendingIntent,它們的send方法會調用失敗。對於通知欄來講,同類的通知只能使用一次,後續的通知將沒法打開。
-
FLAGNOCREATE
當前的PendingIntent不會主動建立,若是當前PendingIntent以前不存在(匹配的PendingIntent),那麼獲取PendingIntent失敗。這個flag不多使用。
-
FLAGCANCELCURRENT
當前的PendingIntent若是存在(匹配的PendingIntent),那麼它們都會被cancel,而後系統建立一個新的PendingIntent。對於通知欄來講,那些被cancel的消息單擊後將沒法打開。
-
FLAGUPDATECURRENT
當前PendingIntent若是已經存在(匹配的PendingIntent),那麼它們都會被更新。即intent中的extras會被替換成最新的。
舉例:
在manager.notify(id,notification)
中,若是id是常量,那麼屢次調用notify只能彈出一個通知,後續的通知會把前面的通知徹底替代。而若是每次id都不一樣,那麼會彈出多個通知。
若是id每次都不一樣且PendingIntent不匹配,那麼flags不會對通知之間形成干擾。
若是id不一樣且PendingIntent匹配:
1. 若是採用了FLAGONESHOT標記位,那麼後續通知中的PendingIntent會和第一條通知徹底一致,包括extras,單擊任何一條通知後,剩下的通知均沒法再打開,當全部的通知被清除後,會再次重複這一過程。
2. 若是採用FLAGCANCELCURRENT,那麼只有最新的通知能夠打開。
3. 若是採用FLAGUPDATECURRENT,那麼以前彈出的通知中的PendingIntent會被更新,與最新一條的通知徹底一致,包括extras,而且這些通知均可以打開。
5.2 RemoteViews的內部機制
構造方法
public RemoteViews(String packageName,int layoutId)
第一個參數是當前應用的包名,第二個參數是待加載的佈局文件。
RemoteViews並不支持全部的view類型,支持類型以下:
-
Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout
-
View:AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper,ListView,GridView、StackView、AdapterViewFlipper、ViewStub。
-
RemoteViews不支持以上view的子類
訪問RemoteViews的view元素,必須經過一系列set方法完成:
|方法名|做用|
|–|–|
|setTextViewText(int viewId,CharSequence text)|設置TextView的文本內容 第一個參數是TextView的id 第二個參數是設置的內容|
|setTextViewTextSize(int viewId,int units,float size) |設置TextView的字體大小 第二個參數是字體的單位
|setTextColor(int viewId,int color) |設置TextView字體顏色
|setImageViewResource(int viewId,int srcId) |設置ImageView的圖片
|setInt(int viewId,String methodName,int value) |反射調用View對象的參數類型爲Int的方法 好比上述的setImageViewResource的方法內部就是這個方法實現 由於srcId爲int型參數
|setLong setBoolean |相似於setInt
|setOnClickPendingIntent(int viewId,PendingIntent pendingIntent)| 添加點擊事件的方法
大部分set方法是經過反射來完成的。
RemoteViews內部機制
通知欄和小組件分別由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM經過Binder分別和SystemService進程中的NotificationManagerService以及AppWidgetService中加載的,而它們運行在系統的SystemService中,這就和咱們進程構成了跨進程通信。
首先RemoteViews會經過Binder傳遞到SystemService進程,由於RemoteViews實現了Parcelable接口,所以它能夠跨進程傳輸,系統會根據RemoteViews的包名等信息拿到該應用的資源;而後經過LayoutInflater去加載RemoteViews中的佈局文件。接着系統會對View進行一系列界面更新任務,這些任務就是以前咱們經過set來提交的。set方法對View的更新並不會當即執行,會記錄下來,等到RemoteViews被加載之後纔會執行。
爲了提升效率,系統沒有直接經過Binder去支持全部的View和View操做。而是提供一個Action概念,Action一樣實現Parcelable接口。系統首先將View操做封裝到Action對象並將這些對象跨進程傳輸到SystemService進程,接着SystemService進程執行Action對象的具體操做。遠程進程經過RemoteViews的apply方法來進行View的更新操做,RemoteViews的apply方法會去遍歷全部的Action對象並調用他們的apply方法。這樣避免了定義大量的Binder接口,也避免了大量IPC操做。
apply和reApply的區別在於:apply會加載佈局並更新界面,而reApply則只會更新界面。RemoteViews在初始化界面時會調用apply方法,後續更新界面調用reApply方法。
關於單擊事件,RemoteViews中只支持發起PendingIntent,不支持onClickListener那種模式。setOnClickPendingIntent用於給普通的View設置單擊事件,不能給集合(ListView/StackView)中的View設置單擊事件(開銷大,系統禁止了這種方式)。若是要給ListView/StackView中的item設置單擊事件,必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用才能夠。
5.3 RemoteViews的意義
當一個應用須要更新另外一個應用的某個界面,咱們能夠選擇用AIDL來實現,但若是更新比較頻繁,效率會有問題,同時AIDL接口就可能變得很複雜。若是採用RemoteViews就沒有這個問題,但RemoteViews僅支持一些經常使用的View,若是界面的View都是RemoteViews所支持的,那麼就能夠考慮採用RemoteViews。
demo A 、B
6 Android的Drawable
Drawable表示的是一種能夠在Canvas上進行繪製的抽象概念,它的種類有不少,最多見的就是顏色和圖片。優勢:使用簡單,比自定義View成本低不少,非圖片類型的Drawable佔用空間較小。本章中,首先描述Drawable的層次關係,接着介紹Drawable的分類,最後介紹自定義Drawable相關的知識。
6.1 Drawable簡介
Drawable有不少種,都表示圖像的概念,但不全是圖片,經過顏色也能夠構造出各式各樣的圖像效果。實際開發中,Drawable常被用來做爲View的背景使用。Drawable通常是經過XML來定義的,Drawable是全部Drawable對象的基類。
Drawable的內部寬、高這個參數比較重要,經過getIntrinsicWidth/getIntrinsicHeight這兩個方法獲取。但並非全部Drawable都有寬高;圖片Drawable的內部寬/高就是圖片的寬/高,可是顏色造成的Drawable並無寬/高的概念。
6.2 Drawable的分類
常見的有BitmapDrawable、ShapeDrawable、LayerDrawable以及StateListDrawable等。
6.2.1 BitmapDrawable
表示的就是一張圖片,能夠直接引用原始圖片便可,也能夠經過XML描述它,從而設置更多效果。
<?xml version="1.0" encoding="utf-8"?>
<bitmap / nine-patch
xmlns:android="http://schemas.android.com/apk/res/android" android:src="@[package:]drawable/drawable_resource" android:antialias=["true" | "false"] android:dither=["true" | "false"] android:filter=["true" | "false"] android:gravity=["top" | "bottom" | "left" | "right" | "center_vertical" | "fillvertical" | "centerhorizontal" | "fill_horizontal" | "center" | "fill" | "clipvertical" | "cliphorizontal"] android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"] />
屬性分析
-
android:src
圖片資源id -
android:antialias
是否開啓圖片抗鋸齒功能。開啓後會讓圖片變得平滑,同時也會必定程度上下降圖片的清晰度,建議開啓; -
android:dither
是否開啓抖動效果。當圖片的像素配置和手機屏幕像素配置不一致時,開啓這個選項可讓高質量的圖片在低質量的屏幕上還能保持較好的顯示效果,建議開啓。 -
android:filter
是否開啓過濾效果。當圖片尺寸被拉伸或壓縮時,開啓過濾效果能夠保持較好的顯示效果,建議開啓; -
android:gravity
當圖片小於容器的尺寸時,設置此選項能夠對圖片進行定位。 -
android:tileMode
平鋪模式,有四種選項[「disabled」 | 「clamp」 | 「repeat」 | 「mirror」]。當開啓平鋪模式後,gravity屬性會被忽略。repeat是指水平和豎直方向上的平鋪效果;mirror是指在水平和豎直方向上的鏡面投影效果;clamp是指圖片四周的像素會擴展到周圍區域,這個比較特別。
NinePatchDrawable
表示一張.9格式的圖片,它和BitmapDrawable都表示一張圖片。用XML描述的方式也和BitmapDrawable同樣。在bitmap標籤中也能夠使用.9圖。
6.2.2 ShapeDrawable
能夠理解爲經過顏色來構造的圖形,能夠是純色或漸變的圖形。
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:bottomLeftRadius="10dp" android:bottomRightRadius="10dp" android:radius="5dp" android:topLeftRadius="10dp" android:topRightRadius="10dp" /> <gradient android:angle="0" android:centerColor="#cccccc" android:centerX="100" android:centerY="20" android:endColor="#abcdef" android:gradientRadius="100dp" android:startColor="#000000" android:type="linear" android:useLevel="false" /> <solid android:color="#cccccc" /> <stroke android:width="1dp" android:color="#cccccc" android:dashGap="2dp" android:dashWidth="50dp" />
屬性分析
- android:shape
表示圖片的形狀,選項:rectangle(矩形)、oval(橢圓)、line(橫線)、ring(圓環)。默認值是矩形,另外line和ring這兩個選項必須經過標籤來指定寬度和顏色,不然看不到效果。
其中,ring有其特殊的5種屬性
|屬性|含義|
|–|–|
|android:innerRadius |圓環的內半徑,和innerRadiusRatio同時存在,以innerRadius爲準
|android:thickness |圓環的厚度,外半徑減去內半徑的大小,和android:thinknessRatio同時存在,以thickness爲準
|android:innerRadiusRatio |內半徑佔整個Drawable的寬度比例,默認值爲9,若是爲n,內半徑=寬度/n
|android:thicknessRatio |厚度佔整個Drawable寬度的比例,默認值爲3,若是爲n,厚度=寬度/n
|android:useLevel |通常都用false
-
<corners>
表示shape的四個角的角度(圓角程度)。只適用於矩形shape。其中android:radius是同時爲4個角設置相同的角度,優先級較低,會被topLeftRadius這種具體指定角度的屬性所覆蓋。 -
<gradient>
與<solid>
標籤相互排斥的,其中solid表示純色填充,而gradient表示漸變效果;gradient有以下幾個屬性:- android:angle——漸變的角度,默認爲0,其值必須是45的倍數,0表示從左往右,90表示從下到上。
- android:centerX 漸變的中心點的橫座標
- android:centerY 漸變的中心點的縱座標;
- android:startColor 漸變的起始色
- android:centerColor 漸變的中間色
- android:endColor 漸變的結束色
- android:gradientRadius 漸變半徑,僅當android:type="radial"時有效。
- android:type 漸變的類型,有linear(線性漸變)、radial(鏡像漸變)、swepp(掃描線漸變)三種,默認是線性漸變。
-
<solid>
表示純色填充,經過android:color便可指定shape中填充的顏色。 -
<stroke>
Shape的描邊,有以下屬性:- android:width 描邊的寬度
- android:color 描邊的顏色
- android:dashWidth 組成虛線的線段的寬度
- android:dashGap 組成虛線之間的間距。dashWidth和dashGap有任何一個爲0,虛線效果都不能生效。
-
<padding>
表示空白,但不是shape的空白,而是包含它的View的空白。 -
<size>
shape的大小,有兩個屬性:android:width和android:height,分別表示shape的寬高。經過標籤指定寬高後,ShapeDrawable就有固定寬/高了。可是做爲view的背景來講,shape仍是會被拉伸或者縮小爲view的大小。
更多參考:Android樣式的開發:shape篇
6.2.3 LayerDrawable
它表示一種層次化的Drawable集合,經過將不一樣的Drawable放置在不一樣層後達到一種疊加效果。
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/res_haimei1" android:bottom="10dp" android:drawable="@mipmap/haimei1" android:left="10dp" android:right="10dp" android:top="10dp" /> <item android:id="@+id/res_icon" android:width="30dp" android:height="30dp" android:drawable="@mipmap/ic_launcher" android:gravity="center" />
6.2.4 StateListDrawable
對應<selector>
標籤。它表示Drawable集合,每一個Drawable對應View的一種狀態,這樣系統就會根據View的狀態來選擇合適的Drawable。主要用於設置可點擊View的背景。
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" android:constantSize="false" android:dither="true" android:variablePadding="false"> <item android:drawable="@mipmap/iclauncher" android:statepressed="true" /> <item android:drawable="@mipmap/haimei1" android:state_pressed="false" />
屬性分析
-
android:constantSize
StateListDrawable的固有大小是否隨着其狀態的變化而變化,由於不一樣的Drawable有不一樣的固有大小。true表示固有大小保持不變,這時它的固有大小是內部全部Drawable的固有大小的最大值。默認值爲false。 -
android:dither
是否開啓抖動效果,默認true -
android:variablePadding
StateListDrawable的padding是否隨着狀態變化而變化。true表示變化,false表示padding是內部全部Drawable的padding的最大值。默認爲false。
view的常見狀態
|狀態|含義|
|–|–|
|android:state_pressed |按下狀態,Button按下以後沒有鬆開
|android:state_focused |View獲取了焦點
|android:state_selected| 用戶選擇了View,如RadioButton
|android:state_checked |用戶選中了View,適用於CheckBox
|android:state_enable |View處於可用狀態
默認的item通常放在最後而且不添加任何狀態,這樣當系統在以前的item沒法選擇的時候,就會匹配默認的item,由於item的默認狀態不附帶任何狀態,因此它能夠適配任何狀態。
6.2.5 LevelListDrawable
對應<level-list>
標籤。一樣表示Drawable集合,集合中的每一個Drawable都會有一個等級的概念,根據等級不一樣來切換對於的Drawable。當它做爲View的背景時,能夠經過Drawable的setLevel方法來設置不一樣的等級從而切換具體的Drawable。level的值從0-10000,默認爲0。
<?xml version="1.0" encoding="utf-8"?> <level-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:maxLevel="0" android:drawable="@drawable/icplaymethodnormal" /> <item android:maxLevel="1" android:drawable="@drawable/icplaymethodrepeat_list" /> <item android:maxLevel="2" android:drawable="@drawable/icplaymethodrepeat_one" /> <item android:maxLevel="3" android:drawable="@drawable/icplaymethodrandom" /> </level-list>
6.2.6 TransitionDrawable
對應<transition>
標籤。用來實現兩個Drawable之間淡入淡出的效果。
<?xml version="1.0" encoding="utf-8"?> <transition xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@mipmap/haimei2" /> <item android:drawable="@mipmap/haimei3" /> </transition> TransitionDrawable drawable = (TransitionDrawable) imageView.getBackground(); drawable.startTransition(1000);
startTransition和reverseTransition方法實現淡入淡出的效果以及它的逆過程。
6.2.7 InsetDrawable
對應於<inset>
標籤。它能夠將其餘Drawable內嵌到本身當中,並能夠在四周留下必定的間距。當一個View但願本身的背景比本身的實際區域小的時候,能夠採用InsetDrawable來實現。經過LayerDrawable也能夠實現。
<?xml version="1.0" encoding="utf-8"?> <inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@mipmap/haimei1" android:insetBottom="10dp" android:insetLeft="10dp" android:insetRight="10dp" android:insetTop="10dp"> <shape android:shape="rectangle"> <solid android:color="#abcdef" /> </shape> </inset>
其中,inset中的shape距離view邊界爲10dp。
6.2.8 ScaleDrawable
ScaleDrawable對應於xml文件中的<scale>
標籤,能夠根據本身的level將指定的drawable縮放到必定比例。
<?xml version="1.0" encoding="utf-8"?> <scale xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@color/blue" android:level="1" android:scaleGravity="center" android:scaleHeight="20%" android:scaleWidth="20%" />
其中,android:scaleGravity屬性至關於gravity屬性。android:scaleHeight/scaleWidth 表示Drawable的縮放比例。
縮放公式: w -= (int) (w * (10000 - level) * mState.mScaleWidth / 10000)
可見,level越大,Drawable看起來越大;scaleHeight/scaleWidth越大,Drawable看起來越小。注意的是,level設置爲0時,Drawable不可見。level不該超過10000。
6.2.9 ClipDrawable
ClipDrawabe對應於<clip>
標籤,他能夠根據本身當前的等級(level)來裁剪一個Drawable,裁剪方向能夠經過Android:clipOrientation和android:gravity兩個屬性共同控制。
<?xml version="1.0" encoding="utf-8"?> <clip xmlns:android="http://schemas.android.com/apk/res/android" android:clipOrientation="vertical\horizontal" android:drawable="@drawable/bitmapdrawable" android:gravity="bottom|top|left|right|center|fill|centervertical|centerhorizontal|fillvertical|fillhorizontal|clipvertical|cliphorizontal" />
clipOrientation表示裁剪方向。gravity須要和clipOrientation一塊兒才能發揮做用。以下所示:
|選項| 含義|
|–|–|
|top |將內部的Drawable放在容器的頂部,不改變大小,若是爲豎直裁剪,就從底部開始裁剪
|bottom |將內部的Drawable放在容器的底部,不改變大小,若是爲豎直裁剪,就從頂部開始裁剪
|left |默認值。內部Drawable放在容器左邊,不改變大小,若是爲水平裁剪,就從右邊開始裁剪。
|right |內部Drawable放在容器右邊,不改變大小,若是爲水平裁剪,就從左邊開始裁剪
|center_vertical |Drawable在容器中豎直居中,不改變大小,豎直裁剪的時候上下同時開始裁剪
|fill_vertical |Drawable在豎直方向填充容器,若是爲豎直裁剪,僅當ClipDrawable的等級爲0(level=0,徹底不可見)時,纔會有裁剪行爲
|center_horizontal |Drawable水平居中,不改變大小,水平裁剪的時候從左右兩邊開始裁剪
|fill_horizontal |Drawable在水平方向填充,若是爲水平裁剪,僅當ClipDrawable等級=0的時候,纔能有裁剪行爲。
|center Drawable|在水平和豎直方向居中,不改變大小,水平裁剪的時候從左右開始裁剪,豎直裁剪的時候從上下開始裁剪。
|fill Drawable|在豎直和水平方向填充容器,僅當level=0的時候纔有裁剪行爲
|clip_vertical |附加選項,豎直方向的裁剪,少使用
|clip_horizontal |附加選項,水平方向的裁剪,少使用
使用步驟:
1. 定義ClipDrawable
2. 佈局文件引用
3. 代碼控制level
ImageView imageClip = (ImageView) findViewById(R.id.image_clip);
ClipDrawable drawable = (ClipDrawable) imageClip.getDrawable();
drawable.setLevel(5000);
level=0的時候,表示徹底裁剪,level=10000的時候表示徹底不裁剪,level=5000的時候表示裁剪了一半。即等級越大,裁剪的區域越小。
6.3 自定義Drawable
在第5章中,咱們分析了View的工做原理,系統會調用Drawable的draw方法繪製view的背景。因此咱們能夠經過重寫Drawable的draw方法來自定義Drawable。可是,一般咱們不必自定義Drawable,由於自定義Drawable沒法在XML中使用。只有在特殊狀況下能夠使用自定義Drawable。
圓形自定義Drawable demo,半徑隨着view的變化而變化
-
draw、setAlpha、setColorFilter和getOpacity這幾個方法都是必需要實現的,其中draw是最重要的。當自定義Drawable有固有大小的時候最好重寫getIntrinsicWidth和getIntrinsicHeight這兩個方法,由於會影響到view的wrap_content佈局。上面的例子中,沒有重寫,其內部大小爲-1。
-
內部大小不等於Drawable的實際區域大小,Drawable實際區域的大小能夠經過getBounds方法來獲得,通常來講它和View的尺寸相同。
7 Android動畫深刻分析
Android動畫分爲三種:
1. View動畫
2. 幀動畫
3. 屬性動畫
本章學習內容:
-
介紹View動畫和自定義View動畫
-
View動畫一些特殊的使用場景
-
對屬性動畫全面性的介紹
-
使用動畫的一些注意事項
7.1 View動畫
View動畫的做用對象是View,支持四種動畫效果:
1. 平移
2. 縮放
3. 旋轉
4. 透明
7.1.1 View動畫的種類
上述四種變換效果對應着Animation四個子類: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。這四種動畫皆能夠經過XML定義,也能夠經過代碼來動態建立。
|名稱|標籤|子類|效果
|–|–|–|–|
|平移動畫|<translate>
|TranslateAnimation|移動View
|縮放動畫|<scale>
|ScaleAnimation|放大或縮小View
|旋轉動畫|<rotate>
|RotateAnimation|旋轉View
|透明度動畫|<alpha>
|AlphaAnimation|改變View的透明度
xml定義動畫
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="300" //動畫插值器,影響動畫的播放速度 android:interpolator="@android:anim/accelerate_interpolator" //表示集合中的動畫是否和集合共享一個插值器 android:shareInterpolator="true" > //透明度動畫,對應 AlphaAnimation 類,能夠改變 View 的透明度 <alpha android:duration="3000" android:fromAlpha="0.0" android:toAlpha="1.0" /> //旋轉動畫,對應着 RotateAnimation ,它能夠使 View 具備旋轉的動畫效果 <rotate android:duration="2000" android:fromDegrees="0" android:interpolator="@android:anim/acceleratedecelerateinterpolator" android:pivotX="50%" android:pivotY="50%" android:startOffset="3000" android:toDegrees="180" /> <!--經過設置第一個alpha動畫播放3s後啓動rotate動畫實現組合動畫,若是不設置startOffset則同時播放 pivotX:表示旋轉時候的相對軸的座標點,即圍繞哪一點進行旋轉,默認狀況下軸點是 View 中心 --> //平移動畫,對應 TranslateAnimation 類,能夠使 View 完成垂直或者水平方向的移動效果。 <translate android:fromXDelta="500" android:toXDelta="0" /> //縮放動畫,對應 ScaleAnimation 類,能夠使 View 具備放大和縮小的動畫效果。 <scale android:duration="1000" android:fromXScale="0.0" android:fromYScale="0.0" android:interpolator="@android:anim/acceleratedecelerateinterpolator" android:pivotX="50" android:pivotY="50" android:toXScale="2" android:toYScale="2" /> </set>
- 標籤表示動畫集合,對應AnimationSet類,能夠包含一個或若干個動畫,內部還能夠嵌套其餘動畫集合。
-
android:interpolator 表示動畫集合所採用的插值器,插值器影響動畫速度,好比非勻速動畫就須要經過插值器來控制動畫的播放過程。
-
android:shareInterpolator 表示集合中的動畫是否和集合共享同一個插值器,若是集合不指定插值器,那麼子動畫就須要單獨指定所需的插值器或默認值。
-
<translate>
、<scale>
、<rotate>
、<alpha>
這幾個子標籤分別表明四種變換效果。- android:fillAfter屬性表示動畫結束之後, View 是否停留在結束動畫的位置,若是爲 false , View 會回到動畫開始的位置。這個參數在動畫 XML 文件的
</set>
節點中設置或在程序 Java 代碼中進行設置:setFillAfter(true)
。 - 定義完View動畫的xml後,經過如下代碼應用動畫:
Aniamation anim = AnimationUtils.loadAnimation(context,R.anim.animation_test);
view.startAnimation(anim);
代碼動態建立動畫
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1); alphaAnimation.setDuration(1500); view.startAnimation(alphaAnimation);
經過 setAnimationListener 給 View 動畫添加過程監聽
public static interface AnimationListener { void onAnimationStart(Animation animation); void onAnimationEnd(Animation animation); void onAnimationRepeat(Animation animation); }
7.1.2 自定義View動畫
除了系統提供的四種動畫外,咱們能夠根據需求自定義動畫,自定義一個新的動畫只須要繼承 Animation 這個抽象類,而後重寫它的 inatialize 和 applyTransformation 這兩個方法,在 initialize 方法中作一些初始化工做,在 Transformation 方法中進行矩陣變換便可,不少時候纔有 Camera 來簡化矩陣的變換過程,其實自定義動畫的主要過程就是矩陣變換的過程,矩陣變換是數學上的概念,須要掌握該方面知識方能輕鬆實現自定義動畫,例子能夠參考 Android 的 APIDemos 中的一個自定義動畫 Rotate3dAnimation ,這是一個能夠圍繞 Y 軸旋轉並同時沿着 Z 軸平移從而實現相似一種 3D 效果的動畫。
7.1.3 幀動畫
幀動畫是順序播放一組預先定義好的圖片,使用簡單,但容易引發OOM,因此在使用幀動畫時應儘可能避免使用過多尺寸較大的圖片。系統提供了另外一個類 AnimationDrawble 來使用幀動畫,使用的時候,須要經過 XML 定義一個 AnimationDrawble ,以下:
//\res\drawable\frameanimationlist.xml <?xml version="1.0" encoding="utf-8"?> <!-- 根標籤爲 animation-list,其中 oneshot 表明着是否只展現一遍,設置爲 false 會不停的循環播放動畫 根標籤下,經過 item 標籤對動畫中的每個圖片進行聲明 android:duration 表示展現所用的該圖片的時間長度 --> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/one" android:duration="2000"/> <item android:drawable="@drawable/two" android:duration="2000"/> <item android:drawable="@drawable/three" android:duration="2000"/> </animation-list
7.2 View動畫的特殊使用場景
View 動畫除了能夠實現的四種基本的動畫效果外,還能夠在一些特殊的場景下使用,好比在 ViewGroup 中能夠控制子元素的出場效果,在 Activity 中能夠實現不一樣 Activity 之間的切換效果。
7.2.1 LayoutAnimation
做用於ViewGroup,爲ViewGroup指定一個動畫,當它的子元素出場時都會具備這種動畫效
果,通常用在ListView上。
//res/anim/layout_animation.xml
<?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:delay="0.5" android:animationOrder="normal" android:animation="@anim/zoom_in"> </layoutAnimation>
-
android:delay
表示子元素開始動畫的延時時間,取值爲子元素入場動畫時間 duration 的倍數,好比子元素入場動畫時間週期爲 300ms ,那麼 0.5 表示每一個子元素都須要延遲 150ms 才能播放入場動畫,即第一個子元素延遲 150ms 開始播放入場動畫,第二個子元素延遲 300ms 開始播放入場動畫,依次類推動行。 -
android:animationOrder
表示子元素動畫的開場順序,normal(正序)、reverse(倒序)、random(隨機)。 -
爲 ViewGroup 指定屬性
android:layoutAnimation="@anim/layout_animation"
經過 LayoutAnimationController 來實現
//用於控制子 view 動畫效果 LayoutAnimationController layoutAnimationController= new LayoutAnimationController(AnimationUtils.loadAnimation(this,R.anim.zoom_in)); layoutAnimationController.setOrder(LayoutAnimationController.ORDER_NORMAL); listView.setLayoutAnimation(layoutAnimationController); listView.startLayoutAnimation();
7.2.2 Activity的切換效果
咱們能夠自定義Activity的切換效果,主要經過overridePendingTransition(int enterAnim , int exitAnim) 方法。該方法必需要在 startActivity(intent) 和 finish() 方法以後調用纔會有效。
//啓動新的Activity帶動畫 Intent intent=new Intent(MainActivity.this,Main2Activity.class); startActivity(intent); overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out); //退出Activity自己帶動畫 @Override public void finish() { super.finish(); overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out); }
Fragment 也能夠添加切換動畫,經過 FragmentTransation 中的 setCustomAnimations() 方法來實現切換動畫,這個動畫須要的是 View 動畫,不能使用屬性動畫,由於屬性動畫也是 API11 才引入的,不兼容。
7.3 屬性動畫
屬性動畫是 API 11 引入的新特性,屬性動畫能夠對任何對象作動畫,甚至還能夠沒有對象。能夠在一個時間間隔內完成對象從一個屬性值到另外一個屬性值的改變。==與View動畫相比,屬性動畫幾乎無所不能,只要對象有這個屬性,它都能實現動畫效果。==API11如下能夠經過 nineoldandroids 庫來兼容之前版本。
屬性動畫有ValueAnimator、ObjectAnimator和AnimatorSet等概念。其中ObjectAnimator繼承自ValueAnimator,AnimatorSet是動畫集合。
舉例:
1. 改變一個對象 TranslationY 屬性,讓其沿着 Y 軸平移一段距離
private void translateViewByObjectAnimator(View targetView){
//TranslationY 目標 View 要改變的屬性
//ivShow.getHeight() 要移動的距離
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,「TranslationY」,ivShow.getHeight());
objectAnimator.start();
}
2. 改變一個對象的背景色屬性,3秒內從0xFFFF8080到0xFF8080FF漸變,無限循環且有反轉效果
private void changeViewBackGroundColor(View targetView){
ValueAnimator valueAnimator=ObjectAnimator.ofInt(targetView,「backgroundColor」, Color.RED,Color.BLUE);
valueAnimator.setDuration(3000);
//設置估值器,該處插入顏色估值器
valueAnimator.setEvaluator(new ArgbEvaluator());
//無限循環
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
//反轉模式
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.start();
}
3. 動畫集合,5 秒內對 View 旋轉、平移、縮放和透明度進行了改變
private void startAnimationSet(View targetView){
AnimatorSet animatorSet=new AnimatorSet();
animatorSet.playTogether(ObjectAnimator.ofFloat(targetView,「rotationX」,0,360),
//旋轉
ObjectAnimator.ofFloat(targetView,「rotationY」,0,360),
ObjectAnimator.ofFloat(targetView,「rotation」,0,-90),
//平移
ObjectAnimator.ofFloat(targetView,「translationX」,0,90),
ObjectAnimator.ofFloat(targetView,「translationY」,0,90),
//縮放
ObjectAnimator.ofFloat(targetView,「scaleX」,1,1.5f),
ObjectAnimator.ofFloat(targetView,「scaleY」,1,1.5f),
//透明度
ObjectAnimator.ofFloat(targetView,「alpha」,1,0.25f,1));
animatorSet.setDuration(3000).start();
}
也能夠經過在xml中定義在 res/animator/ 目錄下。具體以下:
\res\animator\value_animator.xml
<!--對應着 ValueAnimator--> <animator android:duration="300" android:valueFrom="0" android:valueTo="360" android:startOffset="10" android:repeatCount="infinite" android:repeatMode="reverse" android:valueType="intType"/> </set>
使用動畫
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context , R.animator.ani
m);
set.setTarget(view);
set.start();
實際開發中建議使用代碼實現屬性動畫。不少時候一個屬性的起始值是沒法提早肯定的。
7.3.2 理解差值器和估值器
- 時間插值器( TimeInterpolator) 的做用是根據時間流逝的百分比來計算出當前屬性值改變的百分比,系統預置的有LinearInterpolator( 線性插值器:勻速動畫) ,AccelerateDecelerateInterpolator( 加速減速插值器:動畫兩頭慢中間快) ,DecelerateInterpolator(減速插值器:動畫愈來愈慢) 。
- 估值器(類型估值算法, TypeEvaluator) 的做用是根據當前屬性改變的百分比來計算改變後的屬性值。系統預置有IntEvaluator(針對整型屬性) 、FloatEvaluator(浮點型屬性) 、ArgbEvaluator(針對 Color 屬性)。
如圖所示,表示一個勻速動畫,採用了線性插值器和整型估值算法,在 40ms 內,View 的 X 屬性實現了從 0 到 40 的變化。
屬性動畫要求對象的該屬性有 set 和 get(可選) 方法,插值器和估值算法除了系統提供的外,咱們還能夠本身定義,插值器或者估值算法都是一個接口,且內部只有一個方法,咱們只須要派生一個類實現該接口便可,而後就能夠作出變幻無窮的動畫效果了。具體而言是:自定義插值器須要實現 Interpolator 或者 TimeInterpolator ,自定義估值算法須要實現 TypeEvaluator 。若是要對其餘類型(非int,float,color)作動畫,必需要自定義類型估值算法。
7.3.3 屬性動畫的監聽器
屬性動畫提供了監聽器用於監聽動畫的播放過程,主要有以下兩個接口:AnimatorUpdateListener 和 AnimatorListener 。
public static interface AnimatorListener { void onAnimationStart(Animator animation); //動畫開始 void onAnimationEnd(Animator animation); //動畫結束 void onAnimationCancel(Animator animation); //動畫取消 void onAnimationRepeat(Animator animation); //動畫重複播放 }
爲了方便開發,系統提供了AnimatorListenerAdapter類,它是AnimatorListener的適配器類,能夠有選擇的實現以上4個方法。
public static interface AnimatorUpdateListener { void onAnimationUpdate(ValueAnimator animation); }
AnimatorUpdateListener會監聽整個動畫的過程,動畫由許多幀組成的,每播放一幀,onAnimationUpdate就會調用一次。利用這個特性,咱們能夠作一些特殊的事情。
7.3.4 對任意屬性作動畫
屬性動畫原理:屬性動畫要求動畫做用的對象提供 get 方法和 set 方法,屬性動畫根據外界傳遞該屬性的初始值和最終值以動畫的效果去屢次調用 set 方法,每次傳遞給 set 方法的值都不同,確切的來講是隨着時間的推移,所傳遞的值愈來愈接近最終值。總結一下,咱們對 object 對象屬性 abc 作動畫,若是想要動畫生效,要同時知足兩個條件:
1. object 必需要提供 setAbc() 方法,若是動畫的時候沒有傳遞初始值,那麼還要提供 getAbc() 方法,由於系統要去取 abc 屬性的初始值(若是這條不知足,程序直接crash)。
2. object 的 setAbc() 對屬性 abc 所作的改變必須可以經過某種方法反應出來(即最終體現了 UI 的變化),好比會帶來 UI 的改變之類(若是這條不知足,動畫無效果,可是程序不會crash)。
咱們給 Button 的 width 屬性作動畫無效果可是沒有crash的緣由就是 Button 內部提供了 setWidth 和 getWidth 方法,可是這個 setWidth 方法並非改變 UI 大小的,而是用來設置最大寬度和最小寬度的。對於上面屬性動畫的兩個條件來講,這個例子只知足了條件 1 而未知足條件 2。
針對上面問題,官方文檔給出了 3 種解決方法:
1. 請給你的對象加上get和set方法,若是你有權限的話
對於SDK或者其餘第三方類庫的類沒法加上的
2. 用一個類來包裝原始對象,間接爲其提供get和set方法
/**
* 將 Button 沿着 X 軸方向放大
* @param button
*/
private void performAnimationByWrapper(View button){
ViewWrapper viewWrapper=new ViewWrapper(button);
ObjectAnimator.ofInt(viewWrapper,「width」,800)
.setDuration(5000)
.start();
}
private class ViewWrapper {
private View targetView;
public ViewWrapper(View targetView) {
this.targetView = targetView;
}
public int getWidth() {
//注意調用此函數能獲得 View 的寬度的前提是, View 的寬度是精準測量模式,即不能夠是 wrap_content
//不然得不到正確的測量值
return targetView.getLayoutParams().width;
}
public void setWidth(int width) {
//重寫設置目標 view 的佈局參數,使其改變大小
targetView.getLayoutParams().width = width;
//view 大小改變須要調用從新佈局
targetView.requestLayout();
}
}
3. 採用ValueAnimator,監聽動畫過程,本身實現屬性的改變
ValueAnimator 自己不做用於任何對象,也就是說直接使用它沒有任何動畫效果(因此係統提供了它的子類 ObjectAnimator 供咱們直接使用,做用於對象直接執行動畫效果,而 ValueAnimator 只是提供改變一個值的過程,並能監聽到整個值的改變過程,咱們基於這個過程能夠本身去實現動畫效果,在這個過程當中作想要達到的效果,本身去實現)。它能夠對一個值作動畫,而後咱們能夠監聽其動畫過程,在動畫過程當中修改咱們對象的屬性,這樣咱們本身就實現了對對象作了動畫。
//new 一個整型估值器,用於下面比例值計算使用(能夠本身去計算,這裏直接使用系統的)
private IntEvaluator intEvaluator = new IntEvaluator();
private void performAnimatorByValue(final View targetView, final int start, final int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//獲取當前動畫進度值
int currentValue = (int) animation.getAnimatedValue();
//獲取當前進度佔整個動畫比例
int fraction = (int) animation.getAnimatedFraction();
//直接經過估值器根據當前比例計算當前 View 的寬度,而後設置給 View
targetView.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
targetView.requestLayout();
}
});
valueAnimator.setDuration(5000)
.start();
}
7.3.5 屬性動畫的工做原理
屬性動畫須要運行在有Looper的線程中,系統經過反射調用被做用對象get/set方法。
7.4 使用動畫的注意事項
- OOM問題
使用幀動畫時,當圖片數量較多且圖片分辨率較大的時候容易出現OOM,需注意,儘可能避免使用幀動畫。 - 內存泄漏
使用無限循環的屬性動畫時,在Activity退出時即便中止,不然將致使Activity沒法釋放從而形成內存泄露。 - 兼容性問題
動畫在3.0如下的系統存在兼容性問題,特殊場景可能沒法正常工做,需作好適配工做。 - View動畫的問題
View動畫是對View的影像作動畫,並非真正的改變了View的狀態,所以有時候會出現動畫完成後View沒法隱藏( setVisibility(View.GONE) 失效) ,這時候調用 view.clearAnimation() 清理View動畫便可解決。 - 不要使用px
使用px會致使不一樣設備上有不一樣的效果。 - 動畫元素的交互
View動畫是對View的影像作動畫,View的真實位置沒有變更,動畫完成後的新位置是沒法觸發點擊事件的。屬性動畫是真實改變了View的屬性,因此動畫完成後的位置能夠接受觸摸事件。 - 硬件加速
使用動畫的過程當中,使用硬件加速能夠提升動畫的流暢度。
8 理解Window和WindowMananger
Window是一個抽象類,具體實現是 PhoneWindow 。無論是 Activity 、 Dialog 、 Toast 它們的視圖都是附加在Window上的,所以Window其實是View的直接管理者。WindowManager 是外界訪問Window的入口,經過WindowManager能夠建立Window,而Window的具體實現位於 WindowManagerService 中,WindowManager和WindowManagerService的交互是一個IPC過程。
8.1 Window和WindowManager
下面代碼演示了經過WindowManager添加Window的過程:
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mFloatingButton = new Button(this);
mFloatingButton.setText("click me"); mLayoutParams = new WindowManager.LayoutParams( LayoutParams.WRAPCONTENT, LayoutParams.WRAPCONTENT, 0, 0, PixelFormat.TRANSPARENT); mLayoutParams.flags = LayoutParams.FLAGNOTTOUCH_MODAL | LayoutParams.FLAGNOTFOCUSABLE | LayoutParams.FLAGSHOWWHEN_LOCKED; mLayoutParams.type = LayoutParams.TYPESYSTEMERROR; mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; mLayoutParams.x = 100; mLayoutParams.y = 300; mFloatingButton.setOnTouchListener(this); mWindowManager.addView(mFloatingButton, mLayoutParams);
上述代碼將一個button添加到屏幕座標爲(100,300)的位置上。WindowManager的flags和type這兩個屬性比較重要。
Flags表明Window的屬性,控制Window的顯示特性
1. FLAGNOTFOCUSABLE
在此模式下,Window不須要獲取焦點,也不須要接收各類輸入事件,這個標記同時會啓用FLAGNOTTOUCH_MODAL,最終事件會直接傳遞給下層具備焦點的Window。
2. FLAGNOTTOUCH_MODAL
在此模式下,系統將當前Window區域之外的點擊事件傳遞給底層的Window,當前Window區域內的單擊事件則本身處理。通常須要開啓此標記。
3. FLAGSHOWWHEN_LOCKED
開啓此模式Window將顯示在鎖屏界面上。
type參數表示Window的類型
1. 應用Window:Activity
2. 子Window: 如Dialog
3. 系統Window :如Toast和系統狀態欄
Window是分層的,每一個Window對應一個z-ordered,層級大的會覆蓋在層級小的上面,和HTM的z-index概念同樣。在三類Window中,應用Window的層級範圍是199,子Window的層級範圍是10001999,系統Window的層級範圍是2000~2999,這些值對應WindowManager.LayoutParams的type參數。通常系統Window選用 TYPESYSTEMOVERLAY 或者 TYPESYSTEMERROR ( 同時須要權限 <uses-permission android:name="android.permission.SYSTEMALERTWINDOW" />
) 。
WindowManager提供的功能很簡單,經常使用的只有三個方法:
1. 添加View
2. 更新View
3. 刪除View
這個三個方法定義在 ViewManager 中,而WindowManager繼承了ViewManager。
public interface ViewManager { public void addView(View view, ViewGroup.LayoutParams params); public void updateViewLayout(View view, ViewGroup.LayoutParams params); public void removeView(View view); }
如何拖動window?
給view設置onTouchListener:mFloatingButton.setOnTouchListener(this)。
在onTouch方法中更新view的位置,這個位置根據手指的位置設定。
8.2 Window的內部機制
Window是一個抽象的概念,每個Window都對應着一個View和一個ViewRootImpl,Window和View經過ViewRootImpl來創建聯繫。所以Window並非實際存在的,它是以View的形式存在的。因此WindowManager的三個方法都是針對View的,說明View纔是Window存在的實體。在實際使用中沒法直接訪問Window,必須經過WindowManager來訪問Window。
8.2.1 Window的添加過程
Window的添加過程須要經過WindowManager的addView()來實現, 而WindowManager是一個接口, 它的真正實現是WindowManagerImpl類。
WindowManagerImpl並無直接實現Window的三大操做, 而是所有交給了WindowManagerGlobal來處理. WindowManagerGlobal以工廠的形式向外提供本身的實例. 而WindowManagerImpl這種工做模式就典型的橋接模式, 將全部的操做所有委託給WindowManagerGlobal來實現.
- 檢查全部參數是否合法, 若是是子Window那麼還須要調整一些佈局參數.
- 建立ViewRootImpl並將View添加到列表中.
- 經過ViewRootImpl來更新界面並完成Window的添加過程.
這個過程是經過ViewRootImpl的setView()來完成的. View的繪製過程是由ViewRootImpl來完成的, 在內部會調用requestLayout()來完成異步刷新請求. 而scheduleTraversals()其實是View繪製的入口. 接着會經過WindowSession完成Window的添加過程(Window的添加過程是一次IPC調用). 最終會經過WindowManagerService來實現Window的添加.
WindowManagerService內部會爲每個應用保留一個單獨的Session.
8.2.2 Window的刪除過程
Window 的刪除過程和添加過程同樣, 都是先經過WindowManagerImpl後, 在進一步經過WindowManagerGlobal的removeView()來實現的.
方法內首先經過findViewLocked來查找待刪除的View的索引, 這個過程就是創建數組遍歷, 而後調用removeViewLocked來作進一步的刪除.
這裏經過ViewRootImpl的die()完成來完成刪除操做. die()方法只是發送了請求刪除的消息後就馬上返回了, 這個時候View並無完成刪除操做, 因此最後會將其添加到mDyingViews中, mDyingViews表示待刪除的View的列表.
die方法中只是作了簡單的判斷, 若是是異步刪除那麼就發送一個MSG_DIE的消息, ViewRootImpl中的Handler會處理此消息並調用doDie(); 若是是同步刪除, 那麼就不發送消息直接調用doDie()方法.
在doDie()方法中會調用dispatchDetachedFromWindow()方法, 真正刪除View的邏輯在這個方法內部實現. 其中主要作了四件事:
1. 垃圾回收的相關工做, 好比清除數據和消息,移除回調.
2. 經過Session的remove方法刪除Window: mWindowSession.remove(mWindow), 這一樣是一個IPC過程, 最終會調用WMS的removeWindow()方法.
3. 調用View的dispatchDetachedFromWindow()方法, 內部會調用View的onDetachedFromWindow()以及onDetachedFromWindowInternal(). 而對於onDetachedFromWindow()就是在View從Window中移除時, 這個方法就會被調用, 能夠在這個方法內部作一些資源回收的工做. 好比中止動畫,中止線程
4. 調用WindowManagerGlobal#doRemoveView方法刷新數據, 包括mRoots, mParams, mDyingViews, 須要將當前Window所關聯的這三類對象從列表中刪除.
8.2.3 Window的更新過程
WindowManagerGlobal#updateViewLayout()方法作的比較簡單, 它須要更新View的LayoutParams並替換掉老的LayoutParams, 接着在更新ViewRootImpl中的LayoutParams. 這一步主要是經過setLayoutParams()方法實現.
在ViewRootImpl中會經過scheduleTraversals()來對View從新佈局, 包括測量,佈局,重繪. 除了View自己的重繪之外, ViewRootImpl還會經過WindowSession來更新Window的視圖, 這個過程最後由WMS的relayoutWindow()實現一樣是一個IPC過程.
8.3 Window的建立過程
由以前的分析能夠知道,View是Android中視圖的呈現方式,可是View不能單獨存在,必須附着在Window這個抽象的概念上面,所以有視圖的地方就有Window。這些視圖包括:Activity、Dialog、Toast、PopUpWindow、菜單等等。
8.3.1 Activity的Window建立過程
Activity的大致啓動流程: 最終會由ActivityThread中的PerformLaunchActivity()來完成整個啓動過程, 這個方法內部會經過類加載器建立Activity的實例對象, 並調用其attach()方法爲其關聯運行過程當中所依賴的一系列上下文環境變量。
在attach()方法裏, 系統會建立Activity所屬的Window對象併爲其設置回調接口, Window對象的建立是經過PolicyManager#makeNewWindow()方法實現. 因爲Activity實現了Window的CallBack接口, 所以當Window接收到外界的狀態改變的時候就會回調Activity方法. 好比說咱們熟悉的onAttachedToWindow(), onDetachedFromWindow(), dispatchTouchEvent()等等。
Activity將具體實現交給了Window處理, 而Window的具體實現就是PhoneWindow, 因此只須要看PhoneWindow的相關邏輯。分爲如下幾步
1. 若是沒有DecorView, 那麼就建立它. 由installDecor()–>generateDecor()觸發
2. 將View添加到DecorView的mContentParent中
3. 回調Activity的onContentChanged()通知activity視圖已經發生改變
這個時候DecorView已經被建立並初始化完畢, Activity的佈局文件也已經添加成功到DecorView的mContentParent中. 可是這個時候DecorView尚未被WindowManager正式添加到Window中. 雖然早在Activity的attach方法中window就已經被建立了, 可是這個時候因爲DecorView並無被WindowManager識別, 因此這個時候的Window沒法提供具體功能, 由於他還沒法接收外界的輸入信息.
在ActivityThread#handleResumeActivity()方法中, 首先會調用Activity#onResume(), 接着會調用Activity#makeVisible(), 正是在makeVisible方法中, DecorView真正的完成了添加和顯示這兩個過程。
8.3.2 Dialog的Window建立過程
Dialog的Window的建立過程和Activity相似, 有以下幾步
1. 建立Window
Dialog的建立後的實際就是PhoneWindow, 這個過程和Activity的Window建立過程一致
2. 初始化DecorView並將Dialog的視圖添加到DecorView中
這個過程也相似, 都是經過Window去添加指定的佈局文件.
3. 將DecorView添加到Window中並顯示
在Dialog的show方法中, 會經過WindowManager將DecorView添加Window中.
普通的Dialog有一個特殊之處, 那就是必須採用Activity的Content, 若是採用Application的Content, 那麼就會報錯. 報的錯是沒有應用token所致使的, 而應用token通常只有Activity才擁有.
還有一種方法. 系統Window比較特殊, 他能夠不須要token, 所以只須要指定對話框的Window爲系統類型就能夠正常彈出對話框.
//JAVA 給Dialog的Window改變爲系統級的Window dialog.getWindow().setType(WindowManager.LayoutParams.TYPESYSTEMERROR); //XML 聲明權限 <uses-permission android:name="android.permission.SYSTEMALERTWINDOW"/>
8.3.3 Toast的Window建立過程
Toast和Dialog不一樣, 它的工做過程就稍顯複雜. 首先Toast也是基於Window來實現的. 可是因爲Toast具備定時取消的功能, 因此係統採用了Handler. 在Toast的內部有兩類IPC過程, 第一類是Toast訪問NotificationManagerService()後面簡稱NMS. 第二類是NotificationManagerService回調Toast裏的TN接口.
Toast屬於系統Window, 它內部的視圖有兩種方式指定, 一種是系統默認的樣式, 另外一種是經過setView方法來指定一個自定義View. 無論如何, 他們都對應Toast的一個View類型的內部成員mNextView. Toast內部提供了cancel和show兩個方法. 分別用於顯示和隱藏Toast. 他們內部是一個IPC過程.
顯示和隱藏Toast都是須要經過NMS來實現的. 因爲NMS運行在系統的進程中, 因此只能經過遠程調用的方式來顯示和隱藏Toast. 而TN這個類是一個Binder類. 在Toast和NMS進行IPC的過程當中, 當NMS處理Toast的顯示或隱藏請求時會跨進程回調TN的方法. 這個時候因爲TN運行在Binder線程池中, 因此須要經過Handler將其切換到當前主線程. 因此由其可知, Toast沒法在沒有Looper的線程中彈出, 由於Handler須要使用Looper才能完成切換線程的功能.
對於非系統應用來講, 最多能同時存在對Toast封裝的ToastRecord上限爲50個. 這樣作是爲了防止DOS(Denial of Service). 若是不這樣, 當經過大量循環去連續的彈出Toast, 這將會致使其餘應用沒有機會彈出Toast, 那麼對於其餘應用的Toast請求, 系統的行爲就是拒絕服務, 這就是拒絕服務攻擊的含義.
在ToastRecord被添加到mToastQueue()中後, NMS就會經過showNextToastLocked()方法來顯示當前的Toast.
Toast的顯示是由ToastRecord的callback來完成的. 這個callback實際上就是Toast中的TN對象的遠程Binder. 經過callback來訪問TN中的方法是須要跨進程的. 最終被調用的TN中的方法會運行在發起Toast請求的應用的Binder線程池.
Toast的隱藏也會經過ToastRecord的callback完成的.一樣是一次IPC過程. 方式和Toast顯示相似.
以上基本說明Toast的顯示和影響過程其實是經過Toast中的TN這個類來實現的. 他有兩個方法show(), hide(). 分別對應着Toast的顯示和隱藏. 因爲這兩個方法是被NMS以跨進程的方式調用的, 所以他們運行在Binder線程池中. 爲了將執行環境切換到Toast請求所在線程中, 在他們內部使用了handler。
TN的handleShow中會將Toast的視圖添加到Window中.
TN的handleHide中會將Toast的視圖從Window中移除.
以上三節的總結
1. 在建立視圖並顯示出來時,首先是經過建立一個Window對象,而後經過WindowManager對象的 addView(View view, ViewGroup.LayoutParams params); 方法將 contentView 添加到Window中,完成添加和顯示視圖這兩個過程。
2. 在關閉視圖時,經過WindowManager來移除DecorView, mWindowManager.removeViewImmediate( view); 。
3. Toast比較特殊,具備定時取消功能,因此係統採用了Handler,內部有兩類IPC過程:
- Toast訪問 NotificationManagerService
- NotificationManagerService 回調Toast裏的 TN 接口
顯示和隱藏Toast都經過NotificationManagerService( NMS) 來實現,而NMS運行在系統進程中,因此只能經過IPC來進行顯示/隱藏Toast。而TN是一個Binder類,在Toast和NMS進行IPC的過程當中,當NMS處理Toast的顯示/隱藏請求時會跨進程回調TN中的方法,這時因爲TN運行在Binder線程池中,因此須要經過Handler將其切換到當前線程( 即發起Toast請求所在的線程) ,而後經過WindowManager的 addView/removewView 方法真正完成顯示和隱藏Toast。
9 四大組件的工做過程
本章的意義在於加深對四大組件工做方式的認識,有助於加深對Android總體的體系結構的認識。不少狀況下,只有對Android的體系結構有必定認識,在實際開發中才能寫出優秀的代碼。 讀者對四大組件的工做過程有一個感性的認識而且可以給予上層開發一些指導意義。
9.1 四大組件的運行狀態
Android的四大組件除了BroadcastReceiver之外,都須要在AndroidManifest文件註冊,BroadcastReceiver能夠經過代碼註冊。調用方式上,除了ContentProvider之外的三種組件都須要藉助intent。
Activity
是一種展現型組件,用於向用戶直接地展現一個界面,而且能夠接收用戶的輸入信息從而進行交互,扮演的是一個前臺界面的角色。Activity的啓動由intent觸發,有隱式和顯式兩種方式。一個Activity能夠有特定的啓動模式,finish方法結束Activity運行。
Service
是一種計算型組件,在後臺執行一系列計算任務。它自己仍是運行在主線程中的,因此耗時的邏輯仍須要單獨的線程去完成。Activity只有一種狀態:啓動狀態。而service有兩種:啓動狀態和綁定狀態。當service處於綁定狀態時,外界能夠很方便的和service進行通訊,而在啓動狀態中是不可與外界通訊的。Service能夠中止, 須要靈活採用stopService和unBindService
BroadcastReceiver
是一種消息型組件,用於在不一樣的組件乃至不一樣的應用之間傳遞消
息。
-
靜態註冊
在清單文件中進行註冊廣播, 這種廣播在應用安裝時會被系統解析, 此種形式的廣播不須要應用啓動就能夠接收到相應的廣播. -
動態註冊
須要經過Context.registerReceiver()來實現, 並在不須要的時候經過Context.unRegisterReceiver()來解除廣播. 此種形態的廣播要應用啓動才能註冊和接收廣播. 在實際開發中經過Context的一系列的send方法來發送廣播, 被髮送的廣播會被系統發送給感興趣的廣播接收者, 發送和接收的過程的匹配是經過廣播接收者的<intent-filter>
來描述的.能夠實現低耦合的觀察者模式, 觀察者和被觀察者之間能夠沒有任何耦合. 但廣播不適合來作耗時操做.
ContentProvider
是一種數據共享型組件,用於向其餘組件乃至其餘應用共享數據。在它內部維持着一份數據集合, 這個數據集合既能夠經過數據庫來實現, 也能夠採用其餘任何類型來實現, 例如list或者map. ContentProvider對數據集合的具體實現並無任何要求.要注意處理好內部的insert, delete, update, query方法的線程同步, 由於這幾個方法是在Binder線程池被調用.
9.2 Activity的工做過程
1. Activity的全部 startActivity 重載方法最終都會調用 startActivityForResult 。
2. 調用 mInstrumentation.execStartActivity.execStartActivity() 方法。
3. 代碼中啓動Activity的真正實現是由ActivityManagerNative.getDefault().startActivity()方法完成的. ActivityManagerService簡稱AMS. AMS繼承自ActivityManagerNative(), 而ActivityManagerNative()繼承自Binder並實現了IActivityManager這個Binder接口, 所以AMS也是一個Binder, 它是IActivityManager的具體實現.ActivityManagerNative.getDefault()本質是一個IActivityManager類型的Binder對象, 所以具體實現是AMS.
4. 在ActivityManagerNative中, AMS這個Binder對象採用單例模式對外提供, Singleton是一個單例封裝類. 第一次調用它的get()方法時會經過create方法來初始化AMS這個Binder對象, 在後續調用中會返回這個對象.
5. AMS的startActivity()過程
1. checkStartActivityResult () 方法檢查啓動Activity的結果( 包括檢查有無在
manifest註冊)
2. Activity啓動過程通過兩次轉移, 最後又轉移到了mStackSupervisor.startActivityMayWait()這個方法, 所屬類爲ActivityStackSupervisor. 在startActivityMayWait()內部又調用了startActivityLocked()這裏會返回結果碼就是以前checkStartActivityResult()用到的。
3. 方法最後會調用startActivityUncheckedLocked(), 而後又調用了ActivityStack#resumeTopActivityLocked(). 這個時候啓動過程已經從ActivityStackSupervisor轉移到了ActivityStack類中.
6. 在最後的 ActivityStackSupervisor. realStartActivityLocked() 中,調用了 app.thread.scheduleLaunchActivity() 方法。 這個app.thread是ApplicationThread 類型,繼承於 IApplicationThread 是一個Binder類,內部是各類啓動/中止 Service/Activity的接口。
7. 在ApplicationThread中, scheduleLaunchActivity() 用來啓動Activity,裏面的實現就是發送一個Activity的消息( 封裝成 從ActivityClientRecord 對象) 交給Handler處理。這個Handler有一個簡潔的名字 H 。
8. 在H的 handleMessage() 方法裏,經過 handleLaunchActivity() 方法完成Activity對象的建立和啓動,而且ActivityThread經過handleResumeActivity()方法來調用被啓動的onResume()這一輩子命週期方法。PerformLaunchActivity()主要完成了以下幾件事:
1. 從ActivityClientRecord對象中獲取待啓動的Activity組件信息
2. 經過 Instrumentation 的 newActivity 方法使用類加載器建立Activity對象
3. 經過 LoadedApk 的makeApplication方法嘗試建立Application對象,經過類加載器實現( 若是Application已經建立過了就不會再建立)
4. 建立 ContextImpl 對象並經過Activity的 attach 方法完成一些重要數據的初始化(ContextImpl是一個很重要的數據結構, 它是Context的具體實現, Context中的大部分邏輯都是由ContentImpl來完成的. ContextImpl是經過Activity的attach()方法來和Activity創建關聯的,除此以外, 在attach()中Activity還會完成Window的建立並創建本身和Window的關聯, 這樣當Window接收到外部輸入事件收就能夠將事件傳遞給Activity.)
5. 經過 mInstrumentation.callActivityOnCreate(activity, r.state) 方法調用Activity的 onCreate 方法
9.3 Service的工做過程
- 啓動狀態:執行後臺計算
- 綁定狀態:用於其餘組件與Service交互
兩種狀態是能夠共存的
9.3.1 Service的啓動過程
1. Service的啓動從 ContextWrapper 的 startService 開始
2. 在ContextWrapper中,大部分操做經過一個 ContextImpl 對象mBase實現
3. 在ContextImpl中, mBase.startService() 會調用 startServiceCommon 方法,而
startServiceCommon方法又會經過 ActivityManagerNative.getDefault() ( 實際上就是AMS) 這個對象來啓動一個服務。
4. AMS會經過一個 ActiveService 對象( 輔助AMS進行Service管理的類,包括Service的啓動,綁定和中止等) mServices來完成啓動Service: mServices.startServiceLocked() 。
5. 在mServices.startServiceLocked()最後會調用 startServiceInnerLocked() 方法:將Service的信息包裝成一個 ServiceRecord 對象,ServiceRecord一直貫穿着整個Service的啓動過程。經過 bringUpServiceLocked() 方法來處理,bringUpServiceLocked()又調用了 realStartServiceLocked() 方法,這才真正地去啓動一個Service了。
6. realStartServiceLocked()方法的工做以下:
1. app.thread.scheduleCreateService() 來建立Service並調用其onCreate()生命週期方法
2. sendServiceArgsLocked() 調用其餘生命週期方法,如onStartCommand()
3. app.thread對象是 IApplicationThread 類型,實際上就是一個Binder,具體實現是ApplicationThread繼承ApplictionThreadNative
7. 具體看Application對Service的啓動過程app.thread.scheduleCreateService():經過 sendMessage(H.CREATE_SERVICE , s) ,這個過程和Activity啓動過程相似,同時經過發送消息給Handler H來完成的。
8. H會接受這個CREATE_SERVICE消息並經過ActivityThread的 handleCreateService() 來完成Service的最終啓動。
9. handleCreateService()完成了如下工做:
1. 經過ClassLoader建立Service對象
2. 建立Service內部的Context對象
3. 建立Application,並調用其onCreate()( 只會有一次)
4. 經過 service.attach() 方法創建Service與context的聯繫( 與Activity相似)
5. 調用service的 onCreate() 生命週期方法,至此,Service已經啓動了
6. 將Service對象存儲到ActivityThread的一個ArrayMap中
9.3.2 Service的綁定過程
和service的啓動過程相似的:
1. Service的綁定是從 ContextWrapper 的 bindService 開始
2. 在ContextWrapper中,交給 ContextImpl 對象 mBase.bindService()
3. 最終會調用ContextImpl的 bindServiceCommon 方法,這個方法完成兩件事:
- 將客戶端的ServiceConnection轉化成 ServiceDispatcher.InnerConnection 對象。ServiceDispatcher鏈接ServiceConnection和InnerConnection。這個過程經過 LoadedApk 的 getServiceDispatcher 方法來實現,將客戶端的ServiceConnection和ServiceDispatcher的映射關係存在一個ArrayMap中。
- 經過AMS來完成Service的具體綁定過程 ActivityManagerNative.getDefault().bindService()
- AMS中,bindService()方法再調用 bindServiceLocked() ,bindServiceLocked()再調用 bringUpServiceLocked() ,bringUpServiceLocked()又會調用 realStartServiceLocked() 。
- AMS的realStartServiceLocked()會調用 ActiveServices 的requrestServiceBindingLocked() 方法,最終是調用了ServiceRecord對象r的 app.thread.scheduleBindService() 方法。
- ApplicationThread的一系列以schedule開頭的方法,內部都經過Handler H來中轉:scheduleBindService()內部也是經過 sendMessage(H.BIND_SERVICE , s)
- 在H內部接收到BIND_SERVICE這類消息時就交給 ActivityThread 的handleBindService() 方法處理:
- 根據Servcie的token取出Service對象
- 調用Service的 onBind() 方法,至此,Service就處於綁定狀態了。
- 這時客戶端還不知道已經成功鏈接Service,須要調用客戶端的binder對象來調用客戶端的ServiceConnection中的 onServiceConnected() 方法,這個經過 ActivityManagerNative.getDefault().publishService() 進行。ActivityManagerNative.getDefault()就是AMS。
- AMS的publishService()交給ActivityService對象 mServices 的 publishServiceLocked() 來處理,核心代碼就一句話 c.conn.connected(r.name,service) 。對象c的類型是 ConnectionRecord ,c.conn就是ServiceDispatcher.InnerConnection對象,service就是Service的onBind方法返回的Binder對象。
- c.conn.connected(r.name,service)內部實現是交給了mActivityThread.post(new RunnConnection(name ,service,0)); 實現。ServiceDispatcher的mActivityThread是一個Handler,其實就是ActivityThread中的H。這樣一來RunConnection就經由H的post方法從而運行在主線程中,所以客戶端ServiceConnection中的方法是在主線程中被回調的。
- RunConnection的定義以下:
-
繼承Runnable接口, run() 方法的實現也是簡單調用了ServiceDispatcher的 doConnected 方法。
-
因爲ServiceDispatcher內部保存了客戶端的ServiceConntion對象,能夠很方便地調用ServiceConntion對象的 onServiceConnected 方法。
-
客戶端的onServiceConnected方法執行後,Service的綁定過程也就完成了。
-
根據步驟八、九、10service綁定後經過ServiceDispatcher通知客戶端的過程能夠說明ServiceDispatcher起着鏈接ServiceConnection和InnerConnection的做用。 至於Service的中止和解除綁定的過程,系統流程都是相似的。
-
9.4 BroadcastReceiver的工做過程
簡單回顧一下廣播的使用方法, 首先定義廣播接收者, 只須要繼承BroadcastReceiver並重寫onReceive()方法便可. 定義好了廣播接收者, 還須要註冊廣播接收者, 分爲兩種靜態註冊或者動態註冊. 註冊完成以後就能夠發送廣播了.
9.4.1 廣播的註冊過程
1. 動態註冊的過程是從ContextWrapper#registerReceiver()開始的. 和Activity或者Service同樣. ContextWrapper並無作實際的工做, 而是將註冊的過程直接交給了ContextImpl來完成.
2. ContextImpl#registerReceiver()方法調用了本類的registerReceiverInternal()方法.
3. 系統首先從mPackageInfo獲取到IIntentReceiver對象, 而後再採用跨進程的方式向AMS發送廣播註冊的請求. 之因此採用IIntentReceiver而不是直接採用BroadcastReceiver, 這是由於上述註冊過程當中是一個進程間通訊的過程. 而BroadcastReceiver做爲Android中的一個組件是不能直接跨進程傳遞的. 全部須要經過IIntentReceiver來中轉一下.
4. IIntentReceiver做爲一個Binder接口, 它的具體實現是LoadedApk.ReceiverDispatcher.InnerReceiver, ReceiverDispatcher的內部同時保存了BroadcastReceiver和InnerReceiver, 這樣當接收到廣播的時候, ReceiverDispatcher能夠很方便的調用BroadcastReceiver#onReceive()方法. 這裏和Service很像有一樣的類, 而且內部類中一樣也是一個Binder接口.
5. 因爲註冊廣播真正實現過程是在AMS中, 所以跟進AMS中, 首先看registerReceiver()方法, 這裏只關內心面的核心部分. 這段代碼最終會把遠程的InnerReceiver對象以及IntentFilter對象存儲起來, 這樣整個廣播的註冊就完成了.
9.4.2 廣播的發送和接收過程
廣播的發送有幾種:普通廣播、有序廣播和粘性廣播,他們的發送/接收流程是相似的,所以
只分析普通廣播的實現。
1. 廣播的發送和接收, 本質就是一個過程的兩個階段. 廣播的發送仍然開始於ContextImpl#sendBroadcase()方法, 之因此不是Context, 那是由於Context#sendBroad()是一個抽象方法. 和廣播的註冊過程同樣, ContextWrapper#sendBroadcast()仍然什麼都不作, 只是把事情交給了ContextImpl去處理.
2. ContextImpl裏面也幾乎什麼都沒有作, 內部直接向AMS發起了一個異步請求用於發送廣播.
3. 調用AMS#broadcastIntent()方法,繼續調用broadcastIntentLocked()方法。
4. 在broadcastIntentLocked()內部, 會根據intent-filter查找出匹配的廣播接收者並通過一系列的條件過濾. 最終會將知足條件的廣播接收者添加到BroadcastQueue中, 接着BroadcastQueue就會將廣播發送給相應廣播接收者.
5. BroadcastQueue#scheduleBroadcastsLocked()方法內並無當即發送廣播, 而是發送了一個BROADCASTINTENTMSG類型的消息, BroadcastQueue收到消息後會調用processNextBroadcast()方法。
6. 無序廣播存儲在mParallelBroadcasts中, 系統會遍歷這個集合並將其中的廣播發送給他們全部的接收者, 具體的發送過程是經過deliverToRegisteredReceiverLocked()方法實現. deliverToRegisteredReceiverLocked()負責將一個廣播發送給一個特定的接收者, 它的內部調用了performReceiverLocked方法來完成具體發送過程.
7. performReceiverLocked()方法調用的ApplicationThread#scheduleRegisteredReceiver()實現比較簡單, 它經過InnerReceiver來實現廣播的接收
8. scheduleRegisteredReceiver()方法中,receiver.performReceive()中的receiver對應着IIntentReceiver類型的接口. 而具體的實現就是ReceiverDispatcher$InnerReceiver. 這兩個嵌套的內部類是所屬在LoadedApk中的。
9. 又調用了LoadedApk$ReceiverDispatcher#performReceive()的方法.在performReceiver()這個方法中, 會建立一個Args對象並經過mActivityThread的post方法執行args中的邏輯. 而這些類的本質關係就是:
- Args: 實現類Runnable
- mActivityThread: 是一個Handler, 就是ActivityThread中的mH. mH就是ActivityThread$H. 這個內部類H之前說過.
- 實現Runnable接口的Args中BroadcastReceiver#onReceive()方法被執行了, 也就是說應用已經接收到了廣播, 同時onReceive()方法是在廣播接收者的主線程中被調用的.
android 3.1開始就增添了兩個標記爲. 分別是FLAGINCLUDESTOPPEDPACKAGES, FLAGEXCLUDESTOPPEDPACKAGES. 用來控制廣播是否要對處於中止的應用起做用.
-
FLAGINCLUDESTOPPED_PACKAGES: 包含中止應用, 廣播會發送給已中止的應用.
-
FLAG_EXCLUDESTOPPEDPACKAGES: 不包含已中止應用, 廣播不會發送給已中止的應用
在android 3.1開始, 系統就爲全部廣播默認添加了FLAG_EXCLUDESTOPPEDPACKAGES標識。 當這兩個標記共存的時候以FLAGINCLUDESTOPPED_PACKAGES(非默認項爲主).
應用處於中止分爲兩種
-
應用安裝後未運行
-
被手動或者其餘應用強停
開機廣播一樣受到了這個標誌位的影響. 從Android 3.1開始處於中止狀態的應用一樣沒法接受到開機廣播, 而在android 3.1以前處於中止的狀態也是能夠接收到開機廣播的.
9.5 ContentProvider的工做機制
ContentProvider是一種內容共享型組件, 它經過Binder向其餘組件乃至其餘應用提供數據. 當ContentProvider所在的進程啓動時, ContentProvider會同時啓動併發布到AMS中. 要注意:這個時候ContentProvider的onCreate()方法是先於Application的onCreate()執行的,這一點在四大組件是少有的現象.
1. 當一個應用啓動時,入口方法是ActivityThread的main方法,其中建立ActivityThread的實例並建立主線程的消息隊列;
2. ActivityThread的attach方法中會遠程調用ActivityManagerService的attachApplication,並將ApplicationThread提供給AMS,ApplicationThread主要用於ActivityThread和AMS之間的通訊;
3. ActivityManagerService的attachApplication會調用ApplicationThread的bindApplication方法,這個方法會經過H切換到ActivityThread中去執行,即調用handleBindApplication方法;
4. handleBindApplication方法會建立Application對象並加載ContentProvider,注意是先加載ContentProvider,而後調用Application的onCreate方法。
5. ContentProvider啓動後, 外界就能夠經過它所提供的增刪改查這四個接口來操做ContentProvider中的數據源, 這四個方法都是經過Binder來調用的, 外界沒法直接訪問ContentProvider, 它只能經過AMS根據URI來獲取到對應的ContentProvider的Binder接口IContentProvider, 而後再經過IContentProvider來訪問ContentProvider中的數據源.
ContentProvider的android:multiprocess屬性決定它是不是單實例,默認值是false,也就是默認是單實例。當設置爲true時,每一個調用者的進程中都存在一個ContentProvider對象。
當調用ContentProvider的insert、delete、update、query方法中的任何一個時,若是ContentProvider所在的進程沒有啓動的話,那麼就會觸發ContentProvider的建立,並伴隨着ContentProvider所在進程的啓動。
以query調用爲例
1. 首先會獲取IContentProvider對象, 無論是經過acquireUnstableProvider()方法仍是直接經過acquireProvider()方法, 他們的本質都是同樣的, 最終都是經過acquireProvider方法來獲取ContentProvider.
2. ApplicationContentResolver#acquireProvider()方法並無處理任何邏輯, 它直接調用了ActivityThread#acquireProvider()
3. 從ActivityThread中查找是否已經存在了ContentProvider了, 若是存在那麼就直接返回. ActivityThread中經過mProviderMap來存儲已經啓動的ContentProvider對象, 這個集合的存儲類型ArrayMap<ProviderKey, ProviderClientRecord> mProviderMap. 若是目前ContentProvider沒有啓動, 那麼就發送一個進程間請求給AMS讓其啓動項目目標ContentProvider, 最後再經過installProvider()方法來修改引用計數.
4. AMS是如何啓動ContentProvider的呢?首先會啓動ContentProvider所在的進程, 而後再啓動ContentProvider. 啓動進程是由AMS#startProcessLocked()方法來完成, 其內部主要是經過Process#start()方法來完成一個新進程的啓動, 新進程啓動後其入口方法爲ActivityThread#main()方法。
5. ActivityThread#main()是一個靜態方法, 在它的內部首先會建立ActivityThread實例並調用attach()方法來進行一系列初始化, 接着就開始進行消息循環. ActivityThread#attach()方法會將Application對象經過AMS#attachApplication方法跨進程傳遞給AMS, 最終AMS會完成ContentProvider的建立過程.
6. AMS#attachApplication()方法調用了attachApplication(), 而後又調用了ApplicationThread#bindApplication(), 這個過程也屬於進程通訊.bindApplication()方法會發送一個BIND_APPLICATION類型的消息給mH, 這是一個Handler, 它收到消息後會調用ActivityThread#handleBindApplication()方法.
7. ActivityThread#handlerBindApplication()則完成了Application的建立以及ContentProvider 能夠分爲以下四個步驟:
1. 建立ContentProvider和Instrumentation
2. 建立Application對象
3. 啓動當前進程的ContentProvider並調用onCreate()方法. 主要內部實現是installContentProvider()完成了ContentProvider的啓動工做, 首先會遍歷當前進程的ProviderInfo的列表並一一調用installProvider()方法來啓動他們, 接着將已經啓動的ContentProvider發佈到AMS中, AMS會把他們存儲在ProviderMap中, 這樣一來外部調用者就能夠直接從AMS中獲取到ContentProvider. installProvider()內部經過類加載器建立的ContentProvider實例並在方法中調用了attachInfo(), 在這內部調用了ContentProvider#onCreate()
4. 調用Application#onCreate()
通過了上述的四個步驟, ContentProvider已經啓動成功, 而且其所在的進程的Application也已經成功, 這意味着ContentProvider所在的進程已經完成了整個的啓動過程, 而後其餘應用就能夠經過AMS來訪問這個ContentProvider了.
當拿到了ContentProvider之後, 就能夠經過它所提供的接口方法來訪問它. 這裏要注意: 這裏的ContentProvider並非原始的ContentProvider. 而是ContentProvider的Binder類型對象IContentProvider, 而IContentProvider的具體實現是ContentProviderNative和ContentProvider.Transport. 後者繼承了前者.
若是還用query方法來解釋流程: 那麼最開始其餘應用經過AMS獲取到ContentProvider的Binder對象就是IContentProvider. 而IContentProvider的實際實現者是ContentProvider.Transport. 所以實際上外部應用調用的時候本質上會以進程間通訊的方式調用ContentProvider.Transport的query()方法。
10 Android的消息機制
Android的消息機制主要是指Handler的運行機制。從開發的角度來講,Handler是Android消息機制的上層接口。Handler的運行須要底層的 MessageQueue 和 Looper 的支撐。
-
MessageQueue是一個消息隊列,內部存儲了一組消息,以隊列的形式對外提供插入和刪除的工做,內部採用單鏈表的數據結構來存儲消息列表。
-
Lopper會以無限循環的形式去查找是否有新消息,若是有就處理消息,不然就一直等待着。
-
ThreadLocal並非線程,它的做用是在每一個線程中存儲數據。Handler經過ThreadLocal能夠獲取每一個線程中的Looper。
-
線程是默認沒有Looper的,使用Handler就必須爲線程建立Looper。咱們常常提到的主線程,也叫UI線程,它就是ActivityThread,被建立時就會初始化Looper。
10.1 Android的消息機制概述
-
Handler的主要做用是將某個任務切換到Handler所在的線程中去執行。爲何Android要提供這個功能呢?這是由於Android規定訪問UI只能經過主線程,若是子線程訪問UI,程序可能會致使ANR。那咱們耗時操做在子線程執行完畢後,咱們須要將一些更新UI的操做切換到主線程當中去。因此係統就提供了Handler。
-
系統爲何不容許在子線程中去訪問UI呢? 由於Android的UI控件不是線程安全的,多線程併發訪問可能會致使UI控件處於不可預期的狀態,爲何不加鎖?由於加鎖機制會讓UI訪問邏輯變得複雜;其次鎖機制會下降UI訪問的效率,由於鎖機制會阻塞某些線程的執行。因此Android採用了高效的單線程模型來處理UI操做。
-
Handler建立時會採用當前線程的Looper來構建內部的消息循環系統,若是當前線程沒有Looper就會報錯。Handler能夠經過post方法發送一個Runnable到消息隊列中,也能夠經過send方法發送一個消息到消息隊列中,其實post方法最終也是經過send方法來完成。
-
MessageQueue的enqueueMessage方法最終將這個消息放到消息隊列中,當Looper發現有新消息到來時,處理這個消息,最終消息中的Runnable或者Handler的handleMessage方法就會被調用,注意Looper是運行Handler所在的線程中的,這樣一來業務邏輯就切換到了Handler所在的線程中去執行了。
10.2 Android的消息機制分析
10.2.1 ThreadLocal的工做原理
ThreadLocal是一個線程內部的數據存儲類,經過它能夠在指定線程中存儲數據,數據存儲後,只有在指定線程中能夠獲取到存儲的數據,對於其餘線程來講沒法得到數據。
在某些特殊的場景下,ThreadLocal能夠輕鬆實現一些很複雜的功能。Looper、ActivityThread以及AMS都用到了ThreadLocal。當某些數據是以線程爲做用域而且不一樣線程具備不一樣的數據副本的時候,就能夠考慮採用ThreadLocal。
對於Handler來講,它須要獲取當前線程的Looper,而Looper的做用於就是線程而且不一樣的線程具備不一樣的Looper,經過ThreadLocal能夠輕鬆實現線程中的存取。
ThreadLocal的另外一個使用場景是可讓監聽器做爲線程內的全局對象而存在,在線程內部只要經過get方法就能夠獲取到監聽器。若是不採用ThreadLocal,只能採用函數參數調用和靜態變量的方式。而第一種方式在調用棧很深時很糟糕,第二種方式不具備擴展性,好比同時多個線程執行。
雖然在不一樣線程訪問同一個ThreadLocal對象,可是得到的值倒是不一樣的。不一樣線程訪問同一個ThreadLoacl的get方法,ThreadLocal的get方法會從各自的線程中取出一個數組,而後再從數組中根據當前ThreadLocal的索引去查找對應的Value值。
ThreadLocal的set方法:
public void set(T value) { Thread currentThread = Thread.currentThread(); //經過values方法獲取當前線程中的ThreadLoacl數據——localValues Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }
- 在 localValues 內部有一個數組: private Object[] table ,ThreadLocal的值就存在這個數組中。
- ThreadLocal的值在table數組中的存儲位置老是ThreadLocal的reference字段所標識的對象的下一個位置。
ThreadLocal的get方法:
public T get() {
// Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread);//找到localValues對象 if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) {//找到ThreadLocal的reference對象在table數組中的位置 return (T) table[index + 1];//reference字段所標識的對象的下一個位置就是ThreadLocal的值 } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); }
從ThreadLocal的set/get方法能夠看出,它們所操做的對象都是當前線程的localValues對象的table數組,所以在不一樣線程中訪問同一個ThreadLocal的set/get方法,它們ThreadLocal的讀/寫操做僅限於各自線程的內部。理解ThreadLocal的實現方式有助於理解Looper的工做原理。
10.2.2 消息隊列的工做原理
消息隊列指的是MessageQueue,主要包含兩個操做:插入和讀取。讀取操做自己會伴隨着刪除操做。
MessageQueue內部經過一個單鏈表的數據結構來維護消息列表,這種數據結構在插入和刪除上的性能比較有優點。
插入和讀取對應的方法分別是:enqueueMessage和next方法。
enqueueMessage()的源碼實現主要操做就是單鏈表的插入操做
next()的源碼實現也是從單鏈表中取出一個元素的操做,next()方法是一個無線循環的方法,若是消息隊列中沒有消息,那麼next方法會一直阻塞在這裏。當有新消息到來時,next()方法會返回這條消息並將其從單鏈表中移除。
10.2.3 Looper的工做原理
Looper在Android的消息機制中扮演着消息循環的角色,具體來講就是它會不停地從MessageQueue中查看是否有新消息,若是有新消息就會當即處理,不然就一直阻塞在那裏。
經過Looper.prepare()方法便可爲當前線程建立一個Looper,再經過Looper.loop()開啓消息循環。prepareMainLooper()方法主要給主線程也就是ActivityThread建立Looper使用的,本質也是經過prepare方法實現的。
Looper提供quit和quitSafely來退出一個Looper,區別在於quit會直接退出Looper,而quitSafely會把消息隊列中已有的消息處理完畢後才安全地退出。 Looper退出後,這時候經過Handler發送的消息會失敗,Handler的send方法會返回false。
在子線程中,若是手動爲其建立了Looper,在全部事情作完後,應該調用Looper的quit方法來終止消息循環,不然這個子線程就會一直處於等待狀態;而若是退出了Looper之後,這個線程就會馬上終止,所以建議不須要的時候終止Looper。
loop()方法會調用MessageQueue的next()方法來獲取新消息,而next是是一個阻塞操做,但沒有信息時,next方法會一直阻塞在那裏,這也致使loop方法一直阻塞在那裏。若是MessageQueue的next方法返回了新消息,Looper就會處理這條消息:msg.target.dispatchMessage(msg),這裏的msg.target是發送這條消息的Handler對象,這樣Handler發送的消息最終又交給Handler來處理了。
10.2.4 Handler的工做原理
Handler的工做主要包含消息的發送和接收過程。經過post的一系列方法和send的一系列方法來實現。
Handler發送過程僅僅是向消息隊列中插入了一條消息。MessageQueue的next方法就會返回這條消息給Looper,Looper拿到這條消息就開始處理,最終消息會交給Handler的dispatchMessage()來處理,這時Handler就進入了處理消息的階段。
handler處理消息的過程
Handler的構造方法
1. 派生Handler的子類
2. 經過CallbackHandler handler = new Handler(callback)
其中,callback接口定義以下
public interface Callback{
public boolean handleMessage(Message msg);
}
3. 經過Looper
public Handler(Looper looper){
this(looper,null,false);
}
10.3 主線程的消息循環
Android的主線程就是ActivityThread,主線程的入口方法爲main(String[] args),在main方法中系統會經過Looper.prepareMainLooper()來建立主線程的Looper以及MessageQueue,並經過Looper.loop()來開啓主線程的消息循環。
ActivityThread經過ApplicationThread和AMS進行進程間通訊,AMS以進程間通訊的方式完成ActivityThread的請求後會回調ApplicationThread中的Binder方法,而後ApplicationThread會向H發送消息,H收到消息後會將ApplicationThread中的邏輯切換到ActivityTread中去執行,即切換到主線程中去執行。四大組件的啓動過程基本上都是這個流程。
Looper.loop(),這裏是一個死循環,若是主線程的Looper終止,則應用程序會拋出異常。那麼問題來了,既然主線程卡在這裏了,(1)那Activity爲何還能啓動;(2)點擊一個按鈕仍然能夠響應?
問題1:startActivity的時候,會向AMS(ActivityManagerService)發一個跨進程請求(AMS運行在系統進程中),以後AMS啓動對應的Activity;AMS也須要調用App中Activity的生命週期方法(不一樣進程不可直接調用),AMS會發送跨進程請求,而後由App的ActivityThread中的ApplicationThread會來處理,ApplicationThread會經過主線程線程的Handler將執行邏輯切換到主線程。重點來了,主線程的Handler把消息添加到了MessageQueue,Looper.loop會拿到該消息,並在主線程中執行。這就解釋了爲何主線程的Looper是個死循環,而Activity還能啓動,由於四大組件的生命週期都是以消息的形式經過UI線程的Handler發送,由UI線程的Looper執行的。
問題2:和問題1原理同樣,點擊一個按鈕最終都是由系統發消息來進行的,都通過了Looper.loop()處理。 問題2詳細分析請看原書做者的Android中MotionEvent的來源和ViewRootImpl。
11 Android的線程和線程池
在Android系統,線程主要分爲主線程和子線程,主線程處理和界面相關的事情,而子線程通常用於執行耗時操做。AsyncTask底層是線程池;IntentService/HandlerThread底層是線程;
在Android中,線程的形態有不少種:
1. AsyncTask封裝了線程池和Handler。
2. HandlerThread是具備消息循環的線程,內部能夠使用handler
3. IntentService是一種Service,內部採用HandlerThread來執行任務,當任務執行完畢後IntentService會自動退出。因爲它是一種Service,因此不容易被系統殺死
操做系統中,線程是操做系統調度的最小單元,同時線程又是一種受限的系統資源,其建立和銷燬都會有相應的開銷。同時當系統存在大量線程時,系統會經過時間片輪轉的方式調度每一個線程,所以線程不可能作到絕對的併發,除非線程數量小於等於CPU的核心數。
頻繁建立銷燬線程不明智,使用線程池是正確的作法。線程池會緩存必定數量的線程,經過線程池就能夠避免由於頻繁建立和銷燬線程所帶來的系統開銷。
11.1 主線程和子線程
主線程主要處理界面交互邏輯,因爲用戶隨時會和界面交互,因此主線程在任什麼時候候都須要有較高響應速度,則不能執行耗時的任務;
android3.0開始,網絡訪問將會失敗並拋出NetworkOnMainThreadException這個異常,這樣作是爲了不主線程因爲被耗時操做所阻塞從而現ANR現象
11.2 Android中的線程形態
11.2.1 AsyncTask
AsyncTask是一種輕量級的異步任務類, 他能夠在線程池中執行後臺任務, 而後把執行的進度和最終的結果傳遞給主線程並在主線程更新UI. 從實現上來講. AsyncTask封裝了Thread和Handler, 經過AsyncTask能夠更加方便地執行後臺任務,可是AsyncTask並不適合進行特別耗時的後臺任務,對於特別耗時的任務來講, 建議使用線程池。
AsyncTask就是一個抽象的泛型類. 這三個泛型的意義
1. Params:參數的類型
2. Progress:後臺任務的執行進度的類型
3. Result:後臺任務的返回結果的類型
若是不須要傳遞具體的參數, 那麼這三個泛型參數能夠用Void來代替.
四個方法 :
-
onPreExecute()
在主線程執行, 在異步任務執行以前, 此方法會被調用, 通常能夠用於作一些準備工做 -
doInBackground()
在線程池中執行, 此方法用於執行異步任務, 參數params表示異步任務的輸入參數. 在此方法中能夠經過publishProgress()方法來更新任務的進度, publishProgress()方法會調用onProgressUpdate()方法. 另外此方法須要返回計算結果給onPostExecute() -
onProgressUpdate()
在主線程執行,在異步任務執行以後, 此方法會被調用, 其中result參數是後臺任務的返回值, 即doInBackground()的返回值. -
onPostExecute()
在主線程執行, 在異步任務執行以後, 此方法會被調用, 其中result參數是後臺任務的返回值, 即doInBackground的返回值.
除了上述的四種方法,還有onCancelled(), 它一樣在主線程執行, 當異步任務被取消時, onCancelled()方法會被調用, 這個時候onPostExecute()則不會被調用.
AsyncTask在使用過程當中有一些條件限制
1. AsyncTask的類必須在主線程被加載, 這就意味着第一次訪問AsyncTask必須發生在主線程, 這個問題不是絕對, 由於在Android 4.1及以上的版本已經被系統自動完成. 在Android 5.0的源碼中, 能夠看到ActivityThread#main()會調用AsyncTask#init()方法.
2. AsyncTask的對象必須在主線程中建立.
3. execute方法必須在UI線程調用.
4. 不要在程序中直接調用onPreExecute(), onPostExecute(), doInBackground和onProgressUpdate()
5. 一個AsyncTask對象只能執行一次, 即只能調用一次execute()方法, 不然會報運行時異常.
6. 在Android 1.6以前, AsyncTask是串行執行任務的; Android 1.6的時候AsyncTask開始採用線程池裏處理並行任務; 可是Android 3.0開始, 爲了不AsyncTask帶來的併發錯誤, AsyncTask又採用了一個線程來串行的執行任務. 儘管如此在3.0之後, 仍然能夠經過AsyncTask#executeOnExecutor()方法來並行執行任務.
11.2.2 AsyncTask的工做原理
AsyncTask中有兩個線程池(SerialExecutor和THREADPOOLEXECUTOR)和一個Handler(InternalHandler), 其中線程池SerialExecutor用於任務的排列, 而線程池THREADPOOLEXECUTOR用於真正的執行任務, 而InternalHandler用於將執行環境從線程切換到主線程, 其本質仍然是線程的調用過程.
AsyncTask的排隊過程:首先系統會把AsyncTask#Params參數封裝成FutureTask對象, FutureTask是一個併發類, 在這裏充當了Runnable的做用. 接着這個FutureTask會交給SerialExecutor#execute()方法去處理. 這個方法首先會把FutureTask對象插入到任務隊列mTasks中, 若是這個時候沒有正在活動AsyncTask任務, 那麼就會調用SerialExecutor#scheduleNext()方法來執行下一個AsyncTask任務. 同時當一個AsyncTask任務執行完後, AsyncTask會繼續執行其餘任務直到全部的任務都執行完畢爲止, 從這一點能夠看出, 在默認狀況下, AsyncTask是串行執行的
11.2.3 HandlerThread
HandlerThread繼承了Thread, 它是一種能夠使用Handler的Thread, 它的實現也很簡單, 就是run方法中經過Looper.prepare()來建立消息隊列, 並經過Looper.loop()來開啓消息循環, 這樣在實際的使用中就容許在HandlerThread中建立Handler.
從HandlerThread的實現來看, 它和普通的Thread有顯著的不一樣之處. 普通的Thread主要用於在run方法中執行一個耗時任務; 而HandlerThread在內部建立了消息隊列, 外界須要經過Handler的消息方式來通知HandlerThread執行一個具體的任務. HandlerThread是一個頗有用的類, 在Android中一個具體使用場景就是IntentService.
因爲HandlerThread#run()是一個無線循環方法, 所以當明確不須要再使用HandlerThread時, 最好經過quit()或者quitSafely()方法來終止線程的執行.
11.2.4 IntentService
IntentSercie是一種特殊的Service,繼承了Service而且是抽象類,任務執行完成後會自動中止,優先級遠高於普通線程,適合執行一些高優先級的後臺任務; IntentService封裝了HandlerThread和Handler
1. onCreate方法自動建立一個HandlerThread
2. 用它的Looper構造了一個Handler對象mServiceHandler,這樣經過mServiceHandler發送的消息都會在HandlerThread執行;
3. IntentServiced的onHandlerIntent方法是一個抽象方法,須要在子類實現,onHandlerIntent方法執行後,stopSelt(int startId)就會中止服務,若是存在多個後臺任務,執行完最後一個stopSelf(int startId)纔會中止服務。
11.3 Android線程池
優勢:
1. 重用線程池的線程,減小線程建立和銷燬帶來的性能開銷
2. 控制線程池的最大併發數,避免大量線程互相搶系統資源致使阻塞
3. 提供定時執行和間隔循環執行功能
Android中的線程池的概念來源於Java中的Executor, Executor是一個接口, 真正的線程池的實現爲ThreadPoolExecutor.Android的線程池 大部分都是通 過Executor提供的工廠方法建立的。 ThreadPoolExecutor提供了一系列參數來配製線程池, 經過不一樣的參數能夠建立不一樣的線程池. 而從功能的特性來分的話能夠分紅四類.
11.3.1 ThreadPoolExecutor
ThreadPoolExecutor是線程池的真正實現, 它的構造方法提供了一系列參數來配置線程池, 這些參數將會直接影響到線程池的功能特性.
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
-
corePoolSize: 線程池的核心線程數, 默認狀況下, 核心線程會在線程池中一直存活, 即便都處於閒置狀態. 若是將ThreadPoolExecutor#allowCoreThreadTimeOut屬性設置爲true, 那麼閒置的核心線程在等待新任務到來時會有超時的策略, 這個時間間隔由keepAliveTime屬性來決定. 當等待時間超過了keepAliveTime設定的值那麼核心線程將會終止.
-
maximumPoolSize: 線程池所能容納的最大線程數, 當活動線程數達到這個數值以後, 後續的任務將會被阻塞.
-
keepAliveTime: 非核心線程閒置的超時時長, 超過這個時長, 非核心線程就會被回收. allowCoreThreadTimeOut這個屬性爲true的時候, 這個屬性一樣會做用於核心線程.
-
unit: 用於指定keepAliveTime參數的時間單位, 這是一個枚舉, 經常使用的有TimeUtil.MILLISECONDS(毫秒), TimeUtil.SECONDS(秒)以及TimeUtil.MINUTES(分)
-
workQueue: 線程池中的任務隊列, 經過線程池的execute方法提交的Runnable對象會存儲在這個參數中.
-
threadFactory: 線程工廠, 爲線程池提供建立新線程的功能. ThreadFactory是一個接口.
ThreadPoolExecutor執行任務大體遵循以下規則:
1. 若是線程池中的線程數量未達到核心線程的數量, 那麼會直接啓動一個核心線程來執行任務.
2. 若是線程池中的線程數量已經達到或者超過核心線程的數量, 那麼任務會被插入到任務隊列中排隊等待執行.
3. 若是在步驟2中沒法將任務插入到任務隊列中, 這一般是由於任務隊列已滿, 這個時候若是線程數量未達到線程池的規定的最大值, 那麼會馬上啓動一個非核心線程來執行任務.
4. 若是步驟3中的線程數量已經達到最大值的時候, 那麼會拒絕執行此任務,ThreadPoolExecutor會調用RejectedExecution方法來通知調用者.
AsyncTask的THREADPOOLEXECUTOR線程池配置:
1. 核心線程數等於CPU核心數+1
2. 線程池最大線程數爲CPU核心數的2倍+1
3. 核心線程無超時機制,非核心線程的閒置超時時間爲1秒
4. 任務隊列容量是128
11.3.2 線程池的分類
FixedThreadPool
經過Executor#newFixedThreadPool()方法來建立. 它是一種線程數量固定的線程池, 當線程處於空閒狀態時, 它們並不會被回收, 除非線程池關閉了. 當全部的線程都處於活動狀態時, 新任務都會處於等待狀態, 直到有線程空閒出來. 因爲FixedThreadPool只有核心線程而且這些核心線程不會被回收, 這意味着它可以更加快速地響應外界的請求.
CachedThreadPool
經過Executor#newCachedThreadPool()方法來建立. 它是一種線程數量不定的線程池, 它只有非核心線程, 而且其最大值線程數爲Integer.MAX_VALUE. 這就能夠認爲這個最大線程數爲任意大了. 當線程池中的線程都處於活動的時候, 線程池會建立新的線程來處理新任務, 不然就會利用空閒的線程來處理新任務. 線程池中的空閒線程都有超時機制, 這個超時時長爲60S, 超過這個時間那麼空閒線程就會被回收.
和FixedThreadPool不一樣的是, CachedThreadPool的任務隊列其實至關於一個空集合, 這將致使任何任務都會當即被執行, 由於在這種場景下SynchronousQueue是沒法插入任務的. SynchronousQueue是一個很是特殊的隊列, 在不少狀況下能夠把它簡單理解爲一個沒法存儲元素的隊列. 在實際使用中不多使用.這類線程比較適合執行大量的耗時較少的任務
ScheduledThreadPool
經過Executor#newScheduledThreadPool()方法來建立. 它的核心線程數量是固定的, 而非核心線程數是沒有限制的, 而且當非核心線程閒置時會馬上被回收掉. 這類線程池用於執行定時任務和具備固定週期的重複任務
SingleThreadExecutor
經過Executor#newSingleThreadPool()方法來建立. 這類線程池內部只有一個核心線程, 它確保全部的任務都在同一個線程中按順序執行. 這類線程池意義在於統一全部的外界任務到一個線程中, 這使得在這些任務之間不須要處理線程同步的問題
12 Bitmap的加載和Cache
主要介紹:
1. 如何高效地加載一個Bitmap
2. Android中經常使用的緩存策略
3. 如何優化列表的卡頓
12.1 Bitmap的高效加載
先來簡單介紹一下如何加載一個Bitmap, Bitmap在android中指的是一張圖片, 能夠是png格式也能夠是jpg等其餘常見的圖片格式.
那麼如何加載一個圖片?首先BitmapFactory類提供了四種方法: decodeFile(), decodeResource(), decodeStream(), decodeByteArray(). 分別用於從文件系統, 資源文件, 輸入流以及字節數組加載出一個Bitmap對象. 其中decodeFile和decodeResource又間接調用了decodeStream()方法, 這四類方法最終是在Android的底層實現的, 對應着BitmapFactory類的幾個native方法.
高效加載的Bitmap的核心思想:採用BitmapFactory.Options來加載所需尺寸的圖片. 好比說一個ImageView控件的大小爲300300. 而圖片的大小爲800800. 這個時候若是直接加載那麼就比較浪費資源, 須要更多的內存空間來加載圖片, 這不是很必要的. 這裏咱們就能夠先把圖片按必定的採樣率來縮小圖片在進行加載. 不只下降了內存佔用,還在必定程度上避免了OOM異常. 也提升了加載bitmap時的性能.
而經過Options參數來縮放圖片: 主要是用到了inSampleSize參數, 即採樣率.
-
若是是inSampleSize=1那麼和原圖大小同樣,
-
若是是inSampleSize=2那麼寬高都爲原圖1/2, 而像素爲原圖的1/4, 佔用的內存大小也爲原圖的1/4
-
若是是inSampleSize=3那麼寬高都爲原圖1/3, 而像素爲原圖的1/9, 佔用的內存大小也爲原圖的1/9
-
以此類推…..
要知道Android中加載圖片具體在內存中的佔有的大小是根據圖片的像素決定的, 而與圖片的實際佔用空間大小沒有關係.並且若是要加載mipmap下的圖片, 還會根據不一樣的分辨率下的文件夾進行不一樣的放大縮小.
列舉如今有一張圖片像素爲:10241024, 若是採用ARGB8888(四個顏色通道每一個佔有一個字節,至關於1點像素佔用4個字節的空間)的格式來存儲.(這裏不考慮不一樣的資源文件下狀況分析) 那麼圖片的佔有大小就是102410244那如今這張圖片在內存中佔用4MB.
若是針對剛纔的圖片進行inSampleSize=2, 那麼最後佔用內存大小爲512512*4, 也就是1MB
採樣率的數值必須是大於1的整數是纔會有縮放效果, 而且採樣率同時做用於寬/高, 這將致使縮放後的圖片以這個採樣率的2次方遞減, 即內存佔用縮放大小爲1/(inSampleSize的二次方). 若是小於1那麼至關於=1的時候. 在官方文檔中指出, inSampleSize的取值應該老是爲2的指數, 好比1,2,4,8,16,32…若是外界傳遞inSampleSize不爲2的指數, 那麼系統會向下取整並選擇一個最接近的2的指數來代替. 好比若是inSampleSize=3,那麼系統會選擇2來代替. 可是這條規則並不做用於全部的android版本, 因此能夠當成一個開發建議
整理一下開發中代碼流程:
- 將BitmapFactory.Options的inJustDecodeBounds參數設置爲true並加載圖片.
- 從BitmapFactory.Options取出圖片的原始寬高信息, 他們對應於outWidth和outHeight參數
- 根據採樣率的規則並結合目標View的所需大小計算出採樣率inSampleSize
- 將BitmapFactory.Options的inJustDecodeBounds參數設爲false, 而後從新加載.
inJustDecodeBounds這個參數的做用就是在加載圖片的時候是否只是加載圖片寬高信息而不把圖片所有加載到內存. 因此這個操做是個輕量級的.
經過這些步驟就能夠整理出如下的工具加載圖片類調用decodeFixedSizeForResource()便可.
public class MyBitmapLoadUtil { /** * 對一個Resources的資源文件進行指定長寬來加載進內存, 並把這個bitmap對象返回 * * @param res 資源文件對象 * @param resId 要操做的圖片id * @param reqWidth 最終想要獲得bitmap的寬度 * @param reqHeight 最終想要獲得bitmap的高度 * @return 返回採樣以後的bitmap對象 */ public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){ // 首先先指定加載的模式 爲只是獲取資源文件的大小 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); //Calculate Size 計算要設置的採樣率 並把值設置到option上 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 關閉只加載屬性模式, 並從新加載的時候傳入自定義的options對象 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } /** * 一個計算工具類的方法, 傳入圖片的屬性對象和 想要實現的目標大小. 經過計算獲得採樣值 */ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { //Raw height and width of image //原始圖片的寬高屬性 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; // 若是想要實現的寬高比原始圖片的寬高小那麼就能夠計算出採樣率, 不然不須要改變採樣率 if (reqWidth < height || reqHeight < width){ int halfWidth = width/2; int halfHeight = height/2; // 判斷原始長寬的一半是否比目標大小小, 若是小那麼增大采樣率2倍, 直到出現修改後原始值會比目標值大的時候 while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){ inSampleSize *= 2; } } return inSampleSize; } }
12.2 Android中的緩存策略
當程序第一次從網絡上加載圖片後,將其緩存在存儲設備中,下次使用這張圖片的時候就不用再從網絡從獲取了。不少時候爲了提升應用的用戶體驗,每每還會把圖片在內存中再緩存一份,由於從內存中加載圖片比存儲設備中快。通常狀況會把圖片存一份到內存中,一份到存儲設備中,若是內存中沒找到就去存儲設備中找,尚未找到就從網絡上下載。
緩存策略包含緩存的添加、獲取和刪除操做。無論是內存仍是存儲設備,緩存大小都是有限制的。如何刪除舊的緩存並添加新的緩存,就對應緩存算法。
目前經常使用的一種緩存算法是LRU(Least Recently Used), 最近最少使用算法. 核心思想: 當緩存存滿時, 會優先淘汰那些近期最少使用的緩存對象. 採用LRU算法的緩存有兩種: LruCache和DiskLruCache,LruCache用於實現內存緩存, DiskLruCache則充當了存儲設備緩存, 當組合使用後就能夠實現一個相似ImageLoader這樣的類庫.
12.2.1 LruCache
LruCache是Android 3.1所提供的一個緩存類, 經過support-v4兼容包能夠兼容到早期的Android版本
LruCache是一個泛型類, 它內部採用了一個LinkedHashMap以強引用的方式存儲外界的緩存對象, 其提供了get和put方法來完成緩存的獲取和添加的操做. 當緩存滿了時, LruCache會移除較早使用的緩存對象, 而後在添加新的緩存對象. 普及一下各類引用的區別:
-
強引用: 直接的對象引用
-
軟引用: 當一個對象只有軟引用存在時, 系統內存不足時此對象會被gc回收
-
弱引用: 當一個對象只有弱引用存在時, 對象會隨下一次gc時被回收
LruCache是線程安全的。
LruCache 典型初始化過程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight() / 1024; } };
這裏只須要提供緩存的總容量大小(通常爲進程可用內存的1/8)並重寫 sizeOf 方法便可.sizeOf方法做用是計算緩存對象的大小。這裏大小的單位須要和總容量的單位(這裏是kb)一致,所以除以1024。一些特殊狀況下,須要重寫LruCache的entryRemoved方法,LruCache移除舊緩存時會調用entryRemoved方法,所以能夠在entryRemoved中完成一些資源回收工做(若是須要的話)。
還有獲取和添加方法,都比較簡單:
mMemoryCache.get(key) mMemoryCache.put(key,bitmap)
經過remove方法能夠刪除一個指定的對象。
從Android 3.1開始,LruCache稱爲Android源碼的一部分。
12.2.2 DiskLruCache
DiskLruCache用於實現磁盤緩存,DiskLruCache獲得了Android官方文檔推薦,但它不屬於Android SDK的一部分,源碼在這裏。
DiskLruCache的建立
DiskLruCache並不能經過構造方法來建立, 他提供了open()方法用於建立自身, 以下所示
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
-
File directory: 表示磁盤緩存在文件系統中的存儲路徑. 能夠選擇SD卡上的緩存目錄, 具體是指/sdcard/Andriod/data/packagename/cache目錄, packagename表示當前應用的包名, 當應用被卸載後, 此目錄會一併刪除掉. 也能夠選擇data目錄下. 或者其餘地方. 這裏給出的建議:若是應用卸載後就但願刪除緩存文件的話 , 那麼就選擇SD卡上的緩存目錄, 若是但願保留緩存數據那就應該選擇SD卡上的其餘目錄.
-
int appVersion: 表示應用的版本號, 通常設爲1便可. 當版本號發生改變的時候DiskLruCache會清空以前全部的緩存文件, 在實際開發中這個實用性不大.
-
int valueCount: 表示單個節點所對應的數據的個數, 通常設爲1.
-
long maxSize: 表示緩存的總大小, 好比50MB, 當緩存大小超出這個設定值後, DiskLruCache會清除一些緩存而保證總大小不大於這個設定值.
//初始化DiskLruCache,包括一些參數的設置 public void initDiskLruCache { //配置固定參數 // 緩存空間大小 private static final long DISKCACHESIZE = 1024 * 1024 * 50; //下載圖片時的緩存大小 private static final long IOBUFFERSIZE = 1024 * 8; // 緩存空間索引,用於Editor和Snapshot,設置成0表示Entry下面的第一個文件 private static final int DISKCACHEINDEX = 0; //設置緩存目錄 File diskLruCache = getDiskCacheDir(mContext, "bitmap"); if(!diskLruCache.exists()) diskLruCache.mkdirs(); //建立DiskLruCache對象,固然是在空間足夠的狀況下 if(getUsableSpace(diskLruCache) > DISKCACHESIZE) { try { mDiskLruCache = DiskLruCache.open(diskLruCache, getAppVersion(mContext), 1, DISKCACHESIZE); mIsDiskLruCache = true; }catch(IOException e) { e.printStackTrace(); } } }
//上面的初始化過程總共用了3個方法
//設置緩存目錄
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}// 獲取可用的存儲大小
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDKINT >= VERSIONCODES.GINGERBREAD)
return path.getUsableSpace();
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
//獲取應用版本號,注意不一樣的版本號會清空緩存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
DiskLruCache的緩存添加
DiskLruCache的緩存添加的操做是經過Editor完成的, Editor表示一個緩存對象的編輯對象.
若是仍是緩存圖片爲例子, 每一張圖片都經過圖片的url爲key, 這裏因爲url可能會有特殊字符因此採用url的md5值做爲key. 根據這個key就能夠經過edit()來獲取Editor對象, 若是這個緩存對象正在被編輯, 那麼edit()就會返回null. 即DiskLruCache不容許同時編輯一個緩存對象.
當用.edit(key)得到了Editor對象以後. 經過editor.newOutputStream(0)就能夠獲得一個文件輸出流. 因爲以前open()方法設置了一個節點只能有一個數據. 因此在得到輸出流的時候傳入常量0便可.
有了文件輸出流, 能夠當網絡下載圖片時, 圖片就能夠經過這個文件輸出流寫入到文件系統上.最後,要經過Editor中commit()來提交寫操做, 若是下載中發生異常, 那麼使用Editor中abort()來回退整個操做.
DiskLruCache的緩存查找
和緩存的添加過程相似, 緩存查找過程也須要將url轉換成key, 而後經過DiskLruCache#get()方法能夠獲得一個Snapshot對象, 接着在經過Snapshot對象便可獲得緩存的文件輸入流, 有了文件輸入流, 天然就能夠獲得Bitmap對象. 爲了不加載圖片出現OOM因此採用壓縮的方式. 在前面對BitmapFactory.Options的使用說明了. 可是這中方法對FileInputStream的縮放存在問題. 緣由是FileInputStream是一種有序的文件流, 而兩次decodeStream調用會影響文件的位置屬性, 這樣在第二次decodeStream的時候獲得的會是null. 針對這一個問題, 能夠經過文件流來獲得它所對應的文件描述符, 而後經過BitmapFactory.decodeFileDescription()來加載一張縮放後的圖片.
/** * 磁盤緩存的讀取 * @param url * @param reqWidth * @param reqHeight * @return */ private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException { if(Looper.myLooper() == Looper.getMainLooper()) Log.w(TAG, "it's not recommented load bitmap from UI Thread"); if(mDiskLruCache == null) return null; Bitmap bitmap = null; String key = hashKeyForDisk(url); Snapshot snapshot = mDiskLruCache.get(key); if(snapshot != null) { FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISKCACHEINDEX); FileDescriptor fd = fileInputStream.getFD(); bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight); if(bitmap != null) addBitmapToMemoryCache(key, bitmap); } return bitmap; }
12.2.3 ImageLoader的實現
一個好的ImageLoader應該具有如下幾點:
-
圖片的壓縮
-
網絡拉取
-
內存緩存
-
磁盤緩存
-
圖片的同步加載
-
圖片的異步加載
圖片壓縮功能
ImageResizer
內存緩存和磁盤緩存
ImageLoader
同步加載和異步加載的接口設計
ImageLoader 173行
異步加載過程:
1. bindBitmap先嚐試從內存緩存讀取圖片,若是沒有會在線程池中調用loadBitmap方法。獲取成功將圖片封裝爲LoadResult對象經過mMainHandler向UI線程發送消息。選擇線程池和Handler來提供併發能力和異步能力。
2. 爲了解決View複用致使的列表錯位問題,在給ImageView設置圖片以前都會檢查它的url有沒有發生改變,若是改變就再也不給它設置圖片。(76行)
12.3 ImageLoader的使用
12.3.1 照片牆效果
實現照片牆效果,若是圖片都須要是正方形;這樣作很快,自定義一個ImageView,重寫onMeasure方法。
@Override protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ super.onMeasure(widthMeasureSpec,widthMeasureSpec);//將原來的參數heightMeasureSpec換成widthMeasureSpec }
12.3.2 優化列表的卡頓現象
- 不要在getView中執行耗時操做,不要在getView中直接加載圖片。
- 控制異步任務的執行頻率:若是用戶刻意頻繁上下滑動,getView方法會不停調用,從而產生大量的異步任務。能夠考慮在列表滑動中止加載圖片;給ListView或者GridView設置 setOnScrollListener 並在 OnScrollListener 的 onScrollStateChanged 方法中判斷列表是否處於滑動狀態,若是是的話就中止加載圖片。
- 大部分狀況下,能夠使用硬件加速解決莫名卡頓問題,經過設置 android:hardwareAccelerated=「true」 便可爲Activity開啓硬件加速。
13 綜合技術
本章主要講解:
1. CrashHandler來監視App的crash信息
2. 經過Google的multiDex方案解決Android方法數超過65536的問題
3. Android動態加載dex
4. 反編譯
13.1 使用CrashHandler來獲取應用的crash信息
如何檢測崩潰並瞭解詳細的crash信息? 首先需實現一個uncaughtExceptionHandler對象,在它的uncaughtException方法中獲取異常信息並將其存儲到SD卡或者上傳到服務器中,而後調用Thread的setDefaultUncaughtExceptionHandler爲當前進程的全部線程設置異常處理器。
在Application初始化的時候爲線程設置CrashHandler,這樣以後,Crash就會經過咱們本身的異常處理器來處理異常了。
public class BaseApplication extends Application { @Override public void onCreate() { super.onCreate(); CrashHandler crashHandler = CrashHandler.getInstance(); crashHandler.init(this); } }
13.2 使用multidex來解決方法數越界
Android中單個dex文件所能包含的最大方法數爲65536, 這包含了FrameWork, 依賴的jar包以及應用自己的代碼中的全部方法. 會爆出:
com.android.dex.DexIndexOverflowException: method ID not in[0, 0xffff] :65536
可能在一些低版本的手機, 即便沒有超過方法數的上限卻仍是出現錯誤
E/dalvikvm: Optimization failed E/installd: dexopt failed on '/data/dalvik-cache/.....'
這個現象, 首先dexpot是一個程序, 應用在安裝時, 系統會經過dexopt來優化dex文件, 在優化過程當中dexopt採用一個固定大小的緩衝區來存儲應用中全部方法消息, 這個緩衝區就是linearAlloc. LinearAlloc緩衝區在新版本的Android系統中大小爲8MB或者16MB. 在Android 2.2和2.3中卻只有5MB. 這是若是方法過多, 即便方法數沒有超過65535也有可能會由於存儲空間失敗而沒法安裝.
解決方案
-
插件化: 是一套重量級的技術方案, 經過將一個dex拆分紅兩個或者多個dex,能夠在必定程度上解決方法數的越界問題. 可是還有兼容性問題須要考慮, 因此須要權衡是否須要使用這個方案.
-
multidex: 這是Google在2014年提出的解決方案.在Android5.0以前須要引入Google提供的android-support-multidex.jar;從5.0開始系統默認支持了multidex,它能夠從apk文件中加載多個dex文件。
使用步驟:
1. 修改對應工程目錄下的build.gradle文件,在defaultConfig中添加multiDexEnabled
true這個配置項。
2. 在build.gradle的dependencies中添加multidex的依賴:compile
‘com.android.support:multidex:#1.0.0’
3. 代碼中加入支持multidex功能。
1. 第一種方案,在manifest文件中指定Application爲MultiDexApplication。
2. 第二種方案,讓應用的Application繼承MultiDexApplication。
3. 第三種方案,重寫 attachBaseContext 方法,這個方法比onCreate還要先執行。
public class BaseApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
採用上面的配置項後,若是這個應用方法數沒有越界,那麼Gradle是不會生成多個dex文件的,當方法數越界後,Gradle就會在apk中打包2個或多個dex文件。當須要指定主dex文件中所包含的類,這時候就須要經過–multi-dex-list來選項來實現這個功能。
//在對應工程目錄下的build.gradle文件,加入 afterEvaluate { println "afterEvaluate" tasks.matching { it.name.startsWith('dex') }.each { dx -> def listFile = project.rootDir.absolutePath + '/app/maindexlist.txt' println "root dir:" + project.rootDir.absolutePath println "dex task found: " + dx.name if (dx.additionalParameters == null) { dx.additionalParameters = [] } dx.additionalParameters += '--multi-dex' dx.additionalParameters += '--main-dex-list=' + listFile dx.additionalParameters += '--minimal-main-dex' } }
maindexlist.txt
com/ryg/multidextest/TestApplication.class com/ryg/multidextest/MainActivity.class // multidex 這9個類必須在主Dex中 android/support/multidex/MultiDex.class android/support/multidex/MultiDexApplication.class android/support/multidex/MultiDexExtractor.class android/support/multidex/MultiDexExtractor$1.class android/support/multidex/MultiDex$V4.class android/support/multidex/MultiDex$V14.class android/support/multidex/MultiDex$V19.class android/support/multidex/ZipUtil.class android/support/multidex/ZipUtil$CentralDirectory.class
須要注意multidex的jar中的9個類必需要打包到主dex中,由於Application的attachBaseContext方法中須要用到MultiDex.install(this)須要用到MultiDex。
Multidex的缺點:
1. 啓動速度會下降,因爲應用啓動時會加載額外的dex文件,這將致使應用的啓動速度下降,甚至產生ANR現象。
2. 由於Dalvik linearAlloc的bug,能夠致使使用multidex的應用沒法在Android4.0以前的手機上運行,須要作大量兼容性測試。
13.3 Android動態加載技術
動態加載也叫插件化. 當項目愈來愈大的時候, 能夠經過插件化來減輕應用的內存和CPU佔用. 還能夠實現熱插拔, 便可以在不發佈新版本的狀況下更新某些模塊.
學習一下做者的插件化開源框架:dynamic-load-apk
各類插件化方案都須要解決3個基礎性問題
宿主和插件的概念:宿主是指普通的apk, 而插件通常指通過處理的dex或者apk. 在主流的插件化框架中多采用通過處理的apk來做爲插件, 處理方式每每和編譯以及打包環節有關, 另外不少插件化框架都須要用到代理Activity的概念, 插件Activity的啓動大多數是藉助一個代理Activity來實現.
資源訪問
插件中凡是以R開頭的資源文件都不能訪問。
Activity的工做主要是經過ContextImpl完成的,Activity中有一個mBase的成員變量,它的類型就是ContextImpl。Context有兩個獲取資源的抽象方法getAsssets()和getResources();只要實現這兩個方法就能夠解決資源問題。
protected void loadResources() { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, mDexPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(super.getTheme()); }
從loadResources()的實現看出,加載資源的方法是反射,經過調用AssetManager的addAssetPath方法,咱們能夠將一個apk中的資源加載到Resources對象中。傳遞的路徑能夠是zip或資源目錄,所以直接將apk的路徑傳給它,資源就加載到AssetManager了。而後再經過AssetManager建立一個新的Resources對象,經過這個對象就能夠訪問插件apk中的資源了。
接着在代理Activity中實現getAssets()和getResources()。關於代理Activity參考做者的插件化開源框架。
@Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; }
Activity的生命週期管理
爲何會有這個問題,其實很好理解,apk被宿主程序調起之後,apk中的activity其實就是一個普通的對象,不具備activity的性質,由於系統啓動activity是要作不少初始化工做的,而咱們在應用層經過反射去啓動activity是很難完成系統所作的初始化工做的,因此activity的大部分特性都沒法使用包括activity的生命週期管理,這就須要咱們本身去管理。
ClassLoader的管理
爲了不多個ClassLoader加載了同一個類所引起的類型轉換錯誤。將不一樣插件的ClassLoader存儲在一個HashMap中。
13.4 反編譯初步
- 使用dex2jar和jd-gui反編譯apk
- 使用apktool對apk進行二次打包
以上網上資料特別多,不贅述。
14 JNI與NDK編程
Java JNI本意爲Java Native Interface(java本地接口), 是爲方便java調用C或者C++等本地代碼所封裝的一層接口. 因爲Java的跨平臺性致使本地交互能力的很差, 一些和操做系統相關的特性Java沒法完成, 因而Java提供了JNI專門用於和本地代碼交互.
NDK是android所提供的一個工具合集, 經過NDK能夠在Android中更加方便地經過JNI來訪問本地代碼. NDK還提供了交叉編譯工具, 開發人員只須要簡單的修改mk文件就能夠生成特定的CPU平臺的動態庫. 好處以下:
1. 代碼的保護。因爲apk的java層代碼很容易被反編譯,而C/C庫反編譯難度較大。
2. 能夠方便地使用C/C開源庫。
3. 便於移植,用C/C++寫的庫能夠方便在其餘平臺上再次使用
4. 提供程序在某些特定情形下的執行效率,可是並不能明顯提高Android程序的性能。
14.1 JNI的開發流程
在Java中聲明natvie方法
建立一個類
生命了兩個native方法:get和set(String)。這是須要在JNI實現的方法。JniTest頭部有一個加載動態庫的過程, 加載so庫名稱填入的雖然是jni-test, 可是so庫全名稱應該是libjni-test.so,這是加載so庫的規範。
編輯Java源文件獲得class文件, 而後經過javah命令導出JNI頭文件
在包的的根路徑, 進行命令操做
javac com/szysky/note/androiddevseek_14/JNITest.java javah com.szysky.note.androiddevseek_14.JNITest
執行以後會在, 操做的路徑下生成一個comszyskynoteandroiddevseek14_JNITest.h頭文件, 這個就是第二步生成的東西.
- 函數名:格式遵循:Java包名類名方法名包名之間的.分割所有替換成分割.
- 參數: jstring是表明String類型參數. 具體的類型關係後面會說明.
- JNIEnv *: 表示一個指向JNI環境的指針, 能夠經過它來訪問JNI提供的方法.
- jobject: 表示java對象中的this.
- JNIEXPORT和JNICALL: 這是JNI種所定義的宏, 能夠在jni.h這個頭文件查到
#ifdef cplusplus extern "C" { #endif
而這個宏定義是必須的, 做用是指定extern」C」內部的函數採用C語言的命名風格來編譯. 若是設定那麼當JNI採用C++
來實現時, 因爲C/C++
編譯過程對函數的命名風格不一樣, 這將致使JNI在連接時沒法根據函數名找到具體的函數, 那麼JNI調用確定會失效.
用C/C++實現natvie方法
JNI方法是指的Java中聲明的native方法, 這裏能夠選擇c++和c來實現. 過程都是相似的. 只有少許的區別, 這裏兩種都實現一下.
在工程的主目錄建立一個子目錄, 名稱任意, 而後將以前經過javah命令生成的.h頭文件複製到建立的目錄下, 接着建立test.cpp和test.c兩個文件,實現以下:
test.app
#include 「comszyskynoteandroiddevseek14_JNITest.h」
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz){
printf(「執行在c++文件中 get方法\n」);
return env->NewStringUTF(「Hello from JNI .」);
}
JNIEXPORT void JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz, jstring string){
printf(「執行在c++文件中 set方法\n」);
char str = (char) env->GetStringUTFChars(string, NULL);
printf(「\n, str」);
env->ReleaseStringUTFChars(string, str);
}
test.c
#include 「comszyskynoteandroiddevseek14_JNITest.h」
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv *env, jobject thiz){
printf(「執行在c文件中 get方法\n」);
return (env)->NewStringUTF(「Hello from JNI .」);
JNIEXPORT void JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz, jstring string){
printf(「執行在c文件中 set方法\n」);
char str = (char) (*env)->GetStringUTFChars(env, string, NULL);
printf(「%s\n, str」);
(*env)->ReleaseStringUTFChars(env, string, str);
}}
其實C\C++在實現上很類似, 可是對於env的操做方式有所不一樣.
C++: env->ReleaseStringUTFChars(string, str); C: (*env)->ReleaseStringUTFChars(env, string, str);
編譯so庫並在java中調用
so庫的編譯這裏採用gcc. 命令cd到放置剛纔生成c/c++
的目錄下.
使用以下命令:
gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
/user/lib/jvm/java-7-openjdk-amd64是本地jdk的安裝路徑,libjni-test.so是生產的so庫的名字。Java中經過:System.loadLibrary("jni-test")
加載,其中lib
和.so
不須要指出。
切換到主目錄,經過Java指令執行Java程序:java -Djava.library.path=jni com.ryg.JniTest
。其中-Djava.library.path=jni
指明瞭so庫的路徑。
14.2 NDK的開發流程
下載並配置NDK
下載好NDK開發包,而且配置好NDK的全局變量。
建立一個Android項目,並聲明所需的native方法
public static native String getStringFromC();
實現Android項目中所聲明的native方法
1. 生成C/C++的頭文件
i. 打開控制檯,用cd命令切換到當前項目當前目錄
ii. 使用javah命令生成頭文件
javah -classpath bin\classes;C:\MOX\AndroidSDK\platforms\android-23\android.jar -d jni cn.hudp.hellondk.MainActivity
說明:bin\classes 爲項目的class文件的相對路徑 ; C:\MOX\AndroidSDK\platforms\android-23\android.jar 爲android.jar的全路徑,由於咱們的的Activity使用到了Android SDK,因此生成頭文件時須要他; -d jni就是生成的頭文件輸出到項目的jni文件夾下; 最後跟的cn.hudp.hellondk.MainActivity是native方法所在的類的包名和類名。
2. 編寫修改對應的android.mk文件( mk文件是NDK開發所用到的配置文件)
# Copyright (C) 2009 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # L OCAL_PATH := $(call my-dir) include $(CLEAR_VARS) ## 對應Java部分 System.loadLibrary(String libName) 的libname LOCAL_MODULE := hello ## 對應c/c++的實現文件名 LOCALSRCFILES := hello.c include $(BUILDSHAREDLIBRARY)
- 編寫Application.mk,來指定需生成的平臺對應的動態庫,這裏是全平臺支持,也能夠特殊指定。目前常見的架構平臺有armeabi、x86和mips。其中移動設備主要是armeabi,所以大部分apk中只包含armeabi的so庫。
APP_ABI := all
切換到jni目錄的父目錄,而後經過ndk-build命令編譯產生so庫
ndk-build 命令會默認指定jni目錄爲本地源碼的目錄
將編譯好的so庫放到Android項目中的 app/src/main/jniLbis 目錄下,或者經過以下app的gradle設置新的存放so庫的目錄:
android{
……
sourceSets.main{
jniLibs.srcDir 'src/main/jni_libs' } }
還能夠經過 defaultConfig 區域添加NDK選項
android{
……
defaultConfig{
……
ndk{
moduleName "jni-test" } } }
還能夠在 productFlavors 設置動態打包不一樣平臺CPU對應的so庫進apk( 縮小APK體積)
gradle
android{
……
productFlavors{
arm{
ndk{
adiFilter "armeabi" } } x86{ ndk{ adiFilter "x86" } } } } `
在Android中調用
public class MainActivity extends Activity { public static native String getStringFromC(); static{//在靜態代碼塊中調用所須要的so文件,參數對應.so文件所對應的LOCAL_MODULE; System.loadLibrary("hello"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //在須要的地方調用native方法 Toast.makeText(getApplicationContext(), get(), Toast.LENGTH_LONG).show(); } }
14.3 JNI的數據類型和類型簽名
JNI的數據類型包含兩種: 基本類型和引用類型.
基本類型主要有jboolean, jchar, jint等, 和Java中的數據類型對應以下:
|JNI類型 |Java類型 |描述
|–|–|–|–
|jboolean| boolean |無符號8位整型
|jbyte |byte |無符號8位整型
|jchar| char |無符號16位整型
|jshort |short |有符號16位整型
|jint |int |32位整型
|jlong |long |64位整型
|jfloat |float |32位浮點型
|jdouble |double |64位浮點型
|void |void| 無類型
JNI中的引用類型主要有類, 對象和數組. 他們和Java中的引用類型的對應關係以下:
|JNI類型 |Java類型| 描述|
|–|–|–|
|jobject| Object| Object類型
|jclass |Class |Class類型
|jstring |String |String類型
|jobjeckArray| Object[] |對象數組
|jbooleanArray |boolean[] |boolean數組
|jbyteArray |byte[] |byte數組
|jcharArray |char[] |char數組
|jshortArray |short[] |short數組
|jintArray |int[] |int數組
|jlongArray |long[] |long數組
|jfloatArray| float[] |float數組
|jdoubleArray |double[] |double數組
|jthrowable |Throwable |Throwable
JNI的類型簽名標識了一個特定的Java類型, 這個類型既能夠是類也能夠是方法, 也能夠是數據類型.
類的簽名比較簡單, 它採用L+包名+類型+;的形式, 只須要將其中的.替換爲/便可. 例如java.lang.String, 它的簽名爲Ljava/lang/String;, 末尾的;也是一部分.
基本數據類型的簽名採用一系列大寫字母來表示, 以下:
|Java類型| 簽名| Java類型| 簽名| Java類型| 簽名|
|–|–|–|–|–|–|
|boolean |Z| byte |B |char |C
|short |S |int |I |long |J
|float| F| double |D |void |V
基本數據類型的簽名基本都是單詞的首字母, 可是boolean除外由於B已經被byte佔用, 而long的表示也被Java類簽名佔用. 因此不一樣.
而對象和數組, 對象的簽名就是對象所屬的類簽名, 數組的簽名[+類型簽名例如byte數組. 首先類型爲byte,因此簽名爲B而後由於是數組那麼最終造成的簽名就是[B.例如以下各類對應:
char[] [C float[] [F double[] [D long[] [J String[] [Ljava/lang/String; Object[] [Ljava/lang/Object;
若是是多維數組那麼就根據數組的維度多少來決定[的多少, 例如int[][]那麼就是[[I
方法的簽名爲(參數類型簽名)+返回值類型簽名。
-
方法boolean fun(int a, double b, int[] c). 參數類型的簽名是連在一塊兒, 那麼按照方法的簽名規則就是(ID[I)Z
-
方法:void fun(int a, String s, int[] c), 那麼簽名就是(ILjava/lang/String;[I)V
-
方法:int fun(), 對應簽名()I
-
方法:int fun(float f), 對應簽名(F)I
14.4 JNI調用Java方法的流程
JNI調用java方法的流程是先經過類名找到類, 而後在根據方法名找到方法的id, 最後就能夠調用這個方法了. 若是是調用Java的非靜態方法, 那麼須要構造出類的對象後才能夠調用它。
演示一下調用靜態的方法
1. 首先在java中聲明要被調用的靜態方法. 這裏觸發的時機是一個按鈕的點擊,自行添加
static{
System.loadLibrary(「jni-test」);
}
/**
* 定義一個靜態方法 , 提供給JNI調用
*/
public static void methodCalledByJni(String fromJni){
Log.e(「susu」, 「我是從JNI被調用的消息, JNI返回的值是:」+fromJni );
}
// 定義調用本地方法, 好讓本地方法回調java中的方法
public native void callJNIConvertJavaMethod();
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.btn_jni2java:
// 調用JNI的方法
callJNIConvertJavaMethod();
break;
}
}
2. 在JNI的test.cpp中添加一個c的函數用來處理調用java的邏輯, 並提供一個方法供java代碼調起來觸發. 一共兩個方法。
// 定義調用java中的方法的函數
void callJavaMethod( JNIEnv *env, jobject thiz){
// 先找到要調用的類
jclass clazz = env -> FindClass(「com/szysky/note/androiddevseek_14/MainActivity」);
if (clazz == NULL){
printf(「找不到要調用方法的所屬類」);
return;
}
// 獲取java方法id
// 參數二是調用的方法名, 參數三是方法的簽名
jmethodID id = env -> GetStaticMethodID(clazz, 「methodCalledByJni」, 「(Ljava/lang/String;)V」);
if (id == NULL){
printf(「找不到要調用方法」);
return;
}
jstring msg = env->NewStringUTF(「我是在c中生成的字符串」);
// 開始調用java中的靜態方法
env -> CallStaticVoidMethod(clazz, id, msg);
}
void Java_comszyskynoteandroiddevseek114_MainActivity_callJNIConvertJavaMethod(JNIEnv *env, jobject thiz){
printf(「調用c代碼成功, 立刻回調java中的代碼」);
callJavaMethod(env, thiz);
}
稍微說明一下, 程序首先根據類名com/szysky/note/androiddevseek_14/MainActivity找到類, 而後在根據方法名methodCalledByJni找到方法, 並傳入方法對應簽名(Ljava/lang/String;), 最後經過JNIEnv對象的CallStaticVoidMethod()方法來完成最終調用。
最後只要在Java_comszyskynoteandroiddevseek114_MainActivity_callJNIConvertJavaMethod方法中調用callJavaMethod方法便可.
流程就是–> 按鈕觸發了點擊的onClikc –> 而後Java中會調用JNI的callJNIConvertJavaMethod() –> JNI的callJNIConvertJavaMethod()方法內部會調用具體實現回調Java中的方法callJavaMethod() –> 方法最終經過CallStaticVoidMethod()調用了Java中的methodCalledByJni()來接收一個參數並打印一個log。
結果圖:
生成so庫的文件保存在git中的app/src/main/backup目錄下一個兩個版本代碼, 第一個就是第二小節中的NDK開發代碼, 第二個就是第四小節的代碼就是目前的. 而so庫是最新的, 包含了全部的JNI代碼生成的庫文件。
JNI調用Java的過程和Java中方法的定義有很大關聯, 針對不一樣類型的java方法, JNIEnv提供了不一樣的接口去調用, 更爲細節的部分要去開發中或者去網站去了解更多.
15 Android性能優化
Android設備做爲一種移動設備,無論是內存仍是CPU的性能都受到了必定的限制,也意味着Android程序不可能無限制的使用內存和CPU資源,過多的使用內存容易致使OOM,過多的使用CPU資源容易致使手機變得卡頓甚至無響應(ANR)。這也對開發人員提出了更高的要求。 本章主要介紹一些有效的性能優化方法。主要包括佈局優化、繪製優化、內存泄漏優化、響應速度優化、ListView優化、Bitmap優化、線程優化等;同時還介紹了ANR日誌的分析方法。
Google官方的Android性能優化典範專題短視頻課程是學習Android性能優化極佳的課程,目前已更新到第五季; youku地址
15.1 Android的性能優化方法
15.1.1 佈局優化
佈局優化的思想就是儘可能減小布局文件的層級,這樣繪製界面時工做量就少了,那麼程序的性能天然就高了。
-
刪除無用的控件和層級
-
有選擇的使用性能較低的ViewGroup,若是佈局中既能夠使用Linearlayout也能夠使用RelativeLayout,那就是用LinearLayout,由於RelativeLayout功能比較複雜,它的佈局過程須要花費更多的CPU時間。
有時候經過LinearLayou沒法實現產品效果,須要經過嵌套來完成,這種狀況仍是推薦使用RelativeLayout,由於ViewGroup的嵌套至關於增長了佈局的層級,一樣下降程序性能。 -
採用標籤、標籤和ViewStub
-
include標籤
標籤用於佈局重用,能夠將一個指定的佈局文件加載到當前佈局文件中。只支持android:layout開頭的屬性,固然android:id這個屬性是個特例;若是指定了android:layout這種屬性,那麼要求android:layoutwidth和android:layout_height必須存在,不然android:layout屬性沒法生效。若是指定了id屬性,同時被包含的佈局文件的根元素也指定了id屬性,會以指定的這個id屬性爲準。 -
merge標籤
標籤通常和標籤一塊兒使用從而減小布局的層級。若是當前佈局是一個豎直方向的LinearLayout,這個時候被包含的佈局文件也採用豎直的LinearLayout,那麼顯然被包含的佈局文件中的這個LinearLayout是多餘的,經過標籤就能夠去掉多餘的那一層LinearLayout。 -
ViewStub
ViewStub意義在於按需加載所需的佈局文件,由於實際開發中,有不少佈局文件在正常狀況下是不會現實的,好比網絡異常的界面,這個時候就不必在整個界面初始化的時候將其加載進來,在須要使用的時候再加載會更好。在須要加載ViewStub佈局時:
((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
//或者
View importPanel = ((ViewStub)findViewById(R.id.stub_import)).inflate();
當ViewStub經過setVisibility或者inflate方法加載後,ViewStub就會被它內部的佈局替換掉,ViewStub也就再也不是整個佈局結構的一部分了。
-
15.1.2 繪製優化
View的onDraw方法要避免執行大量的操做;
-
onDraw中不要建立大量的局部對象,由於onDraw方法會被頻繁調用,這樣就會在一瞬間產生大量的臨時對象,不只會佔用過多內存還會致使系統頻繁GC,下降程序執行效率。
-
onDraw也不要作耗時的任務,也不能執行成千上萬的循環操做,儘管每次循環都很輕量級,但大量循環依然十分搶佔CPU的時間片,這會形成View的繪製過程不流暢。根據Google官方給出的標準,View繪製保持在60fps是最佳的,這也就要求每幀的繪製時間不超過16ms(1000/60);因此要儘可能下降onDraw方法的複雜度。
15.1.3 內存泄露優化
內存泄露是最容易犯的錯誤之一,內存泄露優化主要分兩個方面;一方面是開發過程當中避免寫出有內存泄露的代碼,另外一方面是經過一些分析工具如LeakCanary或MAT來找出潛在的內存泄露繼而解決。
-
靜態變量致使的內存泄露
好比Activity內,一靜態Conext引用了當前Activity,因此當前Activity沒法釋放。或者一靜態變量,內部持有了當前Activity,Activity在須要釋放的時候依然沒法釋放。 -
單例模式致使的內存泄露
好比單例模式持有了Activity,並且也沒用解註冊的操做。由於單例模式的生命週期和Application保存一致,生命週期比Activity要長,這樣一來就致使Activity對象沒法及時被釋放。 -
屬性動畫致使的內存泄露
屬性動畫中有一類無限循環的動畫,若是在Activity播放了此類動畫而且沒有在onDestroy中去中止動畫,那麼動畫會一直播放下去,而且這個時候Activity的View會被動畫持有,而View又持有了Activity,最終致使Activity沒法釋放。解決辦法是在Activity的onDrstroy中調用animator.cancel()來中止動畫。
15.1.4 響應速度優化和ANR日誌分析
響應速度優化的核心思想就是避免在主線程中去作耗時操做,將耗時操做放在其餘線程當中去執行。Activity若是5秒沒法響應屏幕觸摸事件或者鍵盤輸入事件就會觸發ANR,而BroadcastReceiver若是10秒還未執行完操做也會出現ANR。
當一個進程發生ANR之後系統會在/data/anr的目錄下建立一個文件traces.txt,經過分析該文件就能定位出ANR的緣由。
經過一個例子來了解如何去分析文件, 首先在onCreate()添加以下代碼, 讓主線程等待一個鎖,而後點擊返回5秒後會出現ANR。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 如下代碼是爲了模擬一個ANR的場景來分析日誌 new Thread(new Runnable() { @Override public void run() { testANR(); } }).start(); SystemClock.sleep(10); initView(); } /** * 如下兩個方法用來模擬出一個稍微很差發現的ANR */ private synchronized void testANR(){ SystemClock.sleep(3000 * 1000); } private synchronized void initView(){}
這樣會出現ANR, 而後導出/data/anr/straces.txt文件. 由於內容比較多隻貼出關鍵部分
DALVIK THREADS (15): "main" prio=5 tid=1 Blocked | group="main" sCount=1 dsCount=0 obj=0x73db0970 self=0xf4306800 | sysTid=19949 nice=0 cgrp=apps sched=0/0 handle=0xf778d160 | state=S schedstat=( 151056979 25055334 199 ) utm=5 stm=9 core=1 HZ=100 | stack=0xff5b2000-0xff5b4000 stackSize=8MB | held mutexes= at com.szysky.note.androiddevseek_15.MainActivity.initView(MainActivity.java:0) - waiting to lock <0x2fbcb3de> (a com.szysky.note.androiddevseek_15.MainActivity) - held by thread 15 at com.szysky.note.androiddevseek_15.MainActivity.onCreate(MainActivity.java:42)
這段能夠看出最後指明瞭ANR發生的位置在ManiActivity的42行. 而且經過上面看出initView方法正在等待一個鎖<0x2fbcb3de>鎖的類型是一個MainActivity對象. 而且這個鎖已經被線程id爲15(tid=15)的線程持有了. 接下來找一下線程15
"Thread-404" prio=5 tid=15 Sleeping | group="main" sCount=1 dsCount=0 obj=0x12c00f80 self=0xeb95bc00 | sysTid=19985 nice=0 cgrp=apps sched=0/0 handle=0xef34be80 | state=S schedstat=( 391248 0 1 ) utm=0 stm=0 core=2 HZ=100 | stack=0xe2bfe000-0xe2c00000 stackSize=1036KB | held mutexes= at java.lang.Thread.sleep!(Native method) - sleeping on <0x2e3896a7> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:1031) - locked <0x2e3896a7> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:985) at android.os.SystemClock.sleep(SystemClock.java:120) at com.szysky.note.androiddevseek_15.MainActivity.testANR(MainActivity.java:50) - locked <0x2fbcb3de> (a com.szysky.note.androiddevseek_15.MainActivity)
tid = 15 就是相關信息如上, 首行已經標出線程的狀態爲Sleeping, 緣由在50行, 就是SystemClock.sleep(3000 * 1000);這句話. 也就是testANR(). 而最後一行也代表了持有的locked<0x2fbcb3de>就是主線程在等待的那個鎖對象.
15.1.5 ListView優化和Bitmap優化
ListView/GridView優化:
1. 採用ViewHolder避免在getView中執行耗時操做
2. 其次經過列表的滑動狀態來控制任務的執行頻率,好比快速滑動時不是和開啓大量異步任務
3. 最後能夠嘗試開啓硬件加速使得ListView的滑動更加流暢。
Bitmap優化:主要是想是根據須要對圖片進行採樣顯示,詳細請參考12章。
15.1.6 線程優化
主要思想就是採用線程池, 避免程序中存在大量的Thread. 線程池能夠重用內部的線程, 避免了線程建立和銷燬的性能開銷. 同時線程池還能有效的控制線程的最大併發數, 避免了大量線程因互相搶佔系統資源從而致使阻塞現象的發生.詳細參考第11章的內容。
15.1.7 一些性能優化的小建議
- 避免建立過多的對象,尤爲在循環、onDraw這類方法中,謹慎建立對象;
- 不要過多的使用枚舉,枚舉佔用的內存空間比整形大。Android 中如何使用annotion替代Enum
- 常量使用static final來修飾;
- 使用一些Android特有的數據結構,好比 SparseArray 和 Pair 等,他們都具備更好的性能;
- 適當的使用軟引用和弱引用;
- 採用內存緩存和磁盤緩存;
- 儘可能採用靜態內部類,這樣能夠避免非靜態內部類隱式持有外部類所致使的內存泄露問題。
15.2 內存泄漏分析工具MAT
MAT全程Eclipse Memory Analyzer, 是一個內存泄漏分析工具. 下載後解壓便可. 下載地址http://www.eclipse.org/mat/downloads.php. 這裏僅簡單說一下. 這個我沒有手動去實踐, 就當個記錄, 由於如今Android Studio能夠直接分析hprof文件.
能夠手動寫一個會形成內存泄漏的代碼, 而後打開DDMS, 而後選中要分析的進程, 而後單擊Dump HPROF file這個按鈕. 等一小段會生成一個文件. 這個文件不能被MAT直接識別. 須要使用Android SDK中的工具進行格式轉換一下.這個工具在platform-conv文件夾下
hprof-conv 要轉換的文件名 輸出的文件名
文件名的簽名有包名.
而後打開MAT經過菜單打開轉換後的這個文件. 這裏經常使用的就有兩個
-
Histogram: 能夠直觀的看出內存中不一樣類型的buffer的數量和佔用內存大小
-
Dominator Tree: 把內存中的對象按照從大到小的順序進行排序, 而且能夠分析對象之間的引用關係, 內存泄漏分析就是經過這個完成的.
分析內存泄漏的時候須要分析Dominator Tree裏面的內存信息, 通常會不直接顯示出來, 能夠按照從大到小的順序去排查一遍. 若是發生了了泄漏, 那麼在泄漏對象處右鍵單擊Path To GC Roots->exclude wake/soft references. 能夠看到最終是什麼對象致使的沒法釋放. 剛纔的操做之因此排除軟引用和弱引用是由於,大部分狀況下這兩種類型均可以被gc回收掉,因此基本也就不會形成內存泄漏.
一樣這裏也能夠使用搜索功能, 假如咱們手動模擬了內存泄漏, 泄漏的對象就是Activity那麼咱們back退出重進循環幾回, 會發現其實不少個Activit對象.
15.3 提升程序的可維護性
提升可讀性
1. 命名規範
2. 代碼之間排版需留出合理的空白來區分不一樣的代碼塊
3. 針對很是關鍵的代碼添加註釋。
代碼的層級性
不要把一段業務邏輯放在一個方法或者一個類中所有實現,要把它分紅幾個子邏輯,而後每一個子邏輯作本身的事情,這樣即顯得代碼層級分明,這樣利於提升程序的可擴展性。
程序的擴展性
因爲不少時候在開發過程當中沒法保證已經作好的需求不在後面的版本發生更改, 所以在寫程序的時候要時刻考慮到擴展的問題, 考慮若是這個邏輯之後發生了改變那麼哪些須要修改, 以及怎樣在之後修改的時候下降工做量, 而面向擴展編程可讓程序具備很好的擴展性.
恰當的使用設計模式能夠提升代碼的可維護性和可擴展性,Android程序容易遇到性能瓶頸,要控制設計的度,不能太牽強,避免過分設計。做者推薦查看《 大話設計模式》 和《 Android源碼設計模式解析和實戰》 這兩本書來學習設計模式。
天天進步一點點!