前言
本篇文章是《深入理解Android布局優(yōu)化》系列文章的第一篇。系列的主要目的是希望將Android開發(fā)中涉及布局優(yōu)化的部分做一次系統(tǒng)的歸納了牛、總結(jié)和學習颜屠。本系列文章包含理論基礎(chǔ)、常見工具鹰祸、項目實踐三個部分甫窟。
理論基礎(chǔ):「深入理解Android布局優(yōu)化 1」-布局的加載流程與繪制原理,主要講解布局的加載流程與繪制原理蛙婴,從源碼上發(fā)現(xiàn)布局的性能瓶頸粗井。
常見工具:「深入理解Android布局優(yōu)化 2」-常見工具的使用,主要講解Android布局優(yōu)化時各種常見工具的使用街图。
項目實踐:以一個實際的APP為例浇衬,將學習到的理論和工具,實際運用到Android開發(fā)中餐济。
本文中實踐時使用的項目地址:https://github.com/linux-link/Fan耘擂,可以先閱讀這篇文章了解這個項目一次組件化與Android Jetpack的實踐
本篇屬于三個部分中的理論基礎(chǔ)部分。
目錄
- Android系統(tǒng)的繪圖機制
- Activity的組成
- 布局文件的加載流程
- View的繪制流程
- 布局優(yōu)化的簡單建議
- 總結(jié)
正文
一絮姆、Android系統(tǒng)的繪圖機制
Android系統(tǒng)每隔16ms就重新繪制一次Activity醉冤,這就要求UI界面必須在16ms內(nèi)完成屏幕刷新的全部邏輯操作,這樣才能達到每秒60fps篙悯,然而這個fps是由手機硬件所決定蚁阳,現(xiàn)在大多數(shù)手機屏幕刷新率是60Hz(赫茲是國際單位制中頻率的單位,它是每秒中的周期性變動重復(fù)次數(shù)的計量)鸽照,也就是說我們有16ms(1000ms/60fps=16.66ms)的時間去完成每幀的繪制邏輯操作螺捐,如果超過了就會出現(xiàn)所謂的丟幀。實際開發(fā)中復(fù)雜的界面往往在16ms內(nèi)完成全部繪制矮燎,但是盡量降級UI的繪制時間定血,總是可以有效的降低卡頓感。
對于Android系統(tǒng)的硬件繪圖機制漏峰,并非布局優(yōu)化的重點糠悼,有興趣的可以翻看文末的參考資料届榄。
二浅乔、Activity的組成
一個Activity層級結(jié)構(gòu)圖,如下所示
它有點像洋蔥圈一層包裹著一層铝条,下面我們就來逐個介紹一下靖苇。
-
PhoneWindow
PhoneWindow是Window的子類,Window是頂級窗口外觀和行為策略的抽象基類班缰。它提供標準的UI策略贤壁,例如背景,標題區(qū)域埠忘,默認密鑰處理等脾拆。它的唯一實現(xiàn)就是PhoneWindow
-
DecorView
DecorView是一個ViewGroup類馒索,繼承自FrameLayout,是Activity在繪制布局文件時的宿主名船,也可以把它理解為繪制布局文件時的“畫布”绰上。
-
TitleActionBar
Android提供一個默認的ActionBar,我們在寫demo時經(jīng)常會看到這個ActionBar渠驼,一般正式開發(fā)時蜈块,會在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)
通過這張層級關(guān)系圖迷扇,我們就大致明白了Activity層級結(jié)構(gòu)百揭,理解Activity的頁面層級結(jié)構(gòu)非常的重要,它不僅與性能優(yōu)化息息相關(guān)蜓席,而且也可以幫助我們理解Android觸摸事件的分發(fā)機制器一。
觸摸事件的分發(fā)機制,經(jīng)常涉及到自定義的View瓮床,自定義View其實也是我們在布局優(yōu)化時常用的手段之一盹舞。
這里重新畫了一張“洋蔥圈”一樣的層級結(jié)構(gòu)圖,來幫助你理解觸摸事件的向上傳遞機制隘庄。這張圖很形象的解釋了觸摸事件是如何從Activity中開始傳遞踢步,又是如何回到Activity中的。關(guān)于觸摸事件的分發(fā)具體的分發(fā)機制丑掺,請參閱其他文章获印,這里就不再細說了。
三街州、布局文件的加載流程
在Android開發(fā)中setContentView是我們最常用的將xml格式的布局文件繪制到activity中的方法兼丰。那么布局文件是如何繪制到Activity當中的呢?通過閱讀setcontentView的源代碼唆缴,可以發(fā)現(xiàn)布局文件的加載大致分為鳍征,讀取xml、創(chuàng)建View對象兩個流程面徽。
1.讀取xml布局文件
-
setContentView
setContentView很好理解艳丛,就是向Activity的DecorView中裝載布局文件。Activity再通過decorView拿到當前設(shè)定的布局趟紊,交給LayoutInflater解析氮双。
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); LayoutInflater.from(mContext).inflate(resId, contentParent);
-
LayoutInflater.inflate()
LayoutInflater,在Android系統(tǒng)通過它將布局XML文件實例化為其對應(yīng)的View 對象霎匈。在inflate中通過loadXmlResourceParser方法來讀取xml布局文件戴差,并把讀取到的文件流封裝到XmlResourceParser中。這樣就把xml布局文件就從存儲器中放到了內(nèi)存中铛嘱,注意loadXmlResourceParser是一個IO操作暖释。
2.根據(jù)xml布局文件袭厂,創(chuàng)建對應(yīng)的View或ViewGroup
-
LayoutInflater.createViewFromTag()
在loadXmlResourceParser把文件流裝載到XmlResourceParser之后,LayoutInflater會調(diào)用createViewFromTag方法球匕,根據(jù)標簽來創(chuàng)建對應(yīng)的View對象嵌器,例如根據(jù)讀取到的<TextView>創(chuàng)建TextView對象。
createViewFromTag創(chuàng)建View對象主要是Fractory的onCreateView或是調(diào)用createView方法來實現(xiàn)谐丢,createView內(nèi)部具體是通過反射來創(chuàng)建View對象爽航。
簡單梳理一遍View的加載流程,你會發(fā)現(xiàn)乾忱,到這里Android系統(tǒng)就完成了把xml布局文件轉(zhuǎn)換成具體的View對象讥珍,在這其中我們可以看到至少兩個會影響性能地方,一個是loadXmlResourceParser()窄瘟,把xml讀取到內(nèi)存中這樣的IO操作會影響性能衷佃,另一個則是createView(),通過反射創(chuàng)建對象會影響性能蹄葱。這兩個地方將是我們?nèi)蘸筮M行布局優(yōu)化的重點氏义。
目前為止Activity還是看不任何東西的,因為創(chuàng)建的View還沒有開始繪制图云。接下來我們就來看看View的繪制流程惯悠。
四、View的繪制流程
View繪制流程主要分為三個部分:measure竣况、layout克婶、draw,分別對應(yīng)測量丹泉、布局和繪制情萤,其中measure確定View的測量寬高,layout確定View最終寬高和四個頂點的位置摹恨,draw負責將view最終繪制到屏幕上筋岛。
ViewGroup的繪制流程與View大體相同,唯一的區(qū)別就是晒哄,View只需要繪制它自己睁宰,而ViewGroup不僅要繪制它自己還要繪制它的子View。下面我們就以ViewGroup為例揩晴,簡單從源代碼的角度來看一下這三個流程:
1.Measure與MeasureSpec
測量過程通過measure()來實現(xiàn)勋陪,是View樹自頂向下的遍歷贪磺,每個View在循環(huán)過程中將尺寸細節(jié)向下傳遞硫兰,當測量過程完成之后,所有的View也就都存儲了自己尺寸寒锚。
ViewGroup是一個抽象類劫映,它并沒有重寫View的measure()方法违孝,它在內(nèi)部會調(diào)用measureChildren(),然后再去循環(huán)調(diào)用View的measure()方法。
measure()方法需要傳入兩個參數(shù)widthMeasureSpec和heightMeasureSpec泳赋。
protected void measure(int widthMeasureSpec, int heightMeasureSpec)
表面上看widthMeasureSpec和heightMeasureSpec是int的數(shù)字雌桑,它們是父類傳過來的給當前View的一個建議值(這個建議值是我們在XML中設(shè)定的),實際上是由mode+size組成的祖今。將widthMeasureSpec轉(zhuǎn)換為二進制后校坑,它是一個32位的數(shù)字,前兩位表示模式(mode)千诬,后30位表示數(shù)值(size)耍目。
mode共有三種模式,分別是
-
UNSPECIFIED(未指定)
不做任何限制徐绑,View可以獲得任意大小邪驮。它一般用于系統(tǒng)的內(nèi)部測量過程。
-
EXACTLY(完全)
由父View決定子View的確切大小傲茄,子View將被限定在給定邊界里而忽略它自身的大小毅访。對應(yīng)match_parent和具體的dp值
-
AT_MOST(至多)
View最多達到指定大小的值,對應(yīng)wrap_content
上述3中模式在自定義view時非常有用盘榨,當模式是EXACTLY時喻粹,我們是直接使用父類的建議值,當模式是AT_MOST時草巡,我們則需要自己設(shè)定View的大小磷斧,因為用戶沒有規(guī)定這個View有多大。
2.layout
Layout的作用是ViewGroup用來確定子View的位置捷犹。在ViewGroup中調(diào)用layout方法確定位置確定后弛饭,它會在onLayout中遍歷所有子View的layout方法,子View的layout又會調(diào)用onLayout方法萍歉,確定自己的位置侣颂。
layout的大致流程如下:
首先通過setFrame設(shè)定View的四個頂點位置;
然后調(diào)用onLayout方法枪孩,在這里面調(diào)用每個子View的layout
3.draw
draw的過程是最簡單的憔晒,它的作用就是把View繪制到屏幕上,
public void draw(Canvas canvas) {}
在draw方法中主要完成了一下幾個任務(wù):
- 使用drawBackground方法繪制背景
- 在onDraw中繪制自己
- 在dispatch中繪制子View
- 在onDrawScrollBars中繪制裝飾
在Android中draw方法會被頻繁的調(diào)用蔑舞,例如:按home鍵app進入后臺拒担,當我們在回到APP時,即使APP沒有被銷毀攻询,當前界面下View組件的draw方法也會被調(diào)用从撼。
簡單了解了View的繪制流程后,不難看出這里面也存在至少兩個性能瓶頸钧栖,一個是measure和layout過程中會循環(huán)調(diào)用子View的方法低零,其實這就決定了布局文件不能嵌套過深婆翔,否則循環(huán)的時間復(fù)雜度會很高。另一個是View的draw方法會被頻繁的調(diào)用掏婶,對于這類頻繁調(diào)用的方法啃奴,我們不能在其中創(chuàng)建對象或執(zhí)行耗時操作,否則會產(chǎn)生劇烈的內(nèi)存抖動和頁面卡頓雄妥。
五最蕾、布局優(yōu)化的簡單建議
通過上面的分析,我們對布局的組成老厌,加載以及繪制有了一定的了解揖膜,現(xiàn)在再來看看常見的布局優(yōu)化建議,相信你一定對這些建議有了進一步的認識梅桩。
-
使用ConstraintLayout減少布局嵌套
ConstraintLayout是Google推出一種可以有效減少嵌套問題的布局壹粟,它可以讓你的布局更加的扁平化,如果你沒有使用過ConstraintLayout宿百,強烈推薦使用趁仙。
-
使用<include/>和<merge/>標簽來減少布局嵌套
<include/>標簽可以將一個指定的布局引入到當前的布局中,通過這種方式可以復(fù)用項目中已經(jīng)存在的布局垦页。有時候被引用的布局頂級節(jié)點與外部布局存在重復(fù)的情況雀费,這時就可以使用<merge/>將多余的頂級節(jié)點去掉。關(guān)于<merge/>
-
使用ViewStub延遲加載布局
ViewStub繼承了View痊焊,它的寬高都是0盏袄,因此它不參與任何布局與繪制的過程。在開發(fā)中有的布局正常情況下并不顯示薄啥,這時候就可以使用ViewStub辕羽,在布局初始化的時候可以避免加載這類并不需要立即顯示的布局。
-
不要在onDraw()創(chuàng)建對象或執(zhí)行耗時操作
具體原因在上面已經(jīng)說過了垄惧,這里就不贅述了刁愿。
-
不使用xml布局
使用xml布局文件,Android需要通過IO操作把xml布局文件加載到內(nèi)存中到逊,然后通過反射創(chuàng)建view對象铣口,如果不使用xml就可以完全避免這些影響的性能操作。使用這類思想創(chuàng)建布局文件框架有的iReader的X2C和FaceBook的Litho觉壶,不過這是一類很極端的做法脑题,并不推薦。
-
復(fù)雜布局使用自定義View
當App設(shè)計圖非常復(fù)雜铜靶,我們需要使用非常多的系統(tǒng)組件組合才能實現(xiàn)相似的功能時叔遂,建議使用自定義View,保持界面的扁平化。
六掏熬、總結(jié)
本篇文章梳理了一下Activity的組成,一個xml的布局是如何加載到界面中秒梅,以及是如果繪制出來的旗芬,最后總結(jié)了一下目前的布局優(yōu)化建議。但是在實際的開發(fā)中往往很難讓所有人完全遵守布局優(yōu)化的建議捆蜀,下一篇我們來講講布局優(yōu)化時常用的工具「深入理解Android布局優(yōu)化 2」-常見工具的使用疮丛,通過工具來幫助我們發(fā)現(xiàn)UI的性能問題。
參考資料
Android進階——性能優(yōu)化之布局渲染原理和底層機制詳解(四)
《Android開發(fā)藝術(shù)探索》 任玉剛著