Android的繪製優化其實能夠分爲兩個部分,即佈局(UI)優化和卡頓優化,而佈局優化的核心問題就是要解決因佈局渲染性能不佳而致使應用卡頓的問題,因此它能夠認爲是卡頓優化的一個子集。對於Android開發來講,寫佈局能夠說是一個比較簡單的工做,可是若是想將寫的每個佈局的渲染性能提高到比較好的程度,要付出的努力是要遠遠超過寫佈局所付出的。因爲佈局優化這一主題包含的內容太多,所以,筆者將它分爲了上、下兩篇,本篇,即爲深刻探索Android佈局優化的上篇。本篇包含的主要內容以下所示:html
說到Android的佈局繪製,那麼咱們就不得不先從佈局的繪製原理開始提及。java
Android的繪製實現主要是藉助CPU與GPU結合刷新機制共同完成的。node
這裏舉兩個栗子來說解一些CPU和GPU的做用:android
那麼,軟件繪製和硬件繪製有什麼區別呢?咱們先看看下圖:git
這裏軟件繪製使用的是Skia庫(一款在低端設備如手機上呈現高質量的 2D 圖形的 跨平臺圖形框架)進行繪製的,而硬件繪製本質上是使用的OpenGl ES接口去利用GPU進行繪製的。OpenGL是一種跨平臺的圖形API,它爲2D/3D圖形處理硬件指定了標準的軟件接口。而OpenGL ES是用於嵌入式設備的,它是OpenGL規範的一種形式,也可稱爲其子集。github
而且,因爲OpenGl ES系統版本的限制,有不少 繪製API 都有相應的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升級到最新的 3.2 版本的時候,還添加了對Vulkan(一套適用於高性能 3D 圖形的低開銷、跨平臺 API)的支持。Vulan做爲下一代圖形API以及OpenGL的繼承者,它的優點在於大幅優化了CPU上圖形驅動相關的性能。shell
Android官方的架構圖以下:json
爲了比較好的描述它們之間的做用,咱們能夠把應用程序圖形渲染過程看成一次繪畫過程,那麼繪畫過程當中 Android 的各個圖形組件的做用分別以下:api
在瞭解完Android圖形系統的總體架構以後,咱們還須要瞭解下Android系統的顯示原理,關於這塊內容能夠參考我以前寫的Android性能優化之繪製優化的Android系統顯示原理一節。緩存
在Android系統的顯示過程當中,雖然咱們利用了GPU的圖形高性能計算的能力,可是從計算Display到經過GPU繪製到Frame Buffer都在UI線程中完成,此時若是能讓GPU在不一樣的線程中進行繪製渲染圖形,那麼繪製將會更加地流暢。
因而,在Android 5.0以後,引入了RenderNode和RenderThread的概念,它們的做用以下:
CPU將數據同步給GPU以後,一般不會阻塞等待RenderThread去利用GPU去渲染完視圖,而是通知結束以後就返回。加入ReaderThread以後的整個顯示調用流程圖以下圖所示:
在Android 6.0以後,其在adb shell dumpsys gxinfo命令中添加了更加詳細的信息,在優化工具一節中我將詳細分析下它的使用。
在Android 7.0以後,對HWUI進行了重構,它是用於2D硬件繪圖並負責硬件加速的主要模塊,其使用了OpenGl ES來進行GPU硬件繪圖。此外,Android 7.0還支持了Vulkan,而且,Vulkan 1.1在Android 被引入。
咱們都知道,硬件加速的原理就是將CPU不擅長的圖形計算轉換成GPU專用指令。
16ms發出VSync信號觸發UI渲染,大多數的Android設備屏幕刷新頻率爲60HZ,若是16ms內不能完成渲染過程,則會產生掉幀現象。
咱們都知道,Android手機屏幕的差別化致使了嚴重的碎片化問題,而且屏幕材質也是用戶比較關注的一個重要因素。
首先,咱們來了解下主流Android屏幕材質,目前主要有兩類:
早在20世紀60年代,隨着半導體集成電路的發展,美國人成功研發出了第一塊液晶顯示屏LCD,而如今大部分最新的高端機使用的都是OLED材質,這是由於相比於LCD屏幕,OLED屏幕在色彩、可彎曲程度、厚度和耗電等方面都有必定的優點。正由於如此,如今主流的全面屏、曲面屏與將來的柔性摺疊屏,使用的幾乎都是 OLED 材質。當前,好的材質,它的成本也必然會比較昂貴。
若是要明白OLED 屏幕和LCD屏幕的區別,須要瞭解它們的運行原理,下面,我將分別進行講解。
屏幕由無數個點組成,而且,每一個點由紅綠藍三個子像素組成,每一個像素點經過調節紅綠藍子像素的顏色配比來顯示不一樣的顏色,最終全部的像素點就會造成具體的畫面。
下面,咱們來看下LCD和OLED的整體結構圖,以下所示:
LCD的發光原理主要在於背光層Back-light,它一般都會由大量的LED背光燈組成以用於顯示白光,以後,爲了顯示出彩色,在其上面加了一層有顏色的薄膜,白色的背光穿透了有顏色的薄膜後就能夠顯示出彩色了。可是,爲了實現調整紅綠藍光的比例,須要在背光層和顏色薄膜之間加入一個控制閥門,即液晶層liquid crystal,它能夠經過改變電壓的大小來控制開合的程度,開合大則光多,開合小則光少。
對於OLED來講,它不須要LCD屏幕的背光層和用於控制出光量的液晶層,它就像一個有着無數個小的彩色燈泡組成的屏幕,只須要給它通電就能發光。
它的液晶層不能徹底關合,若是LCD顯示黑色,會有部分光穿過顏色層,因此LCD的黑色其實是白色和黑色混合而成的灰色。而OLED不同,OLED顯示黑色的時候能夠直接關閉區域的像素點。
此外,因爲背光層的存在,因此LCD顯示器的背光很是容易從屏幕與邊框之間的縫隙泄漏出去,即會產生顯示器漏光現象。
咱們都知道,Android 的 系統碎片化、機型以及屏幕尺寸碎片化、屏幕分辨率碎片化很是地嚴重。因此,一個好的屏幕適配方案是很重要的。接下來,我將介紹目前主流的屏幕適配方案。
首先,咱們來回顧一下px、dp、dpi、ppi、density等概念:
一般狀況下,咱們只須要使用dp + 自適應佈局(如鴻神的AutoLayout、ConstraintLayout等等)或weight比例佈局便可基本解決碎片化問題,固然,這種方式也存在一些問題,好比dpi和ppi的差別所致使在同一分辨率手機上控件大小的不一樣。
它就是窮舉市面上全部的Android手機的寬高像素值,經過設立一個基準的分辨率,其餘分辨率都根據這個基準分辨率來計算,在不一樣的尺寸文件夾內部,根據該尺寸編寫對應的dimens文件,以下圖所示:
好比以480x320爲基準分辨率:
那麼對於800*480的分辨率的dimens文件來講:
此時,若是UI設計界面使用的就是基準分辨率,那麼咱們就能夠按照設計稿上的尺寸填寫相對應的dimens去引用,而當APP運行在不一樣分辨率的手機中時,系統會根據這些dimens去引用該分辨率對應的文件夾下面去尋找對應的值。可是這個方案由一個缺點,就是沒法作到向下兼容去使用更小的dimens,好比說800x480的手機就必定要找到800x480的限定符,不然就只能用統一默認的dimens文件了。
因寬高限定符方案的啓發,鴻神出品了一款能使用UI適配更加開發高效和適配精準的項目。
基本使用步驟以下:
第一步:在你的項目的AndroidManifest中註明你的設計稿的尺寸:
<meta-data android:name="design_width" android:value="768">
</meta-data>
<meta-data android:name="design_height" android:value="1280">
</meta-data>
複製代碼
第二步:讓你的Activity繼承自AutoLayoutActivity。若是你不但願繼承AutoLayoutActivity,能夠在編寫佈局文件時,直接使用AutoLinearLayout、Auto***等適配佈局便可。
接下來,直接在佈局文件裏面使用具體的像素值就能夠了,由於在APP運行時,AndroidAutoLayout會幫助咱們根據不一樣手機的具體尺寸按比例伸縮。
AndroidAutoLayout在寬高限定符適配的基礎上,解決了其dimens不能向下兼容的問題,可是它在運行時會在onMeasure裏面對dimens去作變換,因此對於自定義控件或者某些特定的控件須要進行單獨適配;而且,整個UI的適配過程都是由框架完成的,之後想替換成別的UI適配方案成本會比較高,並且,不幸的是,項目已經中止維護了。
smallestWidth即最小寬度,系統會根據當前設備屏幕的 最小寬度 來匹配 values-swdp。
咱們都知道,移動設備都是容許屏幕能夠旋轉的,當屏幕旋轉時,屏幕的高寬就會互換,加上 最小 這兩個字,是由於這個方案是不區分屏幕方向的,它只會把屏幕的高度和寬度中值最小的一方認爲是 最小寬度。
而且它跟寬高限定符適配原理上是同樣,都是系統經過特定的規則來選擇對應的文件。它與AndroidAutoLayout同樣,一樣解決了其dimens不能向下兼容的問題,若是該屏幕的最小寬度是360dp,可是項目中沒有values-sw360dp文件夾的話,它就可能找到values-sw320dp這個文件夾,其尺寸規則命名以下圖所示:
假如加入咱們的設計稿的像素寬度是375,那麼其對應的values-sw360dp和values-sw400dp寬度以下所示:
smallestWidth的適配機制由系統保證,咱們只須要針對這套規則生成對應的資源文件便可,即便對應的smallestWidth值沒有找到徹底對應的資源文件,它也能向下兼容,尋找最接近的資源文件。雖然多個dimens文件可能致使apk變大,可是其增長大小範圍也只是在300kb-800kb這個區間,這仍是能夠接受的。這套方案惟一的變數就是選擇須要適配哪些最小寬度限定符的文件,若是您生成的 values-swdp 與設備實際的 最小寬度 差異不大,那偏差也就在能接受的範圍內,若是差異很大,那效果就會不好。最後,總結一下這套方案的優缺點:
優勢:
插件地址爲自動生成values-sw的項目代碼。生成須要的values-swdp文件夾的步驟以下:
缺點:
若是想讓屏幕寬度隨着屏幕的旋轉而作出改變該怎麼辦呢?
此時根據 values-wdp (去掉 sw 中的 s) 去生成一套資源文件便可。
若是想區分屏幕的方向來作適配該怎麼辦呢?
去根據 屏幕方向限定符 生成一套資源文件,後綴加上 -land 或 -port 便可,如:values-sw360dp-land (最小寬度 360 dp 橫向),values-sw400dp-port (最小寬度 720 dp 縱向)。
注意:
若是UI設計上明顯更適合使用wrap_content,match_parent,layout_weight等,咱們就要絕不猶豫的使用,畢竟,上述都是僅僅針對不得不使用固定寬高的狀況,我相信基礎的UI適配知識大部分開發者仍是具有的。若是不具有的話,請看下方:
它的原理是根據屏幕的寬度或高度動態調整每一個設備的 density (每 dp 佔當前設備屏幕多少像素),經過修改density值的方式,強行把全部不一樣尺寸分辨率的手機的寬度dp值改爲一個統一的值,這樣就能夠解決全部的適配問題。其對應的重要公式以下:
當前設備屏幕總寬度(單位爲像素)/ 設計圖總寬度(單位爲 dp) = density
複製代碼
今日頭條適配方案默認項目中只能以高或寬中的一個做爲基準來進行適配,並不像 AndroidAutoLayout 同樣,高以高爲基準,寬以寬爲基準,來同時進行適配,爲何?
由於,如今中國大部分市面上的 Android 設備的屏幕高寬比都不一致,特別是如今的全面屏、劉海屏、彈性摺疊屏,使這個問題更加嚴重,不一樣廠商推出的手機的屏幕高寬比均可能不一致。因此,咱們只能以高或寬其中的一個做爲基準進行適配,以此避免佈局在高寬比不一致的屏幕上出現變形。
它有如下優點:
它的缺點以下所示:
注意:
千萬不要在此方案上使用smallestWidth適配方案中直接填寫設計圖上標註的 px 值的作法,這樣會使項目強耦合於這個方案,後續切換其它方案都不得不將全部的 layout 文件都改一遍。
這裏推薦一下JessYanCoding的AndroidAutoSize項目,用法以下:
一、首先在項目的build.gradle中添加該庫的依賴:
implementation 'me.jessyan:autosize:1.1.2'
複製代碼
二、接着 AndroidManifest 中填寫全局設計圖尺寸 (單位 dp),若是使用副單位,則能夠直接填寫像素尺寸,不須要再將像素轉化爲 dp:
<manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest>
複製代碼
爲何只需在AndroidManifest.xml 中填寫一下 meta-data 標籤就可實現自動運行?
在 App 啓動時,系統會在 App 的主進程中自動實例化聲明的 ContentProvider,並調用它的 onCreate 方法,執行時機比 Application#onCreate 還靠前,能夠作一些初始化的工做,這個時候咱們就能夠利用它的 onCreate 方法在其中啓動框架。若是項目使用了多進程,調用Application#onCreate 中調用下 ContentProvider#query 就可以使用 ContentProvider 在當前進程中進行實例化。
上述介紹的全部方案並無哪個是十分完美的,但咱們能清晰的認識到不一樣方案的優缺點,並將它們的優勢相結合,這樣才能應付更加複雜的開發需求,創造出最卓越的產品。好比SmallestWidth 限定符適配方案 主打的是穩定性,在運行過程當中極少會出現安全隱患,適配範圍也可控,不會產生其餘未知的影響,而 今日頭條適配方案 主打的是下降開發成本、提升開發效率,使用上更靈活,也能知足更多的擴展需求。因此,具體狀況具體分析,到底選擇哪個屏幕適配方案仍是須要去根據咱們項目自身的需求去選擇。
早在深刻探索Android啓動速度優化一文中咱們就瞭解過Systrace的使用、原理及它做爲啓動速度分析的用法。而它其實主要是用來分析繪製性能方面的問題。下面我就詳細介紹下Systrace做爲繪製優化工具備哪些必須關注的點。
首先,先在左邊欄選中咱們當前的應用進程,在應用進程一欄下面有一欄Frames,咱們能夠看到有綠、黃、紅三種不一樣的小圓圈,以下圖所示:
圖中每個小圓圈表明着當前幀的狀態,大體的對應關係以下:
而且,選中其中某一幀,咱們還能夠在視圖最下方的詳情框看到該幀對應的相關的Alerts報警信息,以幫助咱們去排查問題;此外,若是是大於等於Android 5.0的設備(即API Level21),建立幀的工做工做分爲UI線程和render線程。而在Android 5.0以前的版本中,建立幀的全部工做都是在UI線程上完成的。接下來,咱們看看該幀對應的詳情圖,以下所示:
對應到此幀,咱們發現這裏可能有兩個繪製問題:Bitmap過大、佈局嵌套層級過多致使的measure和layout次數過多,這就須要咱們去在項目中找到該幀對應的Bitmap進行相應的優化,針對佈局嵌套層級過多的問題去選擇更高效的佈局方式,這塊後面咱們會詳細介紹。
此外,Systrace的顯示界面還在在右邊側欄提供了一欄Alert框去顯示出它所檢測出全部可能有繪製性能問題的地方及對應的數量,以下圖所示:
在這裏,咱們能夠將Alert框看作是一個是待修復的Bug列表,一般一個區域的改進能夠消除應用程序中的全部類中該類型的警報,因此,不要爲這裏的警報數量所擔心。
Layout Inspector是AndroidStudio自帶的工具,它的主要做用就是用來查看視圖層級結構的。
具體的操做路徑爲:
點擊Tools工具欄 ->第三欄的Layout Inspector -> 選中當前的進程
複製代碼
下面爲操做以後打開的Awesome-WanAndroid首頁圖,以下所示:
其中,最右側的View Tree就是用來查看視圖的層級結構的,很是方便,這是它最主要的功能,中間的是一個屏幕截圖,最右邊的是一個屬性表格,好比我在截圖中選中某一個TextView(Kotlin/入門及知識點一欄),在屬性表格的text中就能夠顯示相關的信息,以下圖所示:
Choreographer是用來獲取FPS的,而且能夠用於線上使用,具有實時性,可是僅能在Api 16以後使用,具體的調用代碼以下:
Choreographer.getInstance().postFrameCallback();
複製代碼
使用Choreographer獲取FPS的完整代碼以下所示:
private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
* 單次計算FPS使用160毫秒
*/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/**
* 設置計算fps的單位時間間隔1000ms,即fps/s
*/
private static final long MAX_INTERVAL = 1000L;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
// log輸出fps
LogUtils.i("當前實時fps值爲: " + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
複製代碼
經過以上方式咱們就能夠實現實時獲取應用的界面的FPS了。
Tracer for OpenGL ES 是 Android 4.1 新增長的工具,它可逐幀、逐函數的記錄 App 使用 OpenGL ES 的繪製過程,而且,它能夠記錄每一個 OpenGL 函數調用的消耗時間。當使用Systrace還找不到渲染問題時,就能夠去嘗試使用它。
而GAPID是 Android Studio 3.1 推出的工具,能夠認爲是Tracer for OpenGL ES的進化版,它不只實現了跨平臺,並且支持Vulkan與回放。因爲它們主要是用於OpenGL相關開發的使用,這裏我就很少介紹了。
在自動化測試中,咱們一般但願經過執行性能測試的自動化腳原本進行線下的自動化檢測,那麼,有哪些命令能夠用於測量UI渲染的性能呢?
咱們都知道,dumpsys是一款輸出有關係統服務狀態信息的Android工具,利用它咱們能夠獲取當前設備的UI渲染性能信息,目前經常使用的有以下兩種命令:
gfxinfo的主要做用是輸出各階段發生的動畫與幀相關的信息,命令格式以下:
adb shell dumpsys gfxinfo <PackageName>
複製代碼
這裏我以Awesome-WanAndroid項目爲例,輸出其對應的gfxinfo信息以下所示:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid
Applications Graphics Acceleration Info:
Uptime: 549887348 Realtime: 549887348
** Graphics info for pid 1722 [json.chao.com.wanandroid] **
Stats since: 549356564232951ns
Total frames rendered: 5210
Janky frames: 193 (3.70%)
50th percentile: 5ms
90th percentile: 9ms
95th percentile: 13ms
99th percentile: 34ms
Number Missed Vsync: 31
Number High input latency: 0
Number Slow UI thread: 153
Number Slow bitmap uploads: 6
Number Slow issue draw commands: 51
HISTOGRAM: 5ms=4254 6ms=131 7ms=144 8ms=87 9ms=80 10ms=83 11ms=108 12ms=57 13ms=29 14ms=17 15ms=17 16ms=14 17ms=20 18ms=15 19ms=15 20ms=17 21ms=9 22ms=14 23ms=8 24ms=9 25ms=4 26ms=5 27ms=4 28ms=4 29ms=1 30ms=2 31ms=4 32ms=3 34ms=6 36ms=5 38ms=7 40ms=8 42ms=0 44ms=3 46ms=3 48ms=5 53ms=2 57ms=0 61ms=3 65ms=0 69ms=1 73ms=1 77ms=0 81ms=0 85ms=0 89ms=1 93ms=1 97ms=0 101ms=0 105ms=0 109ms=0 113ms=1 117ms=0 121ms=0 125ms=0 129ms=0 133ms=0 150ms=2 200ms=0 250ms=2 300ms=1 350ms=1 400ms=0 450ms=1 500ms=0 550ms=1 600ms=0 650ms=0 700ms=0 750ms=0 800ms=0 850ms=0 900ms=0 950ms=0 1000ms=0 1050ms=0 1100ms=0 1150ms=0 1200ms=0 1250ms=0 1300ms=0 1350ms=0 1400ms=0 1450ms=0 1500ms=0 1550ms=0 1600ms=0 1650ms=0 1700ms=0 1750ms=0 1800ms=0 1850ms=0 1900ms=0 1950ms=0 2000ms=0 2050ms=0 2100ms=0 2150ms=0 2200ms=0 2250ms=0 2300ms=0 2350ms=0 2400ms=0 2450ms=0 2500ms=0 2550ms=0 2600ms=0 2650ms=0 2700ms=0 2750ms=0 2800ms=0 2850ms=0 2900ms=0 2950ms=0 3000ms=0 3050ms=0 3100ms=0 3150ms=0 3200ms=0 3250ms=0 3300ms=0 3350ms=0 3400ms=0 3450ms=0 3500ms=0 3550ms=0 3600ms=0 3650ms=0 3700ms=0 3750ms=0 3800ms=0 3850ms=0 3900ms=0 3950ms=0 4000ms=0 4050ms=0 4100ms=0 4150ms=0 4200ms=0 4250ms=0 4300ms=0 4350ms=0 4400ms=0 4450ms=0 4500ms=0 4550ms=0 4600ms=0 4650ms=0 4700ms=0 4750ms=0 4800ms=0 4850ms=0 4900ms=0 4950ms=0
Caches:
Current memory usage / total memory usage (bytes):
TextureCache 5087048 / 59097600
Layers total 0 (numLayers = 0)
RenderBufferCache 0 / 4924800
GradientCache 20480 / 1048576
PathCache 0 / 9849600
TessellationCache 0 / 1048576
TextDropShadowCache 0 / 4924800
PatchCache 0 / 131072
FontRenderer A8 184219 / 1478656
A8 texture 0 184219 / 1478656
FontRenderer RGBA 0 / 0
FontRenderer total 184219 / 1478656
Other:
FboCache 0 / 0
Total memory usage:
6586184 bytes, 6.28 MB
Pipeline=FrameBuilder
Profile data in ms:
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity/android.view.ViewRootImpl@4a2142e (visibility=8)
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf (visibility=8)
View hierarchy:
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity/android.view.ViewRootImpl@4a2142e
151 views, 154.02 kB of display lists
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf
19 views, 18.70 kB of display lists
Total ViewRootImpl: 2
Total Views: 170
Total DisplayList: 172.73 kB
複製代碼
下面,我將對其中的關鍵信息進行分析。
幀的聚合分析數據
開始的一欄是統計的當前界面全部幀的聚合分析數據,主要做用是綜合查看App的渲染性能以及幀的穩定性。
後續的log數據代表了不一樣組件的緩存佔用信息,幀的創建路徑信息以及總覽信息等等,參考意義不大。
能夠看到,上述的數據只能讓咱們整體感覺到繪製性能的好壞,並不能去定位具體幀的問題,那麼,還有更好的方式去獲取具體幀的信息嗎?
添加framestats去獲取最後120幀的詳細信息
該命令的格式以下:
adb shell dumpsys gfxinfo <PackageName> framestats
複製代碼
這裏仍是以Awesome-WanAndroid項目爲例,輸出項目標籤頁的幀詳細信息:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid framestats
Applications Graphics Acceleration Info:
Uptime: 603118462 Realtime: 603118462
...
Window: json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity
Stats since: 603011709157414ns
Total frames rendered: 3295
Janky frames: 117 (3.55%)
50th percentile: 5ms
90th percentile: 9ms
95th percentile: 14ms
99th percentile: 32ms
Number Missed Vsync: 17
Number High input latency: 3
Number Slow UI thread: 97
Number Slow bitmap uploads: 13
Number Slow issue draw commands: 20
HISTOGRAM: 5ms=2710 6ms=75 7ms=81 8ms=70...
---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,603111579233508,603111579233508,9223372036854775807,0,603111580203105,603111580207688,603111580417688,603111580651698,603111580981282,603111581033157,603111581263417,603111583942011,603111584638678,1590000,259000,
0,603111595904553,603111595904553,9223372036854775807,0,603111596650344,603111596692428,603111596828678,603111597073261,603111597301386,603111597362376,603111597600292,603111600584667,603111601288261,1838000,278000,
...,
---PROFILEDATA---
...
複製代碼
這裏咱們只需關注其中的PROFILEDATA一欄,由於它代表了最近120幀每一個幀的狀態信息。
由於其中的數據是以csv格式顯示的,咱們將PROFILEDATA中的數據所有拷貝過來,而後放入一個txt文件中,接着,把.txt後綴改成.csv,使用WPS表格工具打開,以下圖所示:
從上圖中,咱們看到輸出的第一行是對應的輸出數據列的格式,下面我將詳細進行分析。
Flags:
IntendedVsync:
Vsync:
OldestInputEvent:
NewestInputEvent:
HandleInputStart:
AnimationStart:
PerformTraversalsStart:
DrawStart:
SyncQueued:
SyncStart:
IssueDrawCommandsStart:
SwapBuffers:
FrameCompleted:
綜上,咱們能夠利用這些數據計算獲取咱們在自動化測試中想關注的因素,好比幀耗時、該幀調用View.draw方法所消耗的時間。framestats和幀耗時信息等通常2s收集一次,即一次120幀。爲了精確控制收集數據的時間窗口,如將數據限制爲特定的動畫,能夠重置計數器,從新聚合統計的信息,對應命令以下:
adb shell dumpsys gfxinfo <PackageName> reset
複製代碼
咱們都知道,在Android 4.1之後,系統使用了三級緩衝機制,即此時有三個Graphic Buffer,那麼如何查看每一個Graphic Buffer佔用的內存呢?
答案是使用SurfaceFlinger,命令以下所示:
adb shell dumpsys SurfaceFlinger
複製代碼
輸出的結果很是多,由於包含不少系統應用和界面的相關信息,這裏咱們僅過濾出Awesome-WanAndroid應用對應的信息:
+ Layer 0x7f5a92f000 (json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0)
layerStack= 0, z= 21050, pos=(0,0), size=(1080,2280), crop=( 0, 0,1080,2280), finalCrop=( 0, 0, -1, -1), isOpaque=1, invalidate=0, dataspace=(deprecated) sRGB Linear Full range, pixelformat=RGBA_8888 alpha=0.000, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0x7f5dc23600
format= 1, activeBuffer=[1080x2280:1088, 1], queued-frames=0, mRefreshPending=0
mTexName=386 mCurrentTexture=0
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
- BufferQueue mMaxAcquiredBufferCount=1 mMaxDequeuedBufferCount=2
mDequeueBufferCannotBlock=0 mAsyncMode=0
default-size=[1080x2280] default-format=1 transform-hint=00 frame-counter=51
FIFO(0):
Slots:
// 序號 // 代表是否使用的狀態 // 對象地址 // 當前負責第幾幀 // 手機屏幕分辨率大小
>[00:0x7f5e05a5c0] state=ACQUIRED 0x7f5b1ca580 frame=51 [1080x2280:1088, 1]
[02:0x7f5e05a860] state=FREE 0x7f5b1ca880 frame=49 [1080x2280:1088, 1]
[01:0x7f5e05a780] state=FREE 0x7f5b052a00 frame=50 [1080x2280:1088, 1]
複製代碼
在Slots中,顯示的是緩衝區相關的信息,能夠看到,此時App使用的是00號緩衝區,即第一個緩衝區。
接着,在SurfaceFlinger命令輸出log的最下方有一欄Allocated buffers,這這裏可使用當前緩衝區對應的對象地址去查詢其佔用的內存大小。具體對應到咱們這裏的是0x7f5b1ca580,匹配到的結果以下所示:
0x7f5b052a00: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
0x7f5b1ca580: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
0x7f5b1ca880: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
複製代碼
能夠看到,這裏每個Graphic Buffer都佔用了9MB多的內存,一般分辨率越大,單個Graphic Buffer佔用的內存就越多,如1080 x 1920的手機屏幕,通常佔用8160kb的內存大小。此外,若是應用使用了其它的Surface,如SurfaceView或TextureView(二者通常用在opengl進行圖像處理或視頻處理的過程當中),這個值會更大。若是當App退到後臺,系統就會將這部份內存回收。
瞭解了經常使用佈局優化經常使用的工具與命令以後,咱們就應該開始着手進行優化了,但在開始以前,咱們還得對Android的佈局加載原理有比較深刻的瞭解。
知其然知其因此然,不只要明白在平時開發過程當中是怎樣對佈局API進行調用,還要知道它內部的實現原理是什麼。明白具體的實現原理與流程以後,咱們可能會發現更多可優化的點。
咱們都知道,Android的佈局都是經過setContentView()這個方法進行設置的,那麼它的內部確定實現了佈局的加載,接下來,咱們就詳細分析下它內部的實現原理與流程。
以Awesome-WanAndroid項目爲例,咱們在通用Activity基類的onCreate方法中進行了佈局的設置:
setContentView(getLayoutId());
複製代碼
點進去,發現是調用了AppCompatActivity的setContentView方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
複製代碼
這裏的setContentView實際上是AppCompatDelegate這個代理類的抽象方法:
/**
* Should be called instead of {@link Activity#setContentView(int)}}
*/
public abstract void setContentView(@LayoutRes int resId);
複製代碼
在這個抽象方法的左邊,會有一個綠色的小圓圈,點擊它就能夠查看到對應的實現類與方法,這裏的實現類是AppCompatDelegateImplV9,實現方法以下所示:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
複製代碼
setContentView方法中主要是獲取到了content父佈局,移除其內部全部視圖以後並最終調用了LayoutInflater對象的inflate去加載對應的佈局。接下來,咱們關注inflate內部的實現:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
複製代碼
這裏只是調用了inflate另外一個的重載方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製代碼
在註釋1處,經過Resources的getLayout方法獲取到了一個XmlResourceParser對象,繼續跟蹤下getLayout方法:
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
複製代碼
這裏繼續調用了loadXmlResourceParser方法,注意第二個參數傳入的爲layout,說明此時加載的是一個Xml資源佈局解析器。咱們繼續跟蹤loadXmlResourceParse方法:
@NonNull
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
// 1
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
複製代碼
在註釋1處,若是值類型爲字符串的話,則調用了ResourcesImpl實例的loadXmlResourceParser方法。咱們首先看看這個方法的註釋:
/**
* Loads an XML parser for the specified file.
*
* @param file the path for the XML file to parse
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
...
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
...
return block.newParser();
...
}
複製代碼
註釋的意思說明了這個方法是用於加載指定文件的Xml解析器,這裏咱們之間查看關鍵的mAssets.openXmlBlockAsset方法,這裏的mAssets對象是AssetManager類型的,看看AssetManager實例的openXmlBlockAsset方法作了什麼處理:
/**
* {@hide}
* Retrieve a non-asset as a compiled XML file. Not for use by
* applications.
*
* @param cookie Identifier of the package to be opened.
* @param fileName Name of the asset to retrieve.
*/
/*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
throws IOException {
synchronized (this) {
if (!mOpen) {
throw new RuntimeException("Assetmanager has been closed");
}
// 1
long xmlBlock = openXmlAssetNative(cookie, fileName);
if (xmlBlock != 0) {
XmlBlock res = new XmlBlock(this, xmlBlock);
incRefsLocked(res.hashCode());
return res;
}
}
throw new FileNotFoundException("Asset XML file: " + fileName);
}
複製代碼
能夠看到,最終是調用了註釋1處的openXmlAssetNative方法,這是定義在AssetManager中的一個Native方法:
private native final long openXmlAssetNative(int cookie, String fileName);
複製代碼
與此同時,咱們能夠猜到讀取Xml文件確定是經過IO流的方式進行的,而openXmlBlockAsset方法後拋出的IOException異常也驗證了咱們的想法。由於涉及到IO流的讀取,因此這裏是Android佈局加載流程一個耗時點 ,也有多是咱們後續優化的一個方向。
分析完Resources實例的getLayout方法的實現以後,咱們繼續跟蹤inflate方法的註釋2處:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製代碼
infalte的實現代碼以下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
...
// 1
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 2
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
...
}
...
}
...
}
複製代碼
能夠看到,infalte內部是經過XmlPull解析的方式對佈局的每個節點進行建立對應的視圖的。首先,在註釋1處會判斷節點是不是merge標籤,若是是,則對merge標籤進行校驗,若是merge節點不是當前佈局的父節點,則拋出異常。而後,在註釋2處,經過createViewFromTag方法去根據每個標籤建立對應的View視圖。咱們繼續跟蹤下createViewFromTag方法的實現:
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
複製代碼
在createViewFromTag方法中,首先會判斷mFactory2是否存在,存在就會使用mFactory2的onCreateView方法區建立視圖,不然就會調用mFactory的onCreateView方法,接下來,若是此時的tag是一個Fragment,則會調用mPrivateFactory的onCreateView方法,不然的話,最終都會調用LayoutInflater實例的createView方法:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 1
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// 2
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
...
// 3
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
...
}
複製代碼
LayoutInflater的createView方法中,首先,在註釋1處,使用類加載器建立了對應的Class實例,而後在註釋2處根據Class實例獲取到了對應的構造器實例,並最終在註釋3處經過構造器實例constructor的newInstance方法建立了對應的View對象。能夠看到,在視圖節點的建立過程當中採用到了反射,咱們都知道反射是比較耗性能的,過多的反射可能會致使佈局加載過程變慢,這個點多是後續優化的一個方向。
最後,咱們來總結下Android中的佈局加載流程:
從以上分析可知,在Android的佈局加載流程中,性能瓶頸主要存在兩個地方:
在前面分析的View的建立過程當中,咱們明白系統會優先使用Factory2和Factory去建立對應的View,那麼它們到底是幹什麼的呢?
其實LayoutInflater.Factory是layoutInflater中建立View的一個Hook,Hook即掛鉤,咱們能夠利用它在建立View的過程當中加入一些日誌或進行其它更高級的定製化處理:好比能夠全局替換自定義的TextView等等。
接下來,咱們查看下Factory2的實現:
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
複製代碼
能夠看到,Factory2是直接繼承於Factory,繼續跟蹤下Factory的源碼:
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
複製代碼
onCreateView方法中的第一個參數就是指的tag名字,好比TextView等等,咱們還注意到Factory2比Factory的onCreateView方法多一個parent的參數,這是當前建立的View的父View。看來,Factory2比Factory功能要更強大一些。
最後,咱們總結下Factory與Factory2的區別:
若是要獲取每一個界面的加載耗時,咱們就必需在setContentView方法先後進行手動埋點。可是它有以下缺點:
關於AOP的使用,我在《深刻探索Android啓動速度優化》一文的AOP(Aspect Oriented Programming)打點部分已經詳細講解過了,這裏就再也不贅述,還不瞭解的同窗能夠點擊上面的連接先去學習下AOP的使用。
咱們要使用AOP去獲取界面佈局的耗時,那麼咱們的切入點就是setContentView方法,聲明一個@Aspect註解的PerformanceAop類,而後,咱們就能夠在裏面實現對setContentView進行切面的方法,以下所示:
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
複製代碼
爲了獲取方法的耗時,咱們必須使用@Around註解,這樣第一個參數ProceedingJoinPoint就能夠提供proceed方法去執行咱們的setContentView方法,在此方法的先後就能夠獲取setContentView方法的耗時。後面的execution代表了在setContentView方法執行內部去調用咱們寫好的getSetContentViewTime方法,後面括號內的*是通配符,表示匹配任何Activity的setContentView方法,而且方法參數的個數和類型不作限定。
完成AOP獲取界面佈局耗時的方法以後,重裝應用,打開幾個Activity界面,就能夠看到以下的界面佈局加載耗時日誌:
2020-01-01 12:20:17.605 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 174
2020-01-01 12:20:58.010 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 13
2020-01-01 12:21:27.058 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 44
2020-01-01 12:21:31.128 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 61
2020-01-01 12:23:09.805 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 22
複製代碼
能夠看到,Awesome-WanAndroid項目裏面各個界面的加載耗時通常都在幾十毫秒做用,加載慢的界面可能會達到100多ms,固然,不一樣手機的配置不同,可是,這足夠讓咱們發現哪些界面佈局的加載比較慢。
上面咱們使用了AOP的方式監控了Activity的佈局加載耗時,那麼,若是咱們須要監控每個控件的加載耗時,該怎麼實現呢?
答案是使用LayoutInflater.Factory2,咱們在基類Activity的onCreate方法中直接使用LayoutInflaterCompat.setFactory2方法對Factory2的onCreateView方法進行重寫,代碼以下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局監控Activity界面每個控件的加載耗時,
// 也能夠作全局的自定義控件替換處理,好比:將TextView全局替換爲自定義的TextView。
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定義TextView
}
long time = System.currentTimeMillis();
// 1
View view = getDelegate().createView(parent, name, context, attrs);
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
// 二、setFactory2方法需在super.onCreate方法前調用,不然無效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
unBinder = ButterKnife.bind(this);
mActivity = this;
ActivityCollector.getInstance().addActivity(this);
onViewCreated();
initToolbar();
initEventAndData();
}
複製代碼
這樣咱們就實現了利用LayoutInflaterCompat.Factory2**全局監控Activity界面每個控件加載耗時的處理,後續咱們能夠將這些數據上傳到咱們本身的APM服務端,做爲監控數據能夠分析出哪些控件加載比較耗時。**固然,這裏咱們也能夠作全局的自定義控件替換處理,好比在上述代碼中,咱們能夠將TextView全局替換爲自定義的TextView。
而後,咱們注意到這裏咱們使用getDelegate().createView方法來建立對應的View實例,跟蹤進去發現這裏的createView是一個抽象方法:
public abstract View createView(@Nullable View parent, String name, @NonNull Context context,
@NonNull AttributeSet attrs);
複製代碼
它對應的實現方法爲AppCompatDelegateImplV9對象的createView方法,代碼以下所示:
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
複製代碼
這裏最終又調用了AppCompatViewInflater對象的createView方法:
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
複製代碼
在AppCompatViewInflater對象的createView方法中系統根據不一樣的tag名字建立出了對應的AppCompat兼容控件。看到這裏,咱們明白了Android系統是使用了LayoutInflater的Factor2/Factory結合了AppCompat兼容類來進行高級版本控件的兼容適配的。
接下來,咱們注意到註釋1處,setFactory2方法需在super.onCreate方法前調用,不然無效,這是爲何呢?
這裏能夠先大膽猜想一下,多是由於在super.onCreate()方法中就須要將Factory2實例存儲到內存中以便後續使用。下面,咱們就跟蹤一下super.onCreate()的源碼,看看是否如咱們所假設的同樣。AppCompatActivity的onCreate方法以下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
複製代碼
第一行的delegate實例的installViewFactory()方法就吸引了咱們的注意,由於它包含了一個敏感的關鍵字「Factory「,這裏咱們繼續跟蹤進installViewFactory()方法:
public abstract void installViewFactory();
複製代碼
這裏一個是抽象方法,點擊左邊綠色圓圈,能夠看到這裏具體的實現類爲AppCompatDelegateImplV9,其實現的installViewFactory()方法以下所示:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
複製代碼
能夠看到,若是咱們在super.onCreate()方法前沒有設置LayoutInflater的Factory2實例的話,這裏就會設置一個默認的Factory2。最後,咱們再來看下默認Factory2的onCreateView方法的實現:
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 一、First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// 二、If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
複製代碼
在註釋1處,咱們首先會嘗試讓Activity的Facotry實例去加載對應的View實例,若是Factory不可以處理它,在註釋2處,就會調用createView方法去建立對應的View,AppCompatDelegateImplV9類的createView方法的實現上面咱們已經分析過了,此處就再也不贅述了。
在本篇文章中,咱們主要對Android的佈局繪製以及加載原理、優化工具、全局監控佈局和控件的加載耗時進行了全面的講解,這爲你們學習《深刻探索Android佈局優化(下)》打下了良好的基礎。下面,總結一下本篇文章涉及的五大主題:
下篇,咱們將進入佈局優化的實戰環節,敬請期待~
一、國內Top團隊大牛帶你玩轉Android性能分析與優化 第五章 佈局優化
六、騷年你的屏幕適配方式該升級了!-smallestWidth 限定符適配方案
十二、GAPID-Graphics API Debugger
1八、Test UI performance-gfxinfo
1九、使用dumpsys gfxinfo 測UI性能(適用於Android6.0之後)
2五、[Google Flutter 團隊出品] 深刻了解 Flutter 的高性能圖形渲染
若是這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你能夠掃描下面的二維碼,讓我喝一杯咖啡或啤酒。很是感謝您的捐贈。謝謝!
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~