App繪製優化

屏幕

在不一樣分辨率下,dpi將會不一樣,好比:html

根據上面的表格,咱們能夠發現,720P,和1080P的手機,dpi是不一樣的,這也就意味着,不一樣的分辨率中,1dp對應不一樣數量的px(720P中,1dp=2px,1080P中1dp=3px),這就實現了,當咱們使用dp來定義一個控件大小的時候,他在不一樣的手機裏表現出相應大小的像素值。java

咱們能夠說,經過dp加上自適應佈局和weight比例佈局能夠基本解決不一樣手機上適配的問題,這基本是最原始的Android適配方案。android

糗事百科適配方案:shell

www.jianshu.com/p/a4b8e4c5d…數組

字節跳動適配方案:緩存

mp.weixin.qq.com/s/d9QCoBP6k…bash

CPU 與 GPU

UI 渲染還依賴兩個核心的硬件:CPU 和 GPU。UI 組件在繪製到屏幕以前,都須要通過 Rasterization(柵格化)操做,這是一個耗時操做,而 GPU 能夠加快柵格化。架構

軟件繪製使用的是 Skia 庫,它是一款能在低端設備如手機上呈現高質量的 2D 跨平臺圖形框架,相似 Chrome、Flutter 內部使用的都是 Skia庫。app

OpenGL 與 Vulkan

Android 7.0 把 OpenGL ES 升級到最新的3.2 版本同時,還添加了對Vulkan的支持。Vulkan 是用於高性能 3D圖形的低開銷、跨平臺 API。框架

Android 渲染的演進

Image Stream Producers(圖像生產者)

任何能夠產生圖形信息的組件都統稱爲圖像的生產者,好比OpenGL ES, Canvas 2D, 和 媒體解碼器等。

Image Stream Consumers(圖像消費者)

SurfaceFlinger是最多見的圖像消費者,Window Manager將圖形信息收集起來提供給SurfaceFlinger,SurfaceFlinger接受後通過合成再把圖形信息傳遞給顯示器。同時,SurfaceFlinger也是惟一一個可以改變顯示器內容的服務。SurfaceFlinger使用OpenGL和Hardware Composer來生成surface. 某些OpenGL ES 應用一樣也可以充當圖像消費者,好比相機能夠直接使用相機的預覽界面圖像流,一些非GL應用也能夠是消費者,好比ImageReader 類。

Window Manager

Window Manager是一個用於控制window的系統服務,包含一系列的View。每一個Window都會有一個surface,Window Manager會監視window的許多信息,好比生命週期、輸入和焦點事件、屏幕方向、轉換、動畫、位置、轉換、z-order等,而後將這些信息(統稱window metadata)發送給SurfaceFlinger,這樣,SurfaceFlinger就能將window metadata合成爲顯示器上的surface。

Hardware Composer

爲硬件抽象層(HAL)的子系統。SurfaceFlinger能夠將某些合成工做委託給Hardware Composer,從而減輕OpenGL和GPU的工做。此時,SurfaceFlinger扮演的是另外一個OpenGL ES客戶端,當SurfaceFlinger將一個緩衝區或兩個緩衝區合成到第三個緩衝區時,它使用的是OpenGL ES。這種方式會比GPU更爲高效。

通常應用開發都要將UI數據使用Activity這個載體去展現,典型的Activity顯示流程爲:

startActivity啓動Activity; 爲Activity建立一個window(PhoneWindow),並在WindowManagerService中註冊這個window; 切換到前臺顯示時,WindowManagerService會要求SurfaceFlinger爲這個window建立一個surface用來繪圖。SurfaceFlinger建立一個」layer」(surface)。(以想象一下C/S架構,SF對應Server,對應Layer;App對應Client,對應Surface),這個layer的核心便是一個BufferQueue,這時候app就能夠在這個layer上render了; 將全部的layer進行合成,顯示到屏幕上。

通常app而言,在任何屏幕上起碼有三個layer:

屏幕頂端的status bar 屏幕下面的navigation bar 還有就是app的UI部分。 一些特殊狀況下,app的layer可能多餘或者少於3個,例如對全屏顯示的app就沒有status bar,而對launcher,還有個爲了wallpaper顯示的layer。status bar和navigation bar是由系統進行去render,由於不是普通app的組成部分嘛。而app的UI部分對應的layer固然是本身去render,因此就有了第4條中的全部layer進行「合成」。

GUI框架

Hardware Composer 那麼android是如何使用這兩種合成機制的呢?這裏就是Hardware Composer的功勞。處理流程爲:

SurfaceFlinger給HWC提供layer list,詢問如何處理這些layer; HWC將每一個layer標記爲overlay或者GLES composition,而後回饋給SurfaceFlinger; SurfaceFlinger須要去處理那些GLES的合成,而不用去管overlay的合成,最後將overlay的layer和GLES合成後的buffer發送給HWC處理。

在繪製過程當中,Android 各個圖形組件的做用:

  • 畫筆:Skia 或者 OpenGL。
  • 畫紙:Surface。
  • 畫板:Graphic Buffer,緩衝用於應用程序圖形的繪製。Android 4.1 以前的雙緩衝和以後的三緩衝機制。
  • 顯示:SurfaceFlinger。將 WindowManager 提供的全部 Furface,經過硬件合成器 Hardware Composer 合成並輸出到顯示屏。

Android 的硬件加速的歷史

  • Android 3.0 以前,沒有硬件加速。
  • Android 3.0 開始,Android 支持 硬件加速。
  • Android 4.0 默認開始硬件加速。

沒有開啓硬件加速時的繪製方式:

  • Surface。每一個 View 都由某個窗口管理,而每一個窗口都關聯一個 Surface。
  • Canvas。經過 Surface 的 lock() 方法得到一個 Canvas,Canvas 能夠簡單理解爲 Skia 底層接口的封裝。
  • Graphic Buffer。SurfaceFlinger 會託管一個 BufferQueue,從 BufferQueue 中拿到 Graphic Buffer,而後經過 Canvas 和 Skia 將繪製內容柵格化到上面。
  • SurfaceFlinger,經過 Swap Buffer 把 Front Graphic Buffer 的內容交給 SurfaceFinger,最後硬件合成器 Hardware Composer 合成並輸出到顯示屏。

硬件加速後的繪製方式:

最核心的差異是,經過 GPU 完成 Graphic Buffer 的內容繪製。此外還映入了 DisplayList 的概念,每一個 View 內部都有一個 DisplayList,當某個 View 須要重繪時,將它標記爲 Dirty。

須要重繪時,也是局部重繪,只會繪製一個 View 的 DisplayList,而不是像軟件繪製那也須要向上遞歸。

Android 4.1:Project Butter

單層緩衝引起「畫面撕裂」問題

單層緩衝引起「畫面撕裂」問題

如上圖,CPU/GPU 向 Buffer 中生成圖像,屏幕從 Buffer 中取圖像、刷新後顯示。這是一個典型的生產者——消費者模型。理想的狀況是幀率和刷新頻率相等,每繪製一幀,屏幕顯示一幀。而實際狀況是,兩者之間沒有必然的大小關係,若是沒有鎖來控制同步,很容易出現問題。 所謂」撕裂」就是一種畫面分離的現象,這樣獲得的畫像雖然類似可是上半部和下半部確實明顯的不一樣。這種狀況是因爲幀繪製的頻率和屏幕顯示頻率不一樣步致使的,好比顯示器的刷新率是75Hz,而某個遊戲的FPS是100. 這就意味着顯示器每秒更新75次畫面,而顯示卡每秒更新100次,比你的顯示器快33%。

雙緩衝

兩個緩存區分別爲 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中寫數據,屏幕從 Frame Buffer 中讀數據。VSync 信號負責調度從 Back Buffer 到 Frame Buffer 的複製操做,可認爲該複製操做在瞬間完成。

雙緩衝的模型下,工做流程這樣的:

在某個時間點,一個屏幕刷新週期完成,進入短暫的刷新空白期。此時,VSync 信號產生,先完成複製操做,而後通知 CPU/GPU 繪製下一幀圖像。複製操做完成後屏幕開始下一個刷新週期,即將剛複製到 Frame Buffer 的數據顯示到屏幕上。 在這種模型下,只有當 VSync 信號產生時,CPU/GPU 纔會開始繪製。這樣,當幀率大於刷新頻率時,幀率就會被迫跟刷新頻率保持同步,從而避免「tearing」現象。

VSYNC 偏移

應用和SurfaceFlinger的渲染迴路必須同步到硬件的VSYNC,在一個VSYNC事件中,顯示器將顯示第N幀,SurfaceFlinger合成第N+1幀,app合成第N+2幀。 使用VSYNC同步能夠保證延遲的一致性,減小了app和SurfaceFlinger的錯誤,以及顯示在各個階段之間的偏移。然而,前提是app和SurfaceFlinger每幀時間的變化並不大。所以,從輸入到顯示的延遲至少有兩幀。 爲了解決這個問題,您可使用VSYNC偏移量來減小輸入到顯示的延遲,其方法爲將app和SurfaceFlinger的合成信號與硬件的VSYNC關聯起來。由於一般app的合成耗時是小於兩幀的(33ms左右)。 VSYNC偏移信號細分爲如下3種,它們都保持相同的週期和偏移向量:

HW_VSYNC_0:顯示器開始顯示下一幀。 VSYNC:app讀取輸入並生成下一幀。 SF VSYNC:SurfaceFlinger合成下一幀的。 收到VSYNC偏移信號以後, SurfaceFlinger 纔開始接收緩衝區的數據進行幀的合成,而app才處理輸入並渲染幀,這些操做都將在16.7ms完成。

Jank 掉幀

注意,當 VSync 信號發出時,若是 GPU/CPU 正在生產幀數據,此時不會發生複製操做。屏幕進入下一個刷新週期時,從 Frame Buffer 中取出的是「老」數據,而非正在產生的幀數據,即兩個刷新週期顯示的是同一幀數據。這是咱們稱發生了「掉幀」(Dropped Frame,Skipped Frame,Jank)現象。

流暢性解決方案思路

從dumpsys SurfaceFlinger --latency中獲取127幀的數據 上面的命令返回的第一行爲設備自己固有的幀耗時,單位爲ns,一般在16.7ms左右 從第二行開始,分爲3列,一共有127行,表明每一幀的幾個關鍵時刻,單位也爲ns

第一列t1: when the app started to draw (開始繪製圖像的瞬時時間) 第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令將軟件SF幀傳遞給硬件HW以前的垂直同步時間),也就是對應上面所說的軟件Vsync 第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF將幀傳遞給HW的瞬時時間,及完成繪製的瞬時時間)

將第i行和第i-1行t2相減,便可獲得第i幀的繪製耗時,提取出每一幀不斷地dump出幀信息,計算出

一些計算規則

計算fps: 每dumpsys SurfaceFlinger一次計算彙總出一個fps,計算規則爲: frame的總數N:127行中的非0行數 繪製的時間T:設t=當前行t2 - 上一行的t2,求出全部行的和∑t fps=N/T (要注意時間轉化爲秒) 計算中一些細節問題 一次dumpsys SurfaceFlinger會輸出127幀的信息,可是這127幀多是這個樣子:

...
0               0               0
0               0               0
0               0               0
575271438588    575276081296    575275172129
575305169681    575309795514    575309142441
580245208898    580250445565    580249372231
580279290043    580284176346    580284812908
580330468482    580334851815    580333739054 
0               0               0
0               0               0
...
575271438588    575276081296    575275172129
575305169681    575309795514    575309142441
複製代碼

出現0的地方是因爲buffer中沒有數據,而非0的地方爲繪製幀的時刻,所以僅計算非0的部分數據 觀察127行數據,會發現偶爾會出現9223372036854775808這種數字,這是因爲字符溢出致使的,所以這一行數據也不能加入計算 不能單純的dump一次計算出一個fps,舉個例子,若是A時刻操做了手機,停留3s後,B時刻再次操做手機,按照上面的計算方式,則t>3s,而且也會參與到fps的計算去,從而形成了fps不許確,所以,須要加入一個閥值判斷,當t大於某個值時,就計算一次fps,而且把相關數據從新初始化,這個值通常取500ms 若是t<16.7ms,則直接按16.7ms算,一樣的總耗時T加上的也是16.7

計算jank的次數: 若是t3-t1>16.7ms,則認爲發生一次卡頓 流暢度得分計算公式 設目標fps爲target_fps,目標每幀耗時爲target_ftime=1000/target_fps 從如下幾個維度衡量流暢度:

fps: 越接近target_fps越好,權重分配爲40% 掉幀數:越少越好,權重分配爲40% 超時幀:拆分紅如下兩個維度

超時幀的個數,越少越好,權重分配爲5% 最大超時幀的耗時,越接近target_ftime越好,權重分配爲15%

end_time = round(last_frame_time / 1000000000, 2)
T = utils.get_current_time()
fps = round(frame_counts * 1000 / total_time, 2)

# 計算得分
g = fps / target
if g > 1:
  g = 1
if max_frame_time - kpi <= 1:
       max_frame_time = kpi
h = kpi / max_frame_time
 score = round((g * 50 + h * 10 + (1 - over_kpi_counts / frame_counts) * 40), 2)
複製代碼

2012 年 I/O 大會上宣佈 Project Butter 黃油計劃,在 4.1 中正式開啓這個機制。

Project Butter 主要包含:VSYNC 和 Triple Buffering(三緩存機制)。

VSYNC 相似時鐘中斷。每次收到 VSYNC 中斷,CPU 會當即準備 Buffer 數據,業內標準刷新頻率是 60Hz(每秒刷新 60次),也就是一幀數據的準備時間要在 16ms 內完成。

Android 4.1 以前,Android 使用雙緩衝機制,不一樣的 View 或者 Activity 它們都會共用一個 Window,也就是共用同一個 Surface。

每一個 Surface 都會有一個 BufferQueue 緩衝隊列,這個隊列會由 SurfaceFlinger 管理,經過匿名共享內存機制與 App 應用層交互。

安卓系統中有 2 種 VSync 信號:

屏幕產生的硬件 VSync: 硬件 VSync 是一個脈衝信號,起到開關或觸發某種操做的做用。 由 SurfaceFlinger 將其轉成的軟件 Vsync 信號:經由 Binder 傳遞給 Choreographer。

如何理解 Triple Buffering(三緩存機制)?

雙緩衝只有 A 和 B 兩個緩衝區,若是 CPU/GPU 繪製時間較長,超過一個 VSYNC 信號週期,由於緩衝區 B 中的數據沒有準備好,只能繼續展現 A 緩衝區的內容,這樣緩衝區 A 和 B 都分別被顯示設備和 GPU 佔用,CPU 沒法準備下一幀的數據。

增長一個緩衝區,CPU、GPU 和顯示設備都有各自的緩衝區,互不影響。

簡單來講,三緩存機制就是在雙緩衝機制的基礎上,增長一個 Graphic Buffer 緩衝區,這樣能夠最大限度的利用空閒時間,帶來的壞處是多私用了一個 Graphic Buffer 所佔用的內存。

檢測工具:

Systrace,Android 4.1 新增的新能數據採樣和分析工具。 Tracer for OpenGL ES,Android 4.1 新增的工具,能夠逐幀、逐函數的記錄 App 用 OpenGL ES 的繪製過程。 過分繪製工具,Android 4.2 新增,參考《檢查 GPU 渲染速度和繪製過分》

60 fps

手機屏幕是由許多的像素點組成的,每一個像素點經過顯示不一樣的顏色最終屏幕呈現各類各樣的圖像。手機系統的類型和手機硬件的不一樣致使UI的流暢性體驗個不一致。

屏幕展現的顏色數據

  • 在GPU中有一塊緩衝區叫作 Frame Buffer ,這個幀緩衝區能夠認爲是存儲像素值的二位數組。
  • 數組中的每個值就對應了手機屏幕的像素點須要顯示的顏色。
  • 因爲這個幀緩衝區的數值是在不斷變化的,因此只要完成對屏幕的刷新就能夠顯示不一樣的圖像了。
  • 至於刷新工做的邏輯電路會按期的刷新 Frame Buffer的。 目前主流的刷新頻率爲60次/秒 折算出來就是16ms刷新一次。
  • GPU 除了幀緩衝區用以交給手機屏幕進行繪製外. 還有一個緩衝區 Back Buffer 這個用以交給應用的,讓CPU往裏面填充數據。
  • GPU會按期交換 Back Buffer 和 Frame Buffer ,也就是對Back Buffer中的數據進行柵格化後將其轉到 Frame Buffer 而後交給屏幕進行顯示繪製,同時讓原先的Frame Buffer 變成 Back Buffer 讓程序處理。

Android的16ms

在Choreographer類中咱們有一個方法獲取屏幕刷新速率:

public final class Choreographer {
	private static float getRefreshRate() {
        DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
                Display.DEFAULT_DISPLAY);
        return di.refreshRate;
    }
}

public final class DisplayInfo implements Parcelable {
    public float refreshRate;
}

final class VirtualDisplayAdapter extends DisplayAdapter {
	private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient {
		@Override
        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
            if (mInfo == null) {
                mInfo = new DisplayDeviceInfo();
                mInfo.refreshRate = 60;
            }
            return mInfo;
        }
	}
}
複製代碼

VSYNC

VSYNC是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上已經很早就普遍使用的技術。 可簡單的把它認爲是一種定時中斷。

由上圖可知

1.時間從0開始,進入第一個16ms:Display顯示第0幀,CPU處理完第一幀後,GPU緊接其後處理繼續第一幀。三者互不干擾,一切正常。 2.時間進入第二個16ms:由於早在上一個16ms時間內,第1幀已經由CPU,GPU處理完畢。故Display能夠直接顯示第1幀。顯示沒有問題。但在本16ms期間,CPU和GPU 卻並未及時去繪製第2幀數據(注意前面的空白區),而是在本週期快結束時,CPU/GPU纔去處理第2幀數據。 3.時間進入第3個16ms,此時Display應該顯示第2幀數據,但因爲CPU和GPU尚未處理完第2幀數據,故Display只能繼續顯示第一幀的數據,結果使得第1 幀多畫了一次(對應時間段上標註了一個Jank)。 4.經過上述分析可知,此處發生Jank的關鍵問題在於,爲什麼第1個16ms段內,CPU/GPU沒有及時處理第2幀數據?緣由很簡單,CPU多是在忙別的事情(好比某個應用經過sleep 固定時間來實現動畫的逐幀顯示),不知道該處處理UI繪製的時間了。可CPU一旦想起來要去處理第2幀數據,時間又錯過了!

NSYNC的出現

由圖可知,每收到VSYNC中斷,CPU就開始處理各幀數據。整個過程很是完美。 不過,仔細琢磨圖2卻會發現一個新問題:圖2中,CPU和GPU處理數據的速度彷佛都能在16ms內完成,並且還有時間空餘,也就是說,CPU/GPU的FPS(幀率,Frames Per Second)要高於Display的FPS。確實如此。因爲CPU/GPU只在收到VSYNC時纔開始數據處理,故它們的FPS被拉低到與Display的FPS相同。但這種處理並無什麼問題,由於Android設備的Display FPS通常是60,其對應的顯示效果很是平滑。 若是CPU/GPU的FPS小於Display的FPS,會是什麼狀況呢?請看下圖:

由圖可知: 1.在第二個16ms時間段,Display本應顯示B幀,但卻由於GPU還在處理B幀,致使A幀被重複顯示。 2.同理,在第二個16ms時間段內,CPU無所事事,由於A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦過了VSYNC時間點, CPU就不能被觸發以處理繪製工做了。

Triple Buffer

爲何CPU不能在第二個16ms處開始繪製工做呢?緣由就是隻有兩個Buffer。若是有第三個Buffer的存在,CPU就能直接使用它, 而不至於空閒。出於這一思路就引出了Triple Buffer。結果如圖所示:

由圖可知: 第二個16ms時間段,CPU使用C Buffer繪圖。雖然仍是會多顯示A幀一次,但後續顯示就比較順暢了。 是否是Buffer越多越好呢?回答是否認的。由圖4可知,在第二個時間段內,CPU繪製的第C幀數據要到第四個16ms才能顯示, 這比雙Buffer狀況多了16ms延遲。因此,Buffer最好仍是兩個,三個足矣。

以上對VSYNC進行了理論分析,其實也引出了Project Buffer的三個關鍵點: 核心關鍵:須要VSYNC定時中斷。 Triple Buffer:當雙Buffer不夠使用時,該系統可分配第三塊Buffer。 另外,還有一個很是隱祕的關鍵點:即將繪製工做都統一到VSYNC時間點上。這就是Choreographer的做用。在它的統一指揮下,應用的繪製工做都將變得層次分明。

Android 5.0:RenderThread

5.0 以前,GPU 的高性能運算,都是在 UI 線程完成的,5.0 以後引入了兩個重大改變,一個是引入 RenderNode 的概念,它對 DisplayList 及一些 View 顯示屬性作了進一步封裝;另外一個是引入 RenderThread,全部 GL 命令執行都放在這個單獨的線程上,渲染線程在 RenderNode 中存有渲染幀的全部信息,能夠作一些屬性動畫。

此處還能夠開啓 Profile GPU Rendering 檢查,6.0 以後,會輸出下面的計算和繪製每一個階段的耗時。

UI 渲染測量的兩種工具:

測試工具:Profile GPU Rendering 和 Show GPU Overdraw。參考:《檢查 GPU 渲染速度和繪製過分》

Layout Inspector

用於分析手機上正在運行的呃App的視圖佈局結構。

GPU呈現模式分析工具簡介

Profile GPU Rendering工具的使用很簡單,就是直觀上看一幀的耗時有多長,綠線是16ms的閾值,超過了,可能會致使掉幀,這個跟VSYNC垂直同步信號有關係,固然,這個圖表並非絕對嚴謹的(後文會說緣由)。每一個顏色的方塊表明不一樣的處理階段,先看下官方文檔給的映射表:

想要徹底理解各個階段,要對硬件加速及GPU渲染有必定的瞭解,不過,有一點,必須先記內心:雖名爲 Profile GPU Rendering,但圖標中全部階段都發生在CPU中,不是GPU 。最終CPU將命令提交到 GPU 後觸發GPU異步渲染屏幕,以後CPU會處理下一幀,而GPU並行處理渲染,二者硬件上算是並行。 不過,有些時候,GPU可能過於繁忙,不能跟上CPU的步伐,這個時候,CPU必須等待,也就是最終的swapbuffer部分,主要是最後的紅色及黃色部分(同步上傳的部分不會有問題,我的認爲是由於在Android GPU與CPU是共享內存區域的),在等待時,將看到橙色條和紅色條中出現峯值,且命令提交將被阻止,直到 GPU 命令隊列騰出更多空間。

穩定定位工具:Systrace 和 Tracer for OpenGL ES,參考《Slow rendering》,Android 3.1 以後,推薦使用 Graphics API Debugger(GAPID)來替代 Tracer for OpenGL ES 工具。

有哪些自動化測量 UI 渲染性能的工具?

使用dumpsys gfxinfo 測UI性能

dumpsys是一款運行在設備上的Android工具,將 gfxinfo命令傳遞給dumpsys可在logcat中提供輸出,其中包含各階段發生的動畫以及幀相關的性能信息。

adb shell dumpsys gfxinfo < PACKAGE_NAME >
複製代碼

該命令可用於蒐集幀的耗時數據。運行該命令後,能夠等到以下的 結果:

Applications Graphics Acceleration Info:
Uptime: 102809662 Realtime: 196891968

** Graphics info for pid 31148 [com.android.settings] **

Stats since: 102794621664587ns
Total frames rendered: 105
Janky frames: 2 (1.90%)
50th percentile: 5ms
90th percentile: 7ms
95th percentile: 9ms
99th percentile: 19ms
Number Missed Vsync: 0
Number High input latency: 0
Number Slow UI thread: 2
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 1
HISTOGRAM: 5ms=78 6ms=16 7ms=4 8ms=1 9ms=2 10ms=0 11ms=0 12ms=0 13ms=2 14ms=0 15ms=0 16ms=0 17ms=0 18ms=0 19ms=1 20ms=0 21ms=0 22ms=0 23ms=0 24ms=0 25ms=0 26ms=0 27ms=0 
...
...
複製代碼

Graphics info for pid 31148 [com.android.settings]: 代表當前dump的爲設置界面的幀信息,pid爲31148 Total frames rendered: 105 本次dump蒐集了105幀的信息 Janky frames: 2 (1.90%) 105幀中有2幀的耗時超過了16ms,卡頓機率爲1.9% Number Missed Vsync: 0 垂直同步失敗的幀 Number High input latency: 0 處理input時間超時的幀數 Number Slow UI thread: 2 因UI線程上的工做致使超時的幀數 Number Slow bitmap uploads: 0 因bitmap的加載耗時的幀數 Number Slow issue draw commands: 1 因繪製致使耗時的幀數 HISTOGRAM: 5ms=78 6ms=16 7ms=4 ... 直方圖數據,表面耗時爲0-5ms的幀數爲78,耗時爲5-6ms的幀數爲16,同理類推。

在Android 6.0之後爲gfxinfo 提供了一個新的參數framestats,其做用能夠從最近的幀中提供很是詳細的幀信息,以便您能夠更準確地跟蹤和調試問題。

> adb shell dumpsys gfxinfo < PACKAGE_NAME > framestats
複製代碼

此命令將應用程序生成的最後120幀信息打印出,其中包含納秒時間戳。如下是來自adb dumpsys gfxinfo <PACKAGE_NAME>的示例原始輸出framestats:

0 ,27965466202353 ,27965466202353 ,27965449758000 ,27965461202353 ,27965467153286 ,27965471442505 ,27965471925682 ,27965474025318 ,27965474588547 ,27965474860786 ,27965475078599 ,27965479796151 ,27965480589068 ,0 ,27965482993342 ,27965482993342 ,27965465835000 ,27965477993342 ,27965483807401 ,27965486875630 ,
27965487288443 ,27965489520682 ,27965490184380 ,27965490568703 ,27965491408078 ,27965496119641 ,27965496619641 ,0 ,27965499784331 ,27965499784331 ,27965481404000 ,27965494784331 ,27965500785318 ,27965503736099 ,27965504201151 ,27965506776568 ,27965507298443 ,27965507515005 ,27965508405474 ,27965513495318 ,27965514061984 ,

0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,
複製代碼

此輸出的每一行表明應用程序生成的一幀。每一行的列數都相同,每列對應描述幀在不一樣的時間段的耗時狀況。

Framestats數據格式

因爲數據塊以CSV格式輸出,所以將其粘貼電子表格工具中很是簡單,或者經過腳本進行收集和分析。下表說明了輸出數據列的格式。全部的時間戳都是納秒。

  • FLAGS

FLAGS列爲'0'的行能夠經過從FRAME_COMPLETED列中減去INTENDED_VSYNC列計算其總幀時間。

若是非零,則該行應該被忽略,由於該幀的預期佈局和繪製時間超過16ms,爲異常幀。

  • INTENDED_VSYNC

幀的的預期起點。若是此值與VSYNC不一樣,是因爲 UI 線程中的工做使其沒法及時響應垂直同步信號所形成的。

  • VSYNC

花費在vsync監聽器和幀繪製的時間(Choreographer frame回調,動畫,View.getDrawingTime()等)

  • OLDEST_INPUT_EVENT

輸入隊列中最舊輸入事件的時間戳,若是沒有輸入事件,則輸入Long.MAX_VALUE。

此值主要用於平臺工做,對應用程序開發人員的用處有限。

  • NEWEST_INPUT_EVENT

輸入隊列中最新輸入事件的時間戳,若是幀沒有輸入事件,則爲0。

此值主要用於平臺工做,對應用程序開發人員的用處有限。

然而,經過查看(FRAME_COMPLETED - NEWEST_INPUT_EVENT),能夠大體瞭解應用程序添加的延遲時間。

  • HANDLE_INPUT_START

將輸入事件分派給應用程序的時間戳。

經過查看這段時間和ANIMATION_START之間的時間,能夠測量應用程序處理輸入事件的時間。

若是這個數字很高(> 2ms),這代表程序花費了很是長的時間來處理輸入事件,例如View.onTouchEvent(),也就是說此工做須要優化,或者分發到不一樣的線程。請注意,某些狀況下這是能夠接受的,例如發起新活動或相似活動的點擊事件,而且此數字很大。

  • ANIMATION_START

運行Choreographer註冊動畫的時間戳。

經過查看這段時間和PERFORM_TRANVERSALS_START之間的時間,能夠肯定評估運行的全部動畫器(ObjectAnimator,ViewPropertyAnimator和經常使用轉換器)須要多長時間。

若是此數字很高(> 2ms),請檢查您的應用是否編寫了自定義動畫以確保它們適用於動畫。

  • PERFORM_TRAVERSALS_START

PERFORM_TRAVERSALS_STAR-DRAW_START,則能夠提取佈局和度量階段完成的時間。(注意,在滾動或動畫期間,你會但願這應該接近於零..)

  • DRAW_START

performTraversals的繪製階段開始的時間。這是錄製任何無效視圖的顯示列表的起點。

這和SYNC_START之間的時間是在樹中全部無效視圖上調用View.draw()所花費的時間。

  • SYNC_QUEUED

同步請求發送到RenderThread的時間。

這標誌着開始同步階段的消息被髮送到RenderThread的時刻。若是此時間和SYNC_START之間的時間很長(> 0.1ms左右),則意味着RenderThread忙於處理不一樣的幀。在內部,這被用來區分幀作了太多的工做,超過了16ms的預算,因爲前一幀超過了16ms的預算,幀被中止了。

  • SYNC_START

繪圖的同步階段開始的時間。

若是此時間與ISSUE_DRAW_COMMANDS_START之間的時間很長(> 0.4ms左右),則一般表示有許多新的位圖必須上傳到GPU。

  • ISSUE_DRAW_COMMANDS_START

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

這段時間和FRAME_COMPLETED之間的時間間隔顯示了應用程序正在生產多少GPU。像這樣出現太多透支或低效率渲染效果的問題。

  • SWAP_BUFFERS

eglSwapBuffers被調用的時間。

  • FRAME_COMPLETED

幀的完整時間。花在這個幀上的總時間能夠經過FRAME_COMPLETED - INTENDED_VSYNC來計算。

你能夠用不一樣的方式使用這些數據。例以下面的直方圖,顯示不一樣幀時間的分佈(FRAME_COMPLETED - INTENDED_VSYNC),以下圖所示。

這張圖一目瞭然地告訴咱們,大多數的幀耗時都遠低於16ms(用紅色表示),但幾幀明顯超過了16ms。隨着時間的推移,咱們能夠查看此直方圖中的變化,以查看批量變化或新建立的異常值。您還能夠根據數據中的許多時間戳來繪製出輸入延遲,佈局花費的時間或其餘相似的感興趣度量。

若是在開發者選項中的CPU呈現模式分析中選擇adb shell dumpsys gfxinfo,則adb shell dumpsys gfxinfo命令將輸出最近120幀的時間信息,並將其分紅幾個不一樣的類別,能夠直觀的顯示各部分的快慢。

與上面的framestats相似,將它粘貼到您選擇的電子表格工具中很是簡單,或者經過腳本進行收集和解析。下圖顯示了應用程序生成的幀每一個階段的詳細耗時。

運行gfxinfo,複製輸出,將其粘貼到電子表格應用程序中,並將數據繪製爲直方圖的結果。

每一個垂直條表明一幀動畫; 其高度表示計算該動畫幀所用的毫秒數。條形圖中的每一個彩色段表示渲染管道的不一樣階段,以便您能夠看到應用程序的哪些部分可能會形成瓶頸。

framestats信息和frame耗時信息一般爲2s收集一次(一次120幀,一幀16ms,耗時約2s)。爲了精確控制時間窗口,例如,將數據限制爲特定的動畫 ,能夠重置全部計數器,並從新收集的數據。

> adb shell dumpsys gfxinfo < PACKAGE_NAME > reset
複製代碼

一樣 也適用於須要捕獲小於2s的數據。

dumpsys是能發現問題或者判斷問題的嚴重性,但沒法定位真正的緣由。若是要定位緣由,應當配合systrace工具使用。

SurfaceFlinger。三緩存機制,在 4.1 以後,每一個 Surface 都會有三個 Graphic Buffer,這部分能夠纔看到到當前渲染所佔用的內存信息。對於這部份內存,當應用退到後臺的時候,系統會將這些內存回收,不會記入應用的內存佔用中。

UI 優化有哪些經常使用手段?

  • 儘可能使用硬件加速。

有些 Convas API 不能完美支持硬件加速,參考 drawing-support 文檔。SVG 對不少指令硬件加速也不支持。

  • Create View 優化。

View 的建立在 UI 線程,複雜界面會引發耗時,耗時分析能夠分解成:XML 的隨機讀 的 I/O 時間、解析 XML 時間、生成對象的時間等。可使用的優化方式有:使用代碼建立,例如使用 XML 轉換爲 Java 代碼的工具,例如 X2C;

異步建立,在子線程建立 View 會出現異常,能夠先把子線程的 Looper 的 MessageQueue 替換成 UI 線程 Looper 的 Queue;

View 重用。

  • measure/layout 優化。渲染流程中 measure 和 layout 也須要 CPU 在主線程執行。優化的方法有:減小 UI 佈局層次,儘可能扁平化,<ViewStub>,<merge>、優化 layout 開銷,避免使用 RelativeLayout 或者 基於 weighted 的 LinearLayout。使用ConstraintLayout;背景優化,不要重複設置背景。

TextView 是系統空間中,很是複雜的一個控件,強大的背後就表明了不少計算,2018 年 Google I/O 發佈了 PercomputedText 並已經集成在 Jetpack 中,它提供了接口,能夠異步進行 measure 和 layout,沒必要在主線程中執行。

UI 優化的進階手段有哪些?

  • Litho:異步佈局。這是 Facebook 開源的聲明式 Android UI 渲染框架,基於另一個 Facebook 開源的佈局引擎 Yoga 開發的。Litho 的缺點很明顯,過重了。
  • Flutter:本身的佈局 + 渲染引擎。Flutter 使用 Skia 引擎渲染 UI,直接使用 Dart 虛擬機,跳出了 Android 原有的方案。參考:《Flutter 原理和實踐
  • RenderThread 和 RenderScript。5.0 開始,系統增長了 RenderThread,當主線程阻塞時,普通動畫會出現丟幀卡頓,而使用 RenderThread 渲染的動畫即便阻塞了主線程,仍然不受影響。參考《RenderThread實現動畫異步渲染》

RenderScript 參考:

RenderScript 渲染利器

RenderScript:簡單而快速的圖像處理 Android RenderScript 簡單實現圖片的高斯模糊效果

佈局優化

在編寫Android的佈局時總會遇到這樣或者那樣的痛點,好比:

  1. 有些佈局的在不少頁面都用到了,並且樣式都同樣,每次用到都要複製粘貼一大段,有沒有辦法能夠複用呢?
  2. 解決了1中的問題以後,發現複用的佈局外面總要額外套上一層佈局,要知道佈局嵌套是會影響性能的吶;
  3. 有些佈局只有用到時纔會顯示,可是必須提早寫好,設置雖然爲了invisible或gone,仍是多多少少會佔用內存的。

include

include的中文意思是「包含」,「包括」,你當一個在主頁面裏使用include標籤時,就表示當前的主佈局包含標籤中的佈局,這樣一來,就能很好地起到複用佈局的效果了在那些經常使用的佈局好比標題欄和分割線等上面用上它能夠極大地減小代碼量的它有兩個主要的屬性。:

  • layout:必填屬性,爲你須要插入當前主佈局的佈局名稱,經過R.layout.xx的方式引用;
  • id:當你想給經過包括添加進來的佈局設置一個ID的時候就可使用這個屬性,它能夠重寫插入主佈局的佈局ID。

常規使用

咱們先建立一個ViewOptimizationActivity,而後再建立一個layout_include.xml佈局文件,它的內容很是簡單,就一個TextView的:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:gravity="center_vertical"
    android:textSize="14sp"
    android:background="@android:color/holo_red_light"
    android:layout_height="40dp">
</TextView>
複製代碼

如今咱們就用include標籤,將其添加到ViewOptimizationActivity的佈局中:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--include標籤的使用-->
    <TextView
        android:textSize="18sp"
        android:text="一、include標籤的使用"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <include
        android:id="@+id/tv_include1"
        layout="@layout/layout_include"/>

</LinearLayout>
複製代碼

爲了驗證它就是layout_include.xml的根佈局的TextView的ID,咱們在ViewOptimizationActivity中初始化的TextView,並給它設置文字:

TextView tvInclude1 = findViewById(R.id.tv_include1);
tvInclude1.setText("1.1 常規下的include佈局");
複製代碼

說明咱們設置的佈局和標識都是成功的不過你可能會對ID這個屬性有疑問:?ID我能夠直接在的TextView中設置啊,爲何重寫它呢別忘了咱們的目的是複用,當在你主一個佈局中使用include標籤添加兩個以上的相同佈局時,ID相同就會衝突了,因此重寫它可讓咱們更好地調用它和它裏面的控件。還有一種狀況,假如你的主佈局是RelateLayout,這時爲了設置相對位置,你也須要給它們設置不一樣的ID。

重寫根佈局的佈局屬性

除了id以外,咱們還能夠重寫寬高,邊距和可見性(visibility)這些佈局屬性。可是必定要注意,單單重寫android:layout_height或者android:layout_width是不行,必須兩個同時重寫才起做用。包括邊距也是這樣,若是咱們想給一個包括進來的佈局添加右邊距的話的完整寫法是這樣的:

<include
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginEnd="40dp"
        android:id="@+id/tv_include2"
        layout="@layout/layout_include"/>
複製代碼

控件ID相同時的處理

在1.1中咱們知道了ID能夠屬性重寫include佈局的根佈局ID,但對於根佈局裏面的佈局和控件是無能爲力的,若是這時一個佈局在主佈局中包括了屢次,那怎麼區別裏面的控件呢?

咱們先建立一個layout_include2.xml的佈局,它的根佈局是FrameLayout,裏面有一個TextView,它的ID是tv_same:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_orange_light"
    android:layout_height="wrap_content">

    <TextView
        android:gravity="center_vertical"
        android:id="@+id/tv_same"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</FrameLayout>
複製代碼

在主佈局中添加進去:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--include標籤的使用-->
    ……

    <include layout="@layout/layout_include2"/>

    <include
        android:id="@+id/view_same"
        layout="@layout/layout_include2"/>

</LinearLayout>
複製代碼

爲了區分,這裏給第二個layout_include2設置了ID也許你已經反應過來了,沒錯,咱們就是要建立根佈局的對象,而後再去初始化裏面的控件:

TextView tvSame = findViewById(R.id.tv_same);
tvSame.setText("1.3 這裏的TextView的ID是tv_same");
FrameLayout viewSame = findViewById(R.id.view_same);
TextView tvSame2 = viewSame.findViewById(R.id.tv_same);
tvSame2.setText("1.3 這裏的TextView的ID也是tv_same");
複製代碼

merge

include標籤雖然解決了佈局重用的問題,卻也帶來了另一個問題:佈局嵌套由於把須要重用的佈局放到一個子佈局以後就必須加一個根佈局,若是你的主佈局的根佈局和你須要包括的根佈局都是同樣的(好比都是LinearLayout),那麼就至關於在中間多加了一層多餘的佈局了。有那麼沒有辦法能夠在使用include時不增長佈局層級呢?答案固然是有的,就是那使用merge標籤。

使用merge標籤要注意一點一:必須是一個佈局文件中的根節點,看起來跟其餘佈局沒什麼區別,但它的特別之處在於頁面加載時它的不會繪製,它就像是佈局或者控件的搬運工,把「貨物」搬到主佈局以後就會功成身退,不會佔用任何空間,所以也就不會增長佈局層級了。這正如它的名字同樣,只起「合併」做用。

常規使用

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_merge1"
        android:text="我是merge中的TextView1"
        android:background="@android:color/holo_green_light"
        android:gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="40dp" />

    <TextView
        android:layout_toEndOf="@+id/tv_merge1"
        android:id="@+id/tv_merge2"
        android:text="我是merge中的TextView2"
        android:background="@android:color/holo_blue_light"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp" />
</merge>
複製代碼

這裏我使用了一些相對佈局的屬性,緣由後面你就知道了咱們接着在ViewOptimizationActivity的佈局添加RelativeLayout的,而後使用包括標籤將layout_merge.xml添加進去:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/view_merge"
        layout="@layout/layout_merge"/>
</RelativeLayout>
複製代碼

對佈局層級的影響

在layout_merge.xml中,使用咱們相對佈局的屬性android:layout_toEndOf將藍色的TextView設置到了綠色的TextView的右邊,而layout_merge.xml的父佈局是RelativeLayout,因此這個屬性是起了做用了,merge標籤不會影響裏面的控件,也不會增長佈局層級。

看到能夠RelativeLayout下面直接就是兩個TextView的了,merge標籤並無增長佈局層級。能夠看出merge的侷限性,即須要你明確將merge裏面的佈局控件狀語從句:include到什麼類型的佈局中,提早設置merge裏面的佈局和控件的位置。

合併的ID

學習在include標籤時咱們知道,android:id屬性能夠重寫被包括的根佈局ID,但若是根節點merge呢?說前面了merge並不會做爲一個佈局繪製出來,因此這裏給它設置ID是不起做用的。咱們在它的父佈局RelativeLayout中再加一個TextView的,使用android:layout_below屬性把設置到layout_merge下面:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/view_merge"
        layout="@layout/layout_merge"/>

    <TextView
        android:text="我不是merge中的佈局"
        android:layout_below="@+id/view_merge"
        android:background="@android:color/holo_purple"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
</RelativeLayout>
複製代碼

運行以後你會發現新加的TextView中會把合併佈局蓋住,沒有像預期那樣在其下方。把若是android:layout_below中的ID改成layout_merge.xml中任一的TextView的ID(好比tv_merge1),運行以後就能夠看到以下效果:

即佈局父RelativeLayout下級佈局就是包括進去的TextView的了。

ViewStub

你必定遇到這樣的狀況:頁面中有些佈局在初始化時不必顯示,可是又不得不事先在佈局文件中寫好,設置雖然成了invisible或gone,可是在初始化時仍是會加載,這無疑會影響頁面加載速度。針對這一狀況,Android爲咱們提供了一個利器---- ViewStub。這是一個不可見的,大小爲0的視圖,具備懶加載的功能,它存在於視圖層級中,但只會在setVisibility()狀語從句:inflate()方法調用只會纔會填充視圖,因此不會影響初始化加載速度它有如下三個重要屬性:

  • android:layout:ViewStub須要填充的視圖名稱,爲「R.layout.xx」的形式;
  • android:inflateId:重寫被填充的視圖的父佈局ID。

與include標籤不一樣,ViewStub的android:id屬性的英文設置ViewStub自己ID的,而不是重寫佈局ID,這一點可不要搞錯了。另外,ViewStub還提供了OnInflateListener接口,用於監聽佈局是否已經加載了。

填充佈局的正確方式

咱們先建立一個layout_view_stub.xml,放置裏面一個Switch開關:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_blue_dark"
    android:layout_height="100dp">
    <Switch
        android:id="@+id/sw"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</FrameLayout>
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--ViewStub標籤的使用-->
    <TextView
        android:textSize="18sp"
        android:text="三、ViewStub標籤的使用"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ViewStub
        android:id="@+id/view_stub"
        android:inflatedId="@+id/view_inflate"
        android:layout="@layout/layout_view_stub"
        android:layout_width="match_parent"
        android:layout_height="100dp" />
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:text="顯示"
            android:id="@+id/btn_show"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:text="隱藏"
            android:id="@+id/btn_hide"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:text="操做父佈局控件"
            android:id="@+id/btn_control"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>
複製代碼

在ViewOptimizationActivity中監聽ViewStub的填充事件:

viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub viewStub, View view) {
                Toast.makeText(ViewOptimizationActivity.this, "ViewStub加載了", Toast.LENGTH_SHORT).show();
            }
        });
複製代碼

而後經過按鈕事件來填充和顯示layout_view_stub:

@Override
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.btn_show:
            viewStub.inflate();
            break;
        case R.id.btn_hide:
            viewStub.setVisibility(View.GONE);
            break;
        default:
            break;
    }
}
複製代碼

運行以後,點擊「顯示」按鈕,layout_view_stub顯示了,並彈出 「ViewStub加載了」 的吐司;點擊「隱藏」按鈕,佈局又隱藏掉了,可是再點擊一下「顯示」按鈕,頁面竟然卻閃退了,查看日誌,發現拋出了一個異常:

java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
複製代碼

咱們打開ViewStub的源碼

public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }
複製代碼
private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }
複製代碼

果真,ViewStub在這裏調用了removeViewInLayout()方法把本身從佈局移除了。到這裏咱們就明白了,ViewStub在填充佈局成功以後就會自我銷燬,再次調用inflate()方法就會拋出IllegalStateException異常異常了。此時若是想要再次顯示佈局,能夠調用setVisibility()方法。

爲了不inflate()方法屢次調用,咱們能夠採用以下三種方式:

try {
    viewStub.inflate();
} catch (IllegalStateException e) {
    Log.e("Tag",e.toString());
    view.setVisibility(View.VISIBLE);
}
複製代碼
if (isViewStubShow){
    viewStub.setVisibility(View.VISIBLE);
}else {
    viewStub.inflate();
}
複製代碼
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}
複製代碼

viewStub.getVisibility()爲什麼老是等於0?

在顯示ViewStub中的佈局時,你可能會採起以下的寫法:

if (viewStub.getVisibility() == View.GONE){
    viewStub.setVisibility(View.VISIBLE);
}else {
    viewStub.setVisibility(View.GONE);
}
複製代碼

若是你將viewStub.getVisibility()的值打印出來,就會看到它始終爲0,這偏偏是View.VISIBLE的值。奇怪,咱們明明寫了viewStub.setVisibility(View.GONE),layout_view_stub也隱藏了,爲何ViewStub的狀態仍是可見呢?

從新回到3.1.3,看看ViewStub中的setVisibility()源碼,首先判斷弱引用對象mInflatedViewRef是否爲空,不爲空則取出存放進去的對象,也就是咱們ViewStub中的視圖中,而後調用了視圖的setVisibility()方法,mInflatedViewRef爲空時,則判斷能見度爲VISIBLE或無形時調用充氣()方法填充佈局,若是爲GONE的話則不予處理。這樣一來,在mInflatedViewRef不爲空,也就是已經填充了佈局的狀況下,ViewStub中的setVisibility()方法其實是在設置內部視圖的可見性,而不是ViewStub自己。這樣的設計其實也符合ViewStub的特性,即填充佈局以後就自我銷燬了,給其設置可見性是沒有意義的。

仔細比較一下,其實ViewStub就像是一個懶惰的包含,咱們須要它加載時才加載。要操做佈局裏面的控件也跟包同樣,你能夠先初始化ViewStub中的佈局中再初始化控件:

//一、初始化被inflate的佈局後再初始化其中的控件,
FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId設置的id
Switch sw = frameLayout.findViewById(R.id.sw);
sw.toggle();
複製代碼

若是主佈局中控件的ID沒有衝突,能夠直接初始化控件使用:

//二、直接初始化控件
Switch sw = findViewById(R.id.sw);
sw.toggle();
複製代碼
相關文章
相關標籤/搜索