前言
Android開(kāi)發(fā)中,一個(gè)好的應(yīng)用,除了要有吸引人的功能和交互之外,在性能上也應(yīng)該有高的要求,如果單單實(shí)現(xiàn)頁(yè)面和業(yè)務(wù)功能只是完成了基本任務(wù),Android系統(tǒng)對(duì)內(nèi)存要求也是非常高的,稍不注意,就會(huì)發(fā)生某個(gè)頁(yè)面繪制突然發(fā)生卡頓甚至OOM,這對(duì)產(chǎn)品的用戶(hù)體驗(yàn)都是致命性的打擊,這就需要我們?cè)谌粘i_(kāi)發(fā)中注意性能方面的優(yōu)化。

?
目錄
- 造成卡頓的原因
-
如何分析當(dāng)前頁(yè)面繪制情況
- 使用GPU過(guò)度繪制檢測(cè)頁(yè)面渲染層級(jí)
- 使用Layout Inspector查看布局層級(jí) -
如何優(yōu)化
- 移除疊加的背景
- 合理使用布局設(shè)計(jì)
- 采用布局標(biāo)簽減少布局嵌套
------ include和merge的用法
------ ViewStub的用法
?
正文
布局實(shí)現(xiàn)是Android開(kāi)發(fā)中必不可少的一部分,絕大部分頁(yè)面都離不開(kāi)布局文件的支持,對(duì)于有一定經(jīng)驗(yàn)的開(kāi)發(fā)者來(lái)說(shuō),通過(guò)編寫(xiě)布局文件實(shí)現(xiàn)頁(yè)面展示是一個(gè)很簡(jiǎn)單的操作,然而當(dāng)頁(yè)面設(shè)計(jì)復(fù)雜起來(lái),層級(jí)越來(lái)越深,頁(yè)面會(huì)變得越來(lái)越卡頓,作為開(kāi)發(fā)者都應(yīng)該關(guān)注下性能優(yōu)化,在平時(shí)的開(kāi)發(fā)工作中注意一些細(xì)節(jié),針對(duì)布局文件進(jìn)行多方位分析和調(diào)優(yōu),盡可能地去優(yōu)化應(yīng)用。
?
造成卡頓的原因
我們都知道,Android中是以層級(jí)疊加來(lái)實(shí)現(xiàn)頁(yè)面的展示,一個(gè)Activity綁定著一個(gè)Window,Window又管理著頁(yè)面的根ViewGroup,然后ViewGroup中包含著View,層層包裹,就如同Photoshop中的圖層:

因此如果同一個(gè)位置上面疊加了多個(gè)層級(jí),該像素點(diǎn)就會(huì)被繪制多次。本來(lái)用戶(hù)只需要看到最上面的那一層就夠了,但我們多余的渲染了多次,這就浪費(fèi)了大量的GPU和CPU資源,并且也增加了繪制時(shí)長(zhǎng)。而人眼與大腦之間的協(xié)作無(wú)法感知超過(guò)60fps的畫(huà)面更新,也就是1秒內(nèi)如果必須展示60幀,才能看起來(lái)流暢,1s=1000ms,因此,平均每16ms就要繪制一幀,如果布局層級(jí)很深,渲染時(shí)長(zhǎng)超過(guò)16ms,就會(huì)看起來(lái)稍顯卡頓。
?
如何分析當(dāng)前頁(yè)面繪制情況
1)使用GPU過(guò)度繪制檢測(cè)頁(yè)面渲染層級(jí)
Android系統(tǒng)支持我們查看頁(yè)面繪制情況的功能,在手機(jī)的設(shè)置-開(kāi)發(fā)者選項(xiàng)中有一個(gè)調(diào)試GPU繪制的開(kāi)關(guān):

打開(kāi)之后,會(huì)發(fā)現(xiàn)手機(jī)界面上出現(xiàn)了很多顏色區(qū)域:
?

GPU過(guò)度繪制幫我們顯示了每個(gè)像素點(diǎn)的繪制情況,并通過(guò)幾種顏色來(lái)代表當(dāng)前像素點(diǎn)的繪制次數(shù)(比如說(shuō)上圖中只有背景的區(qū)域都是藍(lán)色,有文字或者圖標(biāo)疊加的地方變成了綠色),GPU過(guò)度繪制一共有以下幾種顏色:
原色:沒(méi)有過(guò)度繪制
藍(lán)色:1 次過(guò)度繪制
綠色:2 次過(guò)度繪制
粉色:3 次過(guò)度繪制
紅色:4 次及以上過(guò)度繪制

平常開(kāi)發(fā)的界面中,應(yīng)該盡可能地將過(guò)度繪制控制為 2 次(綠色)及其以下,原色和藍(lán)色是最理想的。粉色和紅色應(yīng)該盡可能避免,在實(shí)際項(xiàng)目中避免不了時(shí),應(yīng)該盡可能減少粉色和紅色區(qū)域。
2)使用Layout Inspector查看布局層級(jí)
在SDK以前的舊版本,是可以通過(guò)Hierarchy Viewer來(lái)查看,但后面更新了版本,Google采用Layout Inspector來(lái)代替Hierarchy Viewer,詳見(jiàn) Android SDK Tools Revision 25.3.0。
可以在新版本的AndroidStudio中,菜單欄的Tools里面找到Layout Inspector:

然后選擇所要分析的進(jìn)程,比如選擇你自己正在運(yùn)行中的應(yīng)用,然后跳轉(zhuǎn)到你要分析的頁(yè)面,確認(rèn)之后會(huì)在項(xiàng)目目錄下生成一個(gè)captuers文件夾,Layout Inspector會(huì)根據(jù)當(dāng)前手機(jī)正在顯示的頁(yè)面生成一個(gè)文件存放在這個(gè)目錄下:

一共有三塊區(qū)域,左邊可以看到頁(yè)面的層級(jí),從最頂層的DecorView開(kāi)始,其下所有當(dāng)前頁(yè)面存在的View都會(huì)顯示在這里(關(guān)于DecorView的層級(jí)可見(jiàn)我另一篇文章Android 從源碼看懂窗口繪制流程),中間顯示的是當(dāng)前頁(yè)面的預(yù)覽,右邊顯示的是每個(gè)View的布局屬性,包括寬高、padding等等。通過(guò)Layout Inspector能清晰地看到頁(yè)面的層次結(jié)構(gòu),比如說(shuō)布局文件中某一處重復(fù)包裹了兩層View,或者不小心在自定義ViewGroup的時(shí)候多加了一層根View,在這里都能看得出來(lái)。
注意:Layout Inspector有一定限制,要求運(yùn)行的機(jī)器Android版本為16(Android4.1)以上,且要是當(dāng)前正在運(yùn)行的應(yīng)用進(jìn)程才可以,其實(shí)它就相當(dāng)于對(duì)當(dāng)前界面的一個(gè)快照。
?
如何優(yōu)化
1)移除疊加的背景
我們注冊(cè)Activity時(shí)一般都會(huì)為它設(shè)置主題,主題一般都會(huì)有默認(rèn)背景 windowBackground,比如下面這種:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="windowBackground">@color/colorPrimary</item>
</style>
它會(huì)為我們的頁(yè)面設(shè)置一個(gè)背景,但有些時(shí)候,布局文件里面的根布局也會(huì)設(shè)置一個(gè)背景,這個(gè)時(shí)候window的background用戶(hù)完全看不到,實(shí)際上沒(méi)有作用,這種情況下我們可以移除它的背景:
<item name="android:windowBackground">@null</item>
剛才是針對(duì)window的背景做了處理,同理,布局嵌套中也有可能出現(xiàn)這種情況,比如說(shuō)一個(gè)LinearLayout里包裹了兩個(gè)子View,而且這兩個(gè)子View剛好占滿了LinearLayout的全部空間,那LinearLayout同樣就沒(méi)必要設(shè)置背景了。
?
2)合理使用布局設(shè)計(jì)
我們平時(shí)都是用五大布局組合成頁(yè)面的結(jié)構(gòu),相同的效果,可以用不同的ViewGroup來(lái)組合實(shí)現(xiàn),但是RelativeLayout底層會(huì)測(cè)繪兩次,而LinearLayout和FrameLayout只會(huì)繪制一次(詳見(jiàn) RelativeLayout和LinearLayout及FrameLayout性能分析)。因此性能上不如LinearLayout和FrameLayout,比如下面這個(gè)布局:

我們可以采用
RelativeLayout 來(lái)實(shí)現(xiàn):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Describe"/>
</RelativeLayout>
也可以采用 FrameLayout 來(lái)實(shí)現(xiàn):
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="Title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Describe"/>
</FrameLayout>
但是注意,以上是在不增加頁(yè)面層級(jí)的情況下,可以用FrameLayout和LinearLayout代替RelativeLayout實(shí)現(xiàn),但是RelativeLayout也有它的優(yōu)點(diǎn),利用它的各種相對(duì)屬性可以減少我們的頁(yè)面層級(jí),所以總的來(lái)說(shuō)就是,如果能減少頁(yè)面層級(jí)可以考慮采用RelativeLayout,如果是相同層級(jí)的情況下,優(yōu)先考慮采用FrameLayout和LinearLayout。
另外,很多時(shí)候我們?yōu)榱朔奖阆矚g在LinearLayout中,采用它的layout_weight來(lái)為子View設(shè)置顯示的比例,但是layout_weight同樣會(huì)觸發(fā)LinearLayout測(cè)量?jī)杀?,所以慎用?/p>
推薦使用Google推出的一個(gè)約束布局——ConstraintLayout,它的出現(xiàn)主要是為了解決布局嵌套過(guò)多的問(wèn)題,以靈活的方式定位和調(diào)整小部件。它與 RelativeLayout 一樣有相對(duì)的屬性,但性能上比RelativeLayout更勝一籌。另外它還能按照比例約束控件的位置和尺寸。
?
3)采用布局標(biāo)簽減少布局嵌套
Android中提供了幾種布局標(biāo)簽——merge、include、ViewStub。利用它們能夠?yàn)槲覀儨p少很多不必要的嵌套。
include和merge的用法
比如說(shuō)一個(gè)項(xiàng)目中多處使用到一個(gè)重復(fù)的布局,如下:

就是一個(gè)純色背景上面疊加一個(gè)文字居中的布局,布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</FrameLayout>
</FrameLayout>
這樣一方面布局文件顯得很累贅,當(dāng)View多起來(lái)時(shí)不方便查看,另一方面萬(wàn)一項(xiàng)目中要統(tǒng)一更改該布局,也要一處處改動(dòng),這個(gè)時(shí)候可以考慮將我們重復(fù)的部分抽取成一個(gè)xml文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</FrameLayout>
通過(guò) include 標(biāo)簽來(lái)將剛才的布局引入:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/layout_item"/>
</FrameLayout>
但是通過(guò)include只是換了種方式包裹布局,我們使用Layout Inspector查看該頁(yè)面,本質(zhì)上布局層級(jí)還是跟剛才一樣:

這個(gè)時(shí)候就要結(jié)合merge標(biāo)簽來(lái)進(jìn)行合并了,使用merge標(biāo)簽可以幫我們忽略掉我們的子布局的根View,相當(dāng)于直接將子布局添加到我們主布局下,我們將剛才的 layout_item的根FrameLayout改為merge標(biāo)簽:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</merge>
再次運(yùn)行,查看布局層級(jí)如下:

可以看到少了一層
FrameLayout,我們的ImageView和TextView都被直接添加到activity的根FrameLayout下,因此merge經(jīng)常與include搭配使用,減少include場(chǎng)景下的布局嵌套。merge還有其他使用場(chǎng)景,比如Activity的xml文件的根View是FrameLayout,并且只有寬高,沒(méi)有設(shè)置任何其他屬性時(shí),可以考慮采用merge來(lái)替換它,因?yàn)槲覀儀ml根View的父View其實(shí)也是個(gè)FrameLayout,所以實(shí)際上是重疊了兩層。
但是,merge標(biāo)簽有很多要注意的地方:
merge標(biāo)簽必須使用在根布局(這也正是為何推薦搭配include使用的原因)
merge會(huì)幫我們忽略掉根View,因此根View的布局屬性也全都會(huì)失效,會(huì)直接采用主布局中其父View的布局屬性
merge標(biāo)簽本質(zhì)上不是一個(gè)View,對(duì)它設(shè)置的任何布局屬性都是沒(méi)有意義的,并且在通過(guò)LayoutInflate.inflate()方法獲取它的時(shí)候,第二個(gè)參數(shù)必須指定一個(gè)父容器,且第三個(gè)參數(shù)必須為true,也就是必須為merge下的視圖指定一個(gè)父親節(jié)點(diǎn)。(詳見(jiàn)我另一篇 LayoutInflater.inflate各個(gè)參數(shù)作用了解一下?)
?
ViewStub的用法
ViewStub可以用來(lái)包裹布局,被包裹的布局在頁(yè)面加載時(shí)是沒(méi)有被加載出來(lái)的,只有調(diào)用了viewStub.inflate()或者viewStub.setVisible()時(shí),才會(huì)被加載出來(lái),也就是類(lèi)似一種懶加載的機(jī)制,很適用于用來(lái)包裹我們的一些缺省布局,比如無(wú)網(wǎng)絡(luò)提示、加載錯(cuò)誤提示等等,或者一些不需要頁(yè)面一啟動(dòng)就顯示出來(lái)的View,因?yàn)檫@些布局不一定會(huì)展示給用戶(hù),如果全部寫(xiě)在layout文件里面的話,頁(yè)面加載的時(shí)候無(wú)論可不可見(jiàn)實(shí)際上都是會(huì)加載出來(lái)的(注意區(qū)分加載和可見(jiàn)的概念)。
以無(wú)網(wǎng)絡(luò)提示為例子,首先定義我們的無(wú)網(wǎng)絡(luò)布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_nonet"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Check Your Internet Connection"/>
</LinearLayout>
將其通過(guò)ViewStub引入到主布局文件中:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/no_net_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/layout_no_net"/>
<TextView
android:id="@+id/loading_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="正在加載中..."/>
</FrameLayout>
在Activity中模擬無(wú)網(wǎng)絡(luò)提示,讓其延遲2秒加載:
viewStub = findViewById(R.id.no_net_view);
loadingTv = findViewById(R.id.loading_tv);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
loadingTv.setVisibility(View.GONE);
viewStub.inflate();
}
}, 2000);
效果如下:

ViewStub使用時(shí)也有一些要注意的點(diǎn):
ViewStub通過(guò)inflate來(lái)進(jìn)行布局的渲染,但是該方法只能調(diào)用一次。
ViewStub包裹的布局根部不能是merge標(biāo)簽
?
結(jié)語(yǔ)
Android開(kāi)發(fā)中布局優(yōu)化不是一兩天的事,日常開(kāi)發(fā)中靈活運(yùn)用,根據(jù)場(chǎng)景所需采用對(duì)應(yīng)的優(yōu)化策略,雖然這些都是比較小的細(xì)節(jié)處理,但是老話說(shuō)的好,細(xì)節(jié)決定成敗,養(yǎng)成優(yōu)化的習(xí)慣,才能讓你的頁(yè)面體驗(yàn)縱享絲滑。
?
關(guān)于作者
一個(gè)在奮斗路上的Android小生,歡迎關(guān)注,互相交流Android開(kāi)發(fā)的那些事~