前言
本篇文章是《深入理解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對象兩個流程。
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開發藝術探索》 任玉剛著