『Android性能優(yōu)化手冊(cè)』布局分析與調(diào)優(yōu)

前言

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中的圖層:

View層級(jí)示意圖

因此如果同一個(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):

GPU過(guò)度繪制開(kāi)關(guān)

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

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ò)度繪制

過(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:

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è)目錄下:

Layout Inspector預(yù)覽面板

一共有三塊區(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性能分析)。因此性能上不如LinearLayoutFrameLayout,比如下面這個(gè)布局:

布局優(yōu)化demo

我們可以采用 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í)的情況下,可以用FrameLayoutLinearLayout代替RelativeLayout實(shí)現(xiàn),但是RelativeLayout也有它的優(yōu)點(diǎn),利用它的各種相對(duì)屬性可以減少我們的頁(yè)面層級(jí),所以總的來(lái)說(shuō)就是,如果能減少頁(yè)面層級(jí)可以考慮采用RelativeLayout,如果是相同層級(jí)的情況下,優(yōu)先考慮采用FrameLayoutLinearLayout
另外,很多時(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)簽——mergeinclude、ViewStub。利用它們能夠?yàn)槲覀儨p少很多不必要的嵌套。

include和merge的用法

比如說(shuō)一個(gè)項(xiàng)目中多處使用到一個(gè)重復(fù)的布局,如下:


布局優(yōu)化demo

就是一個(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í)還是跟剛才一樣:

include包裹之后的布局層級(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í)如下:

merge優(yōu)化后布局層級(jí)

可以看到少了一層FrameLayout,我們的ImageViewTextView都被直接添加到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效果圖.gif

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ā)的那些事~

GitHubGitHubZJY
CSDN博客IT_ZJYANG
簡(jiǎn) 書(shū)Android小Y

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 靜靜的陪你走了好遠(yuǎn)好遠(yuǎn), 連眼睛紅了都沒(méi)有發(fā)現(xiàn)。 聽(tīng)著你說(shuō)你現(xiàn)在的改變, 看著我依然最?lèi)?ài)你的笑臉。 這條舊路依然沒(méi)...
    賈小呆520閱讀 772評(píng)論 0 0
  • 當(dāng)一個(gè)中間件調(diào)用 next() 則該函數(shù)暫停并將控制傳遞給定義的下一個(gè)中間件。當(dāng)在下游沒(méi)有更多的中間件執(zhí)行后,堆棧...
    我的昵稱(chēng)好聽(tīng)嗎閱讀 6,992評(píng)論 0 2
  • 蜜兒吃小螞蟻閱讀 279評(píng)論 0 1
  • 在中國(guó)古代人們喜歡用“身懷六甲”來(lái)形容一個(gè)懷有身孕的人 那么“六甲”是什么意思 與懷孕又有什么關(guān)系呢? 《西游記》...
    打不死的小編閱讀 939評(píng)論 0 0

友情鏈接更多精彩內(nèi)容