AndroidUI優(yōu)化實(shí)踐

前言

當(dāng)項(xiàng)目發(fā)展到一定規(guī)模后,我們都會(huì)遇到性能瓶頸,黑屏、卡頓等,其中一個(gè)原因就是我們App某些頁面布局過于復(fù)雜,重繪嚴(yán)重。為了解決此問題,我們需要利用某些UI調(diào)試工具,優(yōu)化布局。

相關(guān)知識(shí)點(diǎn)

1.Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)對(duì)UI進(jìn)行渲染,如果每次渲染都成功,這樣就能夠達(dá)到流暢的畫面所需要的60fps,為了能夠?qū)崿F(xiàn)60fps,這意味著程序的大多數(shù)操作都必須在16ms內(nèi)完成。Android性能優(yōu)化典范(胡凱譯)

在此,我們需要明白一個(gè)概念。如果一次UI渲染不能在16ms內(nèi)完成,那么將會(huì)出現(xiàn)丟幀的現(xiàn)象,從而產(chǎn)生卡頓。譬如,ListView的item布局很復(fù)雜,當(dāng)滑動(dòng)ListView時(shí),就可能發(fā)生卡頓的情況。

2.Overdraw(過度繪制)描述的是屏幕上的某個(gè)像素在同一幀的時(shí)間內(nèi)被繪制了多次。在多層次的UI結(jié)構(gòu)里面,如果不可見的UI也在做繪制的操作,這就會(huì)導(dǎo)致某些像素區(qū)域被繪制了多次。這就浪費(fèi)大量的CPU以及GPU資源。

我們知道手機(jī)屏幕是有X軸和Y軸的概念,其實(shí)在系統(tǒng)中還有一個(gè)Z軸的概念。由WindowManagerService來控制View在Z軸的位置,Z軸“位置高”的View顯示在“位置低”的View之上。如果某一個(gè)像素點(diǎn)有很多View都“路過”,那么就會(huì)導(dǎo)致過度繪制的發(fā)生。

UI優(yōu)化工具

1.hierarchyviewer
hierarchyviewer是android sdk提供的UI分析工具,該工具在sdk目錄下的tools文件夾內(nèi)。使用該工具可以查看某Activity的布局層次,也可以觀察每個(gè)view的measure、layout、draw所需時(shí)間。該工具是UI優(yōu)化一大利器。關(guān)于hierarchyviewer的介紹和使用,網(wǎng)上有大量資料可以查看。所以本文就不再啰嗦了。
2.?調(diào)試GPU過度渲染
該工具是android手機(jī)開發(fā)者選項(xiàng)中提供的一個(gè)UI調(diào)試工具,不同手機(jī)ROM該工具的名字可能不一樣,但一般都會(huì)帶有“過度”的字樣。如下圖是三星某款手機(jī)界面。


過度渲染工具使用

接著我們來使用該工具看看QQ首頁的情況。


QQ界面

打開該工具后,我們會(huì)發(fā)現(xiàn)應(yīng)用界面都變的花花綠綠了。通過觀察這些顏色,我們就可以知道哪些區(qū)域出現(xiàn)過度繪制。那么不同的顏色分別代表什么呢。我們來看下圖。
摘自Android性能優(yōu)化典范

淺藍(lán)色代表屏幕上一個(gè)像素只被繪制了一次,此為最優(yōu)解。
薄荷綠代表屏幕上一個(gè)像素被繪制了兩次,這種情況可以接受。
淺粉色代表屏幕上一個(gè)像素被繪制了三次,這種情況還可以忍受。

紅色代表屏幕上一個(gè)像素被繪制了四次及以上,這種情況就不能忍了。

如果App某個(gè)界面大部分區(qū)域都是紅色,那我們就得好好優(yōu)化該界面了。
?3.GPU呈現(xiàn)模式分析
該工具是android手機(jī)開發(fā)者選項(xiàng)中提供的一個(gè)UI調(diào)試工具,不同手機(jī)ROM該工具的名字可能不一樣,但一般都會(huì)帶有“GPU”的字樣。如下圖是三星某款手機(jī)界面。


GPU呈現(xiàn)模式

我們看到屏幕上有很多不同顏色的“柱子”,不同的顏色代表不同的操作。具體是什么,感興趣的可以查下資料。另外還有一條綠色的水平線,這條線是一條基準(zhǔn)線,代表60fps,即VSYNC 信號(hào)時(shí)間間隔16ms。

上圖兩個(gè)區(qū)域都有柱狀圖,和兩條綠色水平線。這是因?yàn)槊總€(gè)Window都會(huì)有自己GPU呈現(xiàn)模式分析,Activity是一個(gè)Window,Dialog也是一個(gè)Window。

如果大部分“柱子”都超過綠色水平線,說明此頁面出現(xiàn)嚴(yán)重丟幀現(xiàn)象。所以為了避免卡頓的情況,我們應(yīng)該盡量維持在綠色水平線之下。

該工具還有一個(gè)作用,當(dāng)界面正在繪制的時(shí)候,柱狀圖是不會(huì)不停的往前走。如果柱狀圖在動(dòng),我們就知道此時(shí)正在發(fā)生UI繪制。

有一次我在利用該工具優(yōu)化UI的時(shí)候,發(fā)現(xiàn)我們應(yīng)用首頁每個(gè)一段時(shí)間都會(huì)發(fā)生UI繪制。但是肉眼沒有看到View在“動(dòng)”。后來通過打印堆棧信息發(fā)現(xiàn),是某一個(gè)View的動(dòng)畫導(dǎo)致的,因?yàn)樵揤iew在布局最底部,當(dāng)該View滑出屏幕的時(shí)候,其實(shí)動(dòng)畫還是繼續(xù)執(zhí)行。為了解決該問題,我們應(yīng)該做一個(gè)判斷,當(dāng)View為不可見的時(shí)候,停止動(dòng)畫。

UI優(yōu)化情景分析

減少布局層次

1.merge標(biāo)簽的使用。
使用merge可以減少不必要的層級(jí)。
比如,自定義一個(gè)控件且繼承LinearLayout,若該自定義控件需要填充布局,那么此時(shí),我們就可以使用merge標(biāo)簽。

public class TestLinearLayout extends LinearLayout {
    public TestLinearLayout(Context context) {
        super(context);
        init();
    }

    public TestLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TestLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setOrientation(VERTICAL);
        View.inflate(getContext(), R.layout.test_layout, this);
    }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</merge>

上述例子中,使用merge標(biāo)簽將上述布局填充到TestLinearLayout中,因?yàn)門estLinearLayout本身就是繼承LinearLayout,擁有LinearLayout的屬性。如果我們將布局文件修改為:

<?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:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

然后再填充到TestLinearLayout中,這時(shí)候就相當(dāng)于多了一層無用的LinearLayout。
2.ViewStub標(biāo)簽的使用
ViewStub標(biāo)簽使我們很少使用,但又是很重要的一個(gè)標(biāo)簽,該標(biāo)簽的作用是用于懶加載布局,當(dāng)系統(tǒng)碰到ViewStub標(biāo)簽的時(shí)候是不進(jìn)行任何處理(measure、layout等),比設(shè)置View隱藏、不可見更高效。當(dāng)我們真正需要顯示某一個(gè)布局的時(shí)候才去渲染。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

上述布局是系統(tǒng)Activity中某種布局。該布局文件對(duì)Activity布局中的actionBar進(jìn)行了懶加載處理,畢竟不是每個(gè)Activity都需要actionBar。
3.利用高級(jí)View的特殊屬性
1)利用TextView滑動(dòng)屬性來避免嵌套一層ScrollView。
在xml中添加TextView的屬性:

android:scrollbars = "vertical"

然后在代碼中添加:

textView.setMovementMethod(new ScrollingMovementMethod())

2)利用TextView的drawableEnd屬性等設(shè)置icon。
3)在TextView設(shè)置文本中利用換行符達(dá)到上下標(biāo)簽的功能。
4)利用GridLayout來完成宮格布局,GridLayout是非常好使的一個(gè)View,多多使用吧。
4.自定義View來減少布局層級(jí)
這方面的優(yōu)化一般優(yōu)先級(jí)較低,隨著大家修為的提升,這方面的工作也越來越得心應(yīng)手。

減少過渡繪制

1.謹(jǐn)慎設(shè)置View的背景


帶分割線的布局

很多情況下,我們會(huì)有上圖的布局需求。要求每個(gè)條目之間有分割線。
很多情況下我們都是直接給XXLayout設(shè)置背景顏色,然后通過margin來產(chǎn)生分割線。然后在加上Activity的背景顏色和TextView背景顏色,這樣就出現(xiàn)過度繪制的情況。
該情況我們可以通過LinearLayout的分割線屬性來解決,同時(shí)我們也可以自定義一個(gè)ILinearLayout并集成LinearLayout來完善分割線的需求。如下代碼。

public class ILinearLayout extends LinearLayout {

    private Paint mPaint;

    private boolean mHeaderDividersEnable = false;

    private boolean mFooterDividersEnable = false;

    private int mLineWidth = DensityUtil.dip2px(getContext(), 0.5f);

    public ILinearLayout(Context context) {
        super(context);
        initResource();
    }

    /**
     * add custom attributeSet
     **/
    public ILinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        String nameSpace = "http://schemas.android.com/apk/res-auto";
        mHeaderDividersEnable = attrs.getAttributeBooleanValue(nameSpace, "headDividerEnable", false);
        mFooterDividersEnable = attrs.getAttributeBooleanValue(nameSpace, "footerDividerEnable", false);
        initResource();
    }

    public ILinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initResource();
    }

    private void initResource() {
        if (mPaint == null) {
            mPaint = new Paint();
            mPaint.setColor(Color.parseColor("#cccccc"));
            mPaint.setStrokeWidth(mLineWidth);
        }
        setShowDividers(SHOW_DIVIDER_MIDDLE);
        GradientDrawable gd = new GradientDrawable();
        Drawable drawable = getResources().getDrawable(R.drawable.linearlayout_divider);
        setDividerDrawable(drawable);
    }

    /**
     * 設(shè)置頭部線是否可見
     */
    public void setShowHeaderDividers(boolean enable) {
        mHeaderDividersEnable = enable;
    }

    /**
     * 設(shè)置尾部線是否可見
     */
    public void setShowFooterDividers(boolean enable) {
        mFooterDividersEnable = enable;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mHeaderDividersEnable) {
            canvas.drawLine(0, 0, getMeasuredWidth(), 0, mPaint);
        }
        if (mFooterDividersEnable) {
            canvas.drawLine(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight(), mPaint);
        }
    }
}

強(qiáng)烈建議:很多情況的過度繪制都是我們?cè)O(shè)置大片的背景顏色,所以在給布局設(shè)置背景顏色上,我們一定要慎重。

2.大面積不可見區(qū)域盡量不要繪制。


不可見區(qū)域不要繪制!

如上圖,是我們app中碰到的一個(gè)優(yōu)化場(chǎng)景(我見過很多App都有這種界面需求,淘寶、QQ、攜程等),XListView設(shè)置了一張背景圖片。在下拉的時(shí)候,圖片會(huì)慢慢出現(xiàn)。這種方式會(huì)出現(xiàn)兩個(gè)問題:
第一、過度繪制。
第二、圖片過大,導(dǎo)致內(nèi)存消耗過大。
所以我們要把背景圖片不可見的部分不進(jìn)行繪制。
實(shí)現(xiàn)步驟其實(shí)很簡單。
在onDraw方法,畫布canvas的clipRect(RectF rect)方法可以只繪制規(guī)定矩形內(nèi)的區(qū)域。當(dāng)下拉刷新的時(shí)候,下拉刷新的頭高度是在變化的。所以我們只需要計(jì)算出任意時(shí)刻下拉刷新頭的矩形大小就可以動(dòng)態(tài)設(shè)置背景。

寫在最后

以上介紹,是我們優(yōu)化UI的時(shí)候碰到的一些常見問題。當(dāng)App達(dá)到一定用戶規(guī)模的時(shí)候,UI優(yōu)化是一個(gè)不可避免的任務(wù)。當(dāng)然隨著我們能力提升,在寫布局的時(shí)候都會(huì)盡量把布局寫的簡單。另外強(qiáng)大的自定義View能力會(huì)對(duì)我們UI優(yōu)化大有裨益。

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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