Android布局優化(轉)

轉自

0前言

Android的繪制優化其實可以分為兩個部分,即布局(UI)優化和卡頓優化,而布局優化的核心問題就是要解決因布局渲染性能不佳而導致應用卡頓的問題,所以它可以認為是卡頓優化的一個子集。

對于Android開發來說,寫布局可以說是一個比較簡單的工作,但是如果想將寫的每一個布局的渲染性能提升到比較好的程度,要付出的努力是要遠遠超過寫布局所付出的。由于布局優化這一主題包含的內容太多,因此,筆者將它分為了上、下兩篇,本篇,即為深入探索Android布局優化的上篇。本篇包含的主要內容如下所示:

  1. 繪制原理

  2. 屏幕適配

  3. 優化工具

  4. 布局加載原理

  5. 獲取界面布局耗時

說到Android的布局繪制,那么我們就不得不先從布局的繪制原理開始說起。

1繪制原理

Android的繪制實現主要是借助CPU與GPU結合刷新機制共同完成的。

1、CPU與GPU

  • CPU負責計算顯示內容,包括Measure、Layout、Record、Execute等操作。在UI繪制上的缺陷在于容易顯示重復的視圖組件,這樣不僅帶來重復的計算操作,而且會占用額外的GPU資源。

  • GPU負責柵格化(用于將UI元素繪制到屏幕上,即將UI組件拆分到不同的像素上顯示)。

這里舉兩個栗子來講解一些CPU和GPU的作用:

  • 文字的顯示首先經過CPU換算成紋理,然后再傳給GPU進行渲染。

  • 而圖片的顯示首先是經過CPU的計算,然后加載到內存當中,最后再傳給GPU進行渲染。

那么,軟件繪制和硬件繪制有什么區別呢?我們先看看下圖:

image

這里軟件繪制使用的是Skia庫(一款在低端設備如手機上呈現高質量的 2D 圖形的 跨平臺圖形框架)進行繪制的,而硬件繪制本質上是使用的OpenGl ES接口去利用GPU進行繪制的。

OpenGL是一種跨平臺的圖形API,它為2D/3D圖形處理硬件指定了標準的軟件接口。而OpenGL ES是用于嵌入式設備的,它是OpenGL規范的一種形式,也可稱為其子集。

并且,由于OpenGl ES系統版本的限制,有很多 繪制API 都有相應的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升級到最新的 3.2 版本的時候,還添加了對Vulkan(一套適用于高性能 3D 圖形的低開銷、跨平臺 API)的支持。

Vulan作為下一代圖形API以及OpenGL的繼承者,它的優勢在于大幅優化了CPU上圖形驅動相關的性能。

2、Android 圖形系統的整體架構

Android官方的架構圖如下:

image

為了比較好的描述它們之間的作用,我們可以把應用程序圖形渲染過程當作一次繪畫過程,那么繪畫過程中 Android 的各個圖形組件的作用分別如下:

  • 畫筆:Skia 或者 OpenGL。我們可以用 Skia去繪制 2D 圖形,也可以用 OpenGL 去繪制 2D/3D 圖形。

  • 畫紙:Surface。所有的元素都在 Surface 這張畫紙上進行繪制和渲染。在 Android 中,Window 是 View 的容器,每個窗口都會關聯一個 Surface。而 WindowManager 則負責管理這些窗口,并且把它們的數據傳遞給 SurfaceFlinger。

  • 畫板:Graphic Buffer。Graphic Buffer 緩沖用于應用程序圖形的繪制,在 Android 4.1 之前使用的是雙緩沖機制,而在 Android 4.1 之后使用的是三緩沖機制。

  • 顯示:SurfaceFlinger。它將 WindowManager 提供的所有 Surface,通過硬件合成器 Hardware Composer 合成并輸出到顯示屏。

在了解完Android圖形系統的整體架構之后,我們還需要了解下Android系統的顯示原理,關于這塊內容可以參考我之前寫的Android性能優化之繪制優化的Android系統顯示原理一節。

https://jsonchao.github.io/2019/07/28/Android%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BB%98%E5%88%B6%E4%BC%98%E5%8C%96/

3、RenderThread

在Android系統的顯示過程中,雖然我們利用了GPU的圖形高性能計算的能力,但是從計算Display到通過GPU繪制到Frame Buffer都在UI線程中完成,此時如果能讓GPU在不同的線程中進行繪制渲染圖形,那么繪制將會更加地流暢。

于是,在Android 5.0之后,引入了RenderNode和RenderThread的概念,它們的作用如下:

  • RenderNode:進一步封裝了Display和某些View的屬性。

  • RenderThread:渲染線程,負責執行所有的OpenGl命令,其中的RenderNode保存有渲染幀的所有信息,能在主線程有耗時操作的前提下保證動畫流暢。

CPU將數據同步給GPU之后,通常不會阻塞等待RenderThread去利用GPU去渲染完視圖,而是通知結束之后就返回。加入ReaderThread之后的整個顯示調用流程圖如下圖所示:

image

在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專用指令。

1、其中的OpenGl API調用和Graphic Buffer緩沖區至少會占用幾MB以上的內存,內存消耗較大。

2、有些OpenGl的繪制API還沒有支持,特別是比較低的Android系統版本,并且由于Android每一個版本都會對渲染模塊進行一些重構,導致了在硬件加速繪制過程中會出現一些不可預知的Bug。

如在Android 5.0~7.0機型上出現的libhwui.so崩潰問題,需要使用inline Hook、GOT Hook等native調試手段去進行分析定位,可能的原因是ReaderThread與UI線程的sync同步過程出現了差錯,而這種情況一般都是有多個相同的視圖繪制而導致的,比如View的復用、多個動畫同時播放。

4、刷新機制

16ms發出VSync信號觸發UI渲染,大多數的Android設備屏幕刷新頻率為60HZ,如果16ms內不能完成渲染過程,則會產生掉幀現象。

2優化工具

1、Systrace

早在深入探索Android啟動速度優化一文中我們就了解過Systrace的使用、原理及它作為啟動速度分析的用法。而它其實主要是用來分析繪制性能方面的問題。下面我就詳細介紹下Systrace作為繪制優化工具有哪些必須關注的點。

1、關注Frames

首先,先在左邊欄選中我們當前的應用進程,在應用進程一欄下面有一欄Frames,我們可以看到有綠、黃、紅三種不同的小圓圈,如下圖所示:

image.gif

圖中每一個小圓圈代表著當前幀的狀態,大致的對應關系如下:

  • 正常:綠色。

  • 丟幀:黃色。

  • 嚴重丟幀:紅色。

并且,選中其中某一幀,我們還可以在視圖最下方的詳情框看到該幀對應的相關的Alerts報警信息,以幫助我們去排查問題;

此外,如果是大于等于Android 5.0的設備(即API Level21),創建幀的工作工作分為UI線程和render線程。而在Android 5.0之前的版本中,創建幀的所有工作都是在UI線程上完成的。

接下來,我們看看該幀對應的詳情圖,如下所示:

image

對應到此幀,我們發現這里可能有兩個繪制問題:Bitmap過大、布局嵌套層級過多導致的measure和layout次數過多,這就需要我們去在項目中找到該幀對應的Bitmap進行相應的優化,針對布局嵌套層級過多的問題去選擇更高效的布局方式,這塊后面我們會詳細介紹。

2、關注Alerts欄

此外,Systrace的顯示界面還在在右邊側欄提供了一欄Alert框去顯示出它所檢測出所有可能有繪制性能問題的地方及對應的數量,如下圖所示:

image

在這里,我們可以將Alert框看做是一個是待修復的Bug列表,通常一個區域的改進可以消除應用程序中的所有類中該類型的警報,所以,不要為這里的警報數量所擔憂。

2、Layout Inspector

Layout Inspector是AndroidStudio自帶的工具,它的主要作用就是用來查看視圖層級結構的。

具體的操作路徑為:

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; text-align: left;">點擊Tools工具欄 ->第三欄的Layout Inspector -> 選中當前的進程 </pre>

下面為操作之后打開的Awesome-WanAndroid首頁圖,如下所示:

https://github.com/JsonChao/Awesome-WanAndroid

image

其中,最左側的View Tree就是用來查看視圖的層級結構的,非常方便,這是它最主要的功能,中間的是一個屏幕截圖,最右邊的是一個屬性表格,比如我在截圖中選中某一個TextView(Kotlin/入門及知識點一欄),在屬性表格的text中就可以顯示相關的信息,如下圖所示:

image.gif

3、Choreographer

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了。

但是我們需要排除掉頁面沒有操作的情況,即只在界面存在繪制的時候才做統計。我們可以通過 addOnDrawListener 去監聽界面是否存在繪制行為,代碼如下所示:

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener

當出現丟幀的時候,我們可以獲取應用當前的頁面信息、View 信息和操作路徑上報至 APM后臺,以降低二次排查的難度。

此外,我們將連續丟幀超過 700 毫秒定義為凍幀,也就是連續丟幀 42 幀以上。這時用戶會感受到比較明顯的卡頓現象,因此,我們可以統計更有價值的凍幀率。凍幀率就是計算發生凍幀時間在所有時間的占比。

通過解決應用中發生凍幀的地方我們就可以大大提升應用的流暢度。

4、Tracer for OpenGL ES 與 GAPID(Graphics API Debugger)

Tracer for OpenGL ES 是 Android 4.1 新增加的工具,它可逐幀、逐函數的記錄 App 使用 OpenGL ES 的繪制過程,并且,它可以記錄每個 OpenGL 函數調用的消耗時間。當使用Systrace還找不到渲染問題時,就可以去嘗試使用它。

而GAPID是 Android Studio 3.1 推出的工具,可以認為是Tracer for OpenGL ES的進化版,它不僅實現了跨平臺,而且支持Vulkan與回放。由于它們主要是用于OpenGL相關開發的使用,這里我就不多介紹了。

5、自動化測量 UI 渲染性能的方式

在自動化測試中,我們通常希望通過執行性能測試的自動化腳本來進行線下的自動化檢測,那么,有哪些命令可以用于測量UI渲染的性能呢?

我們都知道,dumpsys是一款輸出有關系統服務狀態信息的Android工具,利用它我們可以獲取當前設備的UI渲染性能信息,目前常用的有如下兩種命令:

1、gfxinfo

gfxinfo的主要作用是輸出各階段發生的動畫與幀相關的信息,命令格式如下:

adb shell dumpsys gfxinfo <PackageName>

這里我以Awesome-WanAndroid項目為例,輸出其對應的gfxinfo信息如下所示:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroidApplications Graphics Acceleration Info:Uptime: 549887348 Realtime: 549887348** Graphics info for pid 1722     [json.chao.com.wanandroid] **Stats since: 549356564232951nsTotal frames rendered: 5210Janky frames: 193 (3.70%)50th percentile: 5ms90th percentile: 9ms95th percentile: 13ms99th percentile: 34msNumber Missed Vsync: 31Number High input latency: 0Number Slow UI thread: 153Number Slow bitmap uploads: 6Number Slow issue draw commands: 51HISTOGRAM: 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=0Caches:Current memory usage / total memory usage (bytes):TextureCache          5087048 / 59097600Layers total          0 (numLayers = 0)RenderBufferCache           0 /  4924800GradientCache           20480 /  1048576PathCache                   0 /  9849600TessellationCache           0 /  1048576TextDropShadowCache         0 /  4924800PatchCache                  0 /   131072FontRenderer A8        184219 /  1478656    A8   texture 0       184219 /  1478656FontRenderer RGBA           0 /        0FontRenderer total     184219 /  1478656Other:FboCache                    0 /        0Total memory usage:6586184 bytes, 6.28 MBPipeline=FrameBuilderProfile 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@4a2142e151 views, 154.02 kB of display listsjson.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf19 views, 18.70 kB of display listsTotal ViewRootImpl: 2Total Views:        170Total DisplayList:  172.73 kB

下面,我將對其中的關鍵信息進行分析。

幀的聚合分析數據

開始的一欄是統計的當前界面所有幀的聚合分析數據,主要作用是綜合查看App的渲染性能以及幀的穩定性。

  • Graphics info for pid 1722 [json.chao.com.wanandroid] -> 說明了當前提供的是Awesome-WanAndroid應用界面的幀信息,對應的進程id為1722。

  • Total frames rendered 5210 -> 本次dump的數據搜集了5210幀的信息。

  • Janky frames: 193 (3.70%) -> 5210幀中有193幀發生了Jank,即單幀耗時時間超過了16ms,卡頓的概率為3.70%。

  • 50th percentile: 5ms -> 所有幀耗時排序后,其中前50%最大的耗時幀的耗時為5ms。

  • 90th percentile: 9ms -> 同上,依次類推。

  • 95th percentile: 13ms -> 同上,依次類推。

  • 99th percentile: 34ms -> 同上,依次類推。

  • Number Missed Vsync: 31 -> 垂直同步失敗的幀數為31。

  • Number High input latency: 0 -> 處理input耗時的幀數為0。

  • Number Slow UI thread: 153 -> 因UI線程的工作而導致耗時的幀數為153。

  • Number Slow bitmap uploads: 6 -> 因bitmap加載導致耗時的幀數為6。

  • Number Slow issue draw commands: 51 -> 因繪制問題導致耗時的幀數為51。

  • HISTOGRAM: 5ms=4254 6ms=131 7ms=144 8ms=87... -> 直方圖數據列表,說明了耗時05ms的幀數為4254,耗時56ms的幀數為131,后續的數據依次類推即可。

后續的log數據表明了不同組件的緩存占用信息,幀的建立路徑信息以及總覽信息等等,參考意義不大。

可以看到,上述的數據只能讓我們總體感受到繪制性能的好壞,并不能去定位具體幀的問題,那么,還有更好的方式去獲取具體幀的信息嗎?

添加framestats去獲取最后120幀的詳細信息

該命令的格式如下:

adb shell dumpsys gfxinfo <PackageName> framestats

這里還是以Awesome-WanAndroid項目為例,輸出項目標簽頁的幀詳細信息:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid framestatsApplications Graphics Acceleration Info:Uptime: 603118462 Realtime: 603118462...Window: json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivityStats since: 603011709157414nsTotal frames rendered: 3295Janky frames: 117 (3.55%)50th percentile: 5ms90th percentile: 9ms95th percentile: 14ms99th percentile: 32msNumber Missed Vsync: 17Number High input latency: 3Number Slow UI thread: 97Number Slow bitmap uploads: 13Number Slow issue draw commands: 20HISTOGRAM: 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表格工具打開,如下圖所示:

image

從上圖中,我們看到輸出的第一行是對應的輸出數據列的格式,下面我將詳細進行分析。

Flags:

  • Flags為0則可計算得出該幀耗時:FrameCompleted - IntendedVsync。

  • Flags為非0則表示繪制時間超過16ms,為異常幀。

IntendedVsync:

  • 幀的預期Vsync時刻,如果預期的Vsync時刻與現實的Vsync時刻不一致,則表明UI線程中有耗時工作導致其無法響應Vsync信號。

Vsync:

  • 花費在Vsync監聽器和幀繪制的時間,比如Choreographer frame回調、動畫、View.getDrawingTime等待。

  • 理解Vsync:Vsync避免了在屏幕刷新時,把數據從后臺緩沖區復制到幀緩沖區所消耗的時間。

OldestInputEvent:

  • 輸入隊列中最舊輸入事件的時間戳,如果沒有輸入事件,則此列數據都為Long.MAX_VALUE。

  • 通常用于framework層開發。

NewestInputEvent:

  • 輸入隊列中最新輸入時間的時間戳,如果沒有輸入事件,則此列數據都為0。

  • 計算App大致的延遲添加時間:FrameCompleted - NewestInputEvent。

  • 通常用于framework層開發。

HandleInputStart:

  • 將輸入事件分發給App對應的時間戳時刻。

  • 用于測量App處理輸入事件的時間:AnimationStart - HandleInputStart。當值大于2ms時,說明程序花費了很長的時間來處理輸入事件,比如View.onTouchEvent等事件。注意在Activity切換或產生點擊事件時此值一般都比較大,此時是可以接受的。

AnimationStart:

  • 運行Choreographer(舞蹈編排者)注冊動畫的時間戳。

  • 用來評估所有運行的所有動畫器(ObjectAnimator、ViewPropertyAnimator、常用轉換器)需要多長時間:AnimationStart - PerformTraversalsStart。當值大于2ms時,請查看此時是否執行的是自定義動畫且動畫是否有耗時操作。

PerformTraversalsStart:

  • 執行布局遞歸遍歷開始的時間戳。

  • 用于獲取measure、layout的時間:DrawStart - PerformTraversalsStart。(注意滾動或動畫期間此值應接近于0)。

DrawStart:

  • draw階段開始的時間戳,它記錄了任何無效視圖的DisplayList的起點。

  • 用于獲取視圖數中所有無效視圖調用View.draw方法所需的時間:SyncStart - DrawStart。

  • 在此過程中,硬件加速模塊中的DisplayList發揮了重要作用,Android系統仍然使用invalidate()調用draw()方法請求屏幕更新和渲染視圖,但是對實際圖形的處理方式有所不同。Android系統并沒有立即執行繪圖命令,而是將它們記錄在DisplayList中,該列表包含視圖層次結構繪圖所需的所有信息。相對于軟件渲染的另一個優化是,Android系統僅需要記錄和更新DispalyList,以顯示被invalidate() 標記為dirty的視圖。只需重新發布先前記錄的Displaylist,即可重新繪制尚未失效的視圖。此時的硬件繪制模型主要包括三個過程:刷新視圖層級、記錄和更新DisplayList、繪制DisplayList。相對于軟件繪制模型的刷新視圖層級、然后直接去繪制視圖層級的兩個步驟,雖然多了一個步驟,但是節省了很多不必要的繪制開銷。

SyncQueued:

  • sync請求發送到RenderThread線程的時間戳。

  • 獲取sync就緒所花費的時間:SyncStart - SyncQueued。如果值大于0.1ms,則說明RenderThread正在忙于處理不同的幀。

SyncStart:

  • 繪圖的sync階段開始的時間戳。

  • IssueDrawCommandsStart - SyncStart > 0.4ms左右則表明有許多新的位圖需要上傳至GPU。

IssueDrawCommandsStart:

  • 硬件渲染器開始GPU發出繪圖命令的時間戳。

  • 用于觀察App此時繪制時消耗了多少GPU:FrameCompleted - IssueDrawCommandsStart。

SwapBuffers:

  • eglSwapBuffers被調用時的時間戳。

  • 通常用于Framework層開發。

FrameCompleted:

  • 當前幀完成繪制的時間戳。

  • 獲取當前幀繪制的總時間:FrameCompleted - IntendedVsync。

綜上,我們可以利用這些數據計算獲取我們在自動化測試中想關注的因素,比如幀耗時、該幀調用View.draw方法所消耗的時間。

framestats和幀耗時信息等一般2s收集一次,即一次120幀。為了精確控制收集數據的時間窗口,如將數據限制為特定的動畫,可以重置計數器,重新聚合統計的信息,對應命令如下:

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; text-align: left;">

adb shell dumpsys gfxinfo <PackageName> reset

</pre>

2、SurfaceFlinger

我們都知道,在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#00x7f5b1ca580: 9690.00 KiB | 1080 (1088) x 2280 |    1 |        1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#00x7f5b1ca880: 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的布局加載原理有比較深入的了解。

image

3布局加載原理

1、為什么要了解Android布局加載原理?

知其然知其所以然,不僅要明白在平時開發過程中是怎樣對布局API進行調用,還要知道它內部的實現原理是什么。

明白具體的實現原理與流程之后,我們可能會發現更多可優化的點。

2、布局加載源碼分析

我們都知道,Android的布局都是通過setContentView()這個方法進行設置的,那么它的內部肯定實現了布局的加載,接下來,我們就詳細分析下它內部的實現原理與流程。

以Awesome-WanAndroid項目為例,我們在通用Activity基類的onCreate方法中進行了布局的設置:

setContentView(getLayoutId());

點進去,發現是調用了AppCompatActivity的setContentView方法:

@Overridepublic 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,實現方法如下所示:

 @Overridepublic 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方法:

@NonNullXmlResourceParser 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 */@NonNullXmlResourceParser 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中的布局加載流程:

  1. 在setContentView方法中,會通過LayoutInflater的inflate方法去加載對應的布局。

  2. inflate方法中首先會調用Resources的getLayout方法去通過IO的方式去加載對應的Xml布局解析器到內存中。

  3. 接著,會通過createViewFromTag根據每一個tag創建具體的View對象。

  4. 它內部主要是按優先順序為Factory2和Factory的onCreatView、createView方法進行View的創建,而createView方法內部采用了構造器反射的方式實現。

從以上分析可知,在Android的布局加載流程中,性能瓶頸主要存在兩個地方:

  1. 布局文件解析中的IO過程。

  2. 創建View對象時的反射過程。

3、LayoutInflater.Factory分析

在前面分析的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 {    public View onCreateView(String name, Context context, AttributeSet attrs);}

onCreateView方法中的第一個參數就是指的tag名字,比如TextView等等,我們還注意到Factory2比Factory的onCreateView方法多一個parent的參數,這是當前創建的View的父View。看來,Factory2比Factory功能要更強大一些。

最后,我們總結下Factory與Factory2的區別:

  • 1、Factory2繼承與Factory。

  • 2、Factory2比Factory的onCreateView方法多一個parent的參數,即當前創建View的父View。

4獲取界面布局耗時

1、常規方式

如果要獲取每個界面的加載耗時,我們就必需在setContentView方法前后進行手動埋點。但是它有如下缺點:

  • 1、不夠優雅。

  • 2、代碼有侵入性。

2、AOP

關于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界面,就可以看到如下的界面布局加載耗時日志:

WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 174WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 13WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 44WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 61WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 22

可以看到,Awesome-WanAndroid項目里面各個界面的加載耗時一般都在幾十毫秒作用,加載慢的界面可能會達到100多ms,當然,不同手機的配置不一樣,但是,這足夠讓我們發現哪些界面布局的加載比較慢。

3、LayoutInflaterCompat.setFactory2

上面我們使用了AOP的方式監控了Activity的布局加載耗時,那么,如果我們需要監控每一個控件的加載耗時,該怎么實現呢?

答案是使用LayoutInflater.Factory2,我們在基類Activity的onCreate方法中直接使用LayoutInflaterCompat.setFactory2方法對Factory2的onCreateView方法進行重寫,代碼如下所示:

@Overrideprotected 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;        }    });    // 2、setFactory2方法需在super.onCreate方法前調用,否則無效            super.onCreate(savedInstanceState);    setContentView(getLayoutId());    unBinder = ButterKnife.bind(this);    mActivity = this;    ActivityCollector.getInstance().addActivity(this);    onViewCreated();    initToolbar();    initEventAndData();}

鴻洋注:這里去捕獲控件創建確實是個思路,但是并不能捕獲到所有的控件,如果大家有這方面需求,可以在 github 上看一些換膚框架的處理。

這樣我們就實現了利用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方法,代碼如下所示:

@Overridepublic 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方法如下所示:

@Overrideprotected 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()方法如下所示:

@Overridepublic 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}. */@Overridepublic final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {    // 1、First let the Activity's Factory try and inflate the view    final View view = callActivityOnCreateView(parent, name, context, attrs);    if (view != null) {        return view;    }    // 2、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方法的實現上面我們已經分析過了,此處就不再贅述了。

5總結

在本篇文章中,我們主要對Android的布局繪制以及加載原理、優化工具、全局監控布局和控件的加載耗時進行了全面的講解,這為大家學習《深入探索Android布局優化(下)》打下了良好的基礎。

下面,總結一下本篇文章涉及的五大主題:

  1. 繪制原理:CPU\GPU、Android圖形系統的整體架構、繪制線程、刷新機制。

  2. 屏幕適配:OLED 屏幕和 LCD 屏幕的區別、屏幕適配方案。(本文略過了,可以去原文查看)

  3. 優化工具:使用Systrace來進行布局優化、利用Layout Inspector來查看視圖層級結構、采用Choreographer來獲取FPS以及自動化測量 UI 渲染性能的方式(gfxinfo、SurfaceFlinger等dumpsys命令)。

  4. 布局加載原理:布局加載源碼分析、LayoutInflater.Factory分析。

  5. 獲取界面布局耗時:使用AOP的方式去獲取界面加載的耗時、利用LayoutInflaterCompat.setFactory2去監控每一個控件加載的耗時。

下篇,我們將進入布局優化的實戰環節,敬請期待~

參考鏈接:

1、國內Top團隊大牛帶你玩轉Android性能分析與優化 第五章 布局優化

2、極客時間之Android開發高手課 UI優化

3、手機屏幕的前世今生 可能比你想的還精彩

4、OLED 和 LCD 什么區別?

5、Android 目前穩定高效的UI適配方案

6、騷年你的屏幕適配方式該升級了!-smallestWidth 限定符適配方案

7、dimens_sw github

8、一種極低成本的Android屏幕適配方式

9、騷年你的屏幕適配方式該升級了!-今日頭條適配方案

10、今日頭條屏幕適配方案終極版正式發布!

11、使用Systrace分析UI性能

12、GAPID-Graphics API Debugger

13、Android性能優化之渲染篇

14、Android 屏幕繪制機制及硬件加速

15、Android 圖形處理官方教程

16、Vulkan - 高性能渲染

17、Android Vulkan Tutorial

18、Test UI performance-gfxinfo

19、使用dumpsys gfxinfo 測UI性能(適用于Android6.0以后)

20、TextureView API

21、PrecomputedText API

22、Litho Tutorial

23、基本功 | Litho的使用及原理剖析

24、Flutter官方文檔中文版

25、[Google Flutter 團隊出品] 深入了解 Flutter 的高性能圖形渲染

26、Flutter渲染機制—UI線程

27、RenderThread:異步渲染動畫

28、RenderScript官方文檔

29、RenderScript :簡單而快速的圖像處理

30、RenderScript渲染利器

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容