「深入理解Android布局優化 1」-布局的加載流程與繪制原理

前言

本篇文章是《深入理解Android布局優化》系列文章的第一篇。系列的主要目的是希望將Android開發中涉及布局優化的部分做一次系統的歸納、總結和學習。本系列文章包含理論基礎常見工具項目實踐三個部分。

理論基礎:「深入理解Android布局優化 1」-布局的加載流程與繪制原理,主要講解布局的加載流程與繪制原理,從源碼上發現布局的性能瓶頸。

常見工具:「深入理解Android布局優化 2」-常見工具的使用,主要講解Android布局優化時各種常見工具的使用。

項目實踐:以一個實際的APP為例,將學習到的理論和工具,實際運用到Android開發中。

本文中實踐時使用的項目地址:https://github.com/linux-link/Fan,可以先閱讀這篇文章了解這個項目一次組件化與Android Jetpack的實踐

本篇屬于三個部分中的理論基礎部分。

目錄

  • Android系統的繪圖機制
  • Activity的組成
  • 布局文件的加載流程
  • View的繪制流程
  • 布局優化的簡單建議
  • 總結

正文

一、Android系統的繪圖機制

Android系統每隔16ms就重新繪制一次Activity,這就要求UI界面必須在16ms內完成屏幕刷新的全部邏輯操作,這樣才能達到每秒60fps,然而這個fps是由手機硬件所決定,現在大多數手機屏幕刷新率是60Hz(赫茲是國際單位制中頻率的單位,它是每秒中的周期性變動重復次數的計量),也就是說我們有16ms(1000ms/60fps=16.66ms)的時間去完成每幀的繪制邏輯操作,如果超過了就會出現所謂的丟幀。實際開發中復雜的界面往往在16ms內完成全部繪制,但是盡量降級UI的繪制時間,總是可以有效的降低卡頓感。

對于Android系統的硬件繪圖機制,并非布局優化的重點,有興趣的可以翻看文末的參考資料。

二、Activity的組成

一個Activity層級結構圖,如下所示


它有點像洋蔥圈一層包裹著一層,下面我們就來逐個介紹一下。

  • PhoneWindow

    PhoneWindow是Window的子類,Window是頂級窗口外觀和行為策略的抽象基類。它提供標準的UI策略,例如背景,標題區域,默認密鑰處理等。它的唯一實現就是PhoneWindow

  • DecorView

    DecorView是一個ViewGroup類,繼承自FrameLayout,是Activity在繪制布局文件時的宿主,也可以把它理解為繪制布局文件時的“畫布”。

  • TitleActionBar

    Android提供一個默認的ActionBar,我們在寫demo時經常會看到這個ActionBar,一般正式開發時,會在Style.xml中把它去掉.

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    
  • ContentView

    ContentView就是我們在setContentView時傳入的xml布局文件繪制出來的ViewGroup,在Activity(kotlin語言)中我們可以通過如下代碼獲取到各個ContentView

    //kotlin
    window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
    //java
    getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)
    

通過這張層級關系圖,我們就大致明白了Activity層級結構,理解Activity的頁面層級結構非常的重要,它不僅與性能優化息息相關,而且也可以幫助我們理解Android觸摸事件的分發機制。

觸摸事件的分發機制,經常涉及到自定義的View,自定義View其實也是我們在布局優化時常用的手段之一。

這里重新畫了一張“洋蔥圈”一樣的層級結構圖,來幫助你理解觸摸事件的向上傳遞機制。這張圖很形象的解釋了觸摸事件是如何從Activity中開始傳遞,又是如何回到Activity中的。關于觸摸事件的分發具體的分發機制,請參閱其他文章,這里就不再細說了。

洋蔥圈結構圖

三、布局文件的加載流程

在Android開發中setContentView是我們最常用的將xml格式的布局文件繪制到activity中的方法。那么布局文件是如何繪制到Activity當中的呢?通過閱讀setcontentView的源代碼,可以發現布局文件的加載大致分為,讀取xml創建View對象兩個流程。

image

1.讀取xml布局文件

  • setContentView

    setContentView很好理解,就是向Activity的DecorView中裝載布局文件。Activity再通過decorView拿到當前設定的布局,交給LayoutInflater解析。

    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
     
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    
  • LayoutInflater.inflate()

    LayoutInflater,在Android系統通過它將布局XML文件實例化為其對應的View 對象。在inflate中通過loadXmlResourceParser方法來讀取xml布局文件,并把讀取到的文件流封裝到XmlResourceParser中。這樣就把xml布局文件就從存儲器中放到了內存中,注意loadXmlResourceParser是一個IO操作

2.根據xml布局文件,創建對應的View或ViewGroup

  • LayoutInflater.createViewFromTag()

    在loadXmlResourceParser把文件流裝載到XmlResourceParser之后,LayoutInflater會調用createViewFromTag方法,根據標簽來創建對應的View對象,例如根據讀取到的<TextView>創建TextView對象。

    createViewFromTag創建View對象主要是Fractory的onCreateView或是調用createView方法來實現,createView內部具體是通過反射來創建View對象。

簡單梳理一遍View的加載流程,你會發現,到這里Android系統就完成了把xml布局文件轉換成具體的View對象,在這其中我們可以看到至少兩個會影響性能地方,一個是loadXmlResourceParser(),把xml讀取到內存中這樣的IO操作會影響性能,另一個則是createView(),通過反射創建對象會影響性能。這兩個地方將是我們日后進行布局優化的重點。

目前為止Activity還是看不任何東西的,因為創建的View還沒有開始繪制。接下來我們就來看看View的繪制流程。

四、View的繪制流程

View繪制流程主要分為三個部分:measure、layout、draw,分別對應測量、布局和繪制,其中measure確定View的測量寬高,layout確定View最終寬高和四個頂點的位置,draw負責將view最終繪制到屏幕上。

ViewGroup的繪制流程與View大體相同,唯一的區別就是,View只需要繪制它自己,而ViewGroup不僅要繪制它自己還要繪制它的子View。下面我們就以ViewGroup為例,簡單從源代碼的角度來看一下這三個流程:

1.Measure與MeasureSpec

測量過程通過measure()來實現,是View樹自頂向下的遍歷,每個View在循環過程中將尺寸細節向下傳遞,當測量過程完成之后,所有的View也就都存儲了自己尺寸。

ViewGroup是一個抽象類,它并沒有重寫View的measure()方法,它在內部會調用measureChildren(),然后再去循環調用View的measure()方法。

measure()方法需要傳入兩個參數widthMeasureSpec和heightMeasureSpec。

protected void measure(int widthMeasureSpec, int heightMeasureSpec)

表面上看widthMeasureSpec和heightMeasureSpec是int的數字,它們是父類傳過來的給當前View的一個建議值(這個建議值是我們在XML中設定的),實際上是由mode+size組成的。將widthMeasureSpec轉換為二進制后,它是一個32位的數字,前兩位表示模式(mode),后30位表示數值(size)。

mode共有三種模式,分別是

  • UNSPECIFIED(未指定)

    不做任何限制,View可以獲得任意大小。它一般用于系統的內部測量過程。

  • EXACTLY(完全)

    由父View決定子View的確切大小,子View將被限定在給定邊界里而忽略它自身的大小。對應match_parent和具體的dp值

  • AT_MOST(至多)

    View最多達到指定大小的值,對應wrap_content

上述3中模式在自定義view時非常有用,當模式是EXACTLY時,我們是直接使用父類的建議值,當模式是AT_MOST時,我們則需要自己設定View的大小,因為用戶沒有規定這個View有多大。

2.layout

Layout的作用是ViewGroup用來確定子View的位置。在ViewGroup中調用layout方法確定位置確定后,它會在onLayout中遍歷所有子View的layout方法,子View的layout又會調用onLayout方法,確定自己的位置。

layout的大致流程如下:

首先通過setFrame設定View的四個頂點位置;

然后調用onLayout方法,在這里面調用每個子View的layout

3.draw

draw的過程是最簡單的,它的作用就是把View繪制到屏幕上,

 public void draw(Canvas canvas) {}

在draw方法中主要完成了一下幾個任務:

  • 使用drawBackground方法繪制背景
  • 在onDraw中繪制自己
  • 在dispatch中繪制子View
  • 在onDrawScrollBars中繪制裝飾

在Android中draw方法會被頻繁的調用,例如:按home鍵app進入后臺,當我們在回到APP時,即使APP沒有被銷毀,當前界面下View組件的draw方法也會被調用。

簡單了解了View的繪制流程后,不難看出這里面也存在至少兩個性能瓶頸,一個是measure和layout過程中會循環調用子View的方法,其實這就決定了布局文件不能嵌套過深,否則循環的時間復雜度會很高。另一個是View的draw方法會被頻繁的調用,對于這類頻繁調用的方法,我們不能在其中創建對象或執行耗時操作,否則會產生劇烈的內存抖動和頁面卡頓。

五、布局優化的簡單建議

通過上面的分析,我們對布局的組成,加載以及繪制有了一定的了解,現在再來看看常見的布局優化建議,相信你一定對這些建議有了進一步的認識。

  • 使用ConstraintLayout減少布局嵌套

    ConstraintLayout是Google推出一種可以有效減少嵌套問題的布局,它可以讓你的布局更加的扁平化,如果你沒有使用過ConstraintLayout,強烈推薦使用。

  • 使用<include/>和<merge/>標簽來減少布局嵌套

    <include/>標簽可以將一個指定的布局引入到當前的布局中,通過這種方式可以復用項目中已經存在的布局。有時候被引用的布局頂級節點與外部布局存在重復的情況,這時就可以使用<merge/>將多余的頂級節點去掉。關于<merge/>

  • 使用ViewStub延遲加載布局

    ViewStub繼承了View,它的寬高都是0,因此它不參與任何布局與繪制的過程。在開發中有的布局正常情況下并不顯示,這時候就可以使用ViewStub,在布局初始化的時候可以避免加載這類并不需要立即顯示的布局。

  • 不要在onDraw()創建對象或執行耗時操作

    具體原因在上面已經說過了,這里就不贅述了。

  • 不使用xml布局

    使用xml布局文件,Android需要通過IO操作把xml布局文件加載到內存中,然后通過反射創建view對象,如果不使用xml就可以完全避免這些影響的性能操作。使用這類思想創建布局文件框架有的iReader的X2C和FaceBook的Litho,不過這是一類很極端的做法,并不推薦。

  • 復雜布局使用自定義View

    當App設計圖非常復雜,我們需要使用非常多的系統組件組合才能實現相似的功能時,建議使用自定義View,保持界面的扁平化。

六、總結

本篇文章梳理了一下Activity的組成,一個xml的布局是如何加載到界面中,以及是如果繪制出來的,最后總結了一下目前的布局優化建議。但是在實際的開發中往往很難讓所有人完全遵守布局優化的建議,下一篇我們來講講布局優化時常用的工具「深入理解Android布局優化 2」-常見工具的使用,通過工具來幫助我們發現UI的性能問題。

參考資料

Android進階——性能優化之布局渲染原理和底層機制詳解(四)

《Android開發藝術探索》 任玉剛著

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容