過度繪制分析及解決方案

過度繪制

繪制原理

Android系統(tǒng)要求每一幀都要在 16ms 內(nèi)繪制完成,平滑的完成一幀意味著任何特殊的幀需要執(zhí)行所有的渲染代碼(包括 framework 發(fā)送給 GPU 和CPU 繪制到緩沖區(qū)的命令)都要在 16ms 內(nèi)完成,保持流暢的體驗(yàn)。這個(gè)速度允許系統(tǒng)在動(dòng)畫和輸入事件的過程中以約 60 幀每秒( 1秒 / 0.016幀每秒 = 62.5幀/秒 )的平滑幀率來渲染。



如果應(yīng)用沒有在 16ms 內(nèi)完成這一幀的繪制,假設(shè)你花了 24ms 來繪制這一幀,那么就會(huì)出現(xiàn)掉幀的情況。



系統(tǒng)準(zhǔn)備將新的一幀繪制到屏幕上,但是這一幀并沒有準(zhǔn)備好,所有就不會(huì)有繪制操作,畫面也就不會(huì)刷新。反饋到用戶身上,就是用戶盯著同一張
圖看了 32ms 而不是 16ms ,也就是說掉幀發(fā)生了。

掉幀

掉幀是用戶體驗(yàn)中一個(gè)非常核心的問題。丟棄了當(dāng)前幀,并且之后不能夠延續(xù)之前的幀率,這種不連續(xù)的間隔會(huì)容易會(huì)引起用戶的注意,也就是我們
常說的卡頓、不流暢。
掉幀的原因很多,比如:

  • ViewTree非常龐大,花了很多時(shí)間重新繪制界面中的控件,這樣非常浪費(fèi)CPU周期:


  • 過度繪制嚴(yán)重,在繪制用戶看不到的對(duì)象上花費(fèi)了太多的時(shí)間:


  • 大量動(dòng)畫多次重復(fù),消耗CPU和GPU
  • 頻繁地觸發(fā)GC機(jī)制
    目前我們的項(xiàng)目的 APP 卡頓現(xiàn)象主要是由于 ViewTree 過于龐大和過度繪制嚴(yán)重造成

UI繪制機(jī)制

在現(xiàn)在的設(shè)備上,UI繪制主要由CPU和GPU協(xié)作完成,其工作原理如下圖:


其實(shí)這圖我也看得不太懂,但知道那么兩個(gè)解決問題的方法:

  • 利用 Android Studio 自帶的 Hierarchy Viewer 去檢測(cè)各 View 層的繪制時(shí)間,刪除或合并圖層
  • 打開手機(jī)的 ShowGPUOverdraw去檢測(cè)Overdraw,移除不必要的background

Hierarchy Viewer 的使用

Hierarchy Viewer

Hierarchy Viewer工具在Android device monitor中
在Mac的Android Studio中:

圖片來源http://blog.csdn.net/lmj623565791/article/details/45556391/

在windows的Android Studio中:

那么如何使用呢?

圖片來源http://blog.csdn.net/lmj623565791/article/details/45556391/

簡(jiǎn)單使用

打開ViewTree視圖后,點(diǎn)擊任意一個(gè)view,然后點(diǎn)擊Profile Node即可展示每個(gè)view在各個(gè)階段的耗時(shí)情況,如:



圖中可以看到,該view在Measure、Layout和Draw階段都比其它view耗時(shí)要多(下面的點(diǎn)變成紅色了),圖中看出該view節(jié)點(diǎn)后有72個(gè)子view,還可以讀出數(shù)據(jù):

階段 耗時(shí)
Measure 0.028ms
Layout 0.434ms
Draw 9.312ms

在ViewTree中查找可刪減或合并的view,找到耗時(shí)嚴(yán)重的view加以改良,可以減輕過度繪制現(xiàn)象,例如:

例如圖中的兩個(gè)LinearLayout只要保留一個(gè)就夠了,而前面這個(gè)RecyclerView的item只有一個(gè),沒必要使用RecyclerView,可以考慮用其它view來替代

用Show GPU Overdraw方法來檢測(cè)

過度繪制的檢測(cè)

按照以下步驟打開ShowGPUOverrdraw的選項(xiàng):
設(shè)置 -> 開發(fā)者選項(xiàng) -> 調(diào)試GPU過度繪制 -> 顯示GPU過度繪制

打開后,屏幕會(huì)有多種顏色,切換到需要檢測(cè)的應(yīng)用程序,對(duì)于各個(gè)色塊,有一張參考圖:

其中藍(lán)色部分表示1層過度繪制,紅色表示4層過度繪制。

解決方案

移除不必要的background

下面舉個(gè)簡(jiǎn)單的例子:

  • activity_main 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical"
    tools:context="com.example.erkang.overdraw.MainActivity">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        card_view:cardBackgroundColor="@color/white">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white">

            <TextView
                android:id="@+id/title_tv"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:paddingBottom="5dp"
                android:paddingTop="5dp"
                android:text="OverDraw展示樣式" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="100dp"
                android:layout_below="@+id/title_tv"
                android:layout_marginBottom="5dp"
                android:scaleType="fitCenter"
                android:src="@drawable/infernal_affairs_0" />
        </RelativeLayout>
    </android.support.v7.widget.CardView>

    <View
        android:layout_width="match_parent"
        android:layout_height="20dp" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/gray" />
</LinearLayout>
  • RecyclerView的item的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center">
    <ImageView
        android:layout_marginLeft="10dp"
        android:id="@+id/item_iv"
        android:layout_width="100dp"
        android:layout_height="100dp"
        tools:src="@drawable/infernal_affairs_1" />
    <TextView
        android:layout_marginLeft="10dp"
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="對(duì)唔住,我喺差人。" />
</LinearLayout>
  • Activity代碼:
public class MainActivity extends AppCompatActivity {
    private MyAdapter myAdapter;
    private RecyclerView recyclerView;
    private static final int ITEM_COUNT = 20;
    private static final int ITEM_DISTANCE = 40;
    private LinearLayoutManager layoutManager;
    private MyItemDecoration myItemDecoration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportActionBar().hide();
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        myAdapter = new MyAdapter(MainActivity.this, ITEM_COUNT);
        layoutManager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);
        myItemDecoration = new MyItemDecoration(ITEM_DISTANCE);
        recyclerView.addItemDecoration(myItemDecoration);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(myAdapter);
    }
}
  • ItemDecoration代碼:
public class MyItemDecoration extends RecyclerView.ItemDecoration{
    protected int halfSpace;

    /**
     * @param space item之間的間隙
     */
    public MyItemDecoration(int space){
        setSpace(space);
    }
    public void setSpace(int space) {
        this.halfSpace = space / 2;
    }
  
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.top = halfSpace;
        outRect.bottom = halfSpace;
        outRect.left = halfSpace;
        outRect.right = halfSpace;
    }
}

現(xiàn)在看起來的效果是這樣的:


圖中,我們需要上方展示的部分背景為白色,而下方列表Item之間的顏色為灰色,item的背景為白色。
打開顯示過度繪制功能后是這樣的:


圖中可以看到很多區(qū)域出現(xiàn)了三重或四重的過度繪制現(xiàn)象。那么我們開始去掉不必要的background。

  • 不必要的background 1:

總布局LinearLayout中的 android:background="@color/white" 可以去掉;

  • 不必要的background 2:

上方布局RelativeLayout中的android:background="@color/white"可以去掉;

去掉這兩個(gè)background后,我們重新安裝一下應(yīng)用程序,發(fā)現(xiàn)界面上方過度繪制現(xiàn)象明顯改善:


但界面下方仍存在過度繪制現(xiàn)象,若把RecyclerView中的background值的灰色去掉,則下方列表Item之間的就會(huì)變成白色,顯然不是我們想要的效
果:


于是,我們需要對(duì)RecyclerView的ItemDecoration類進(jìn)行改造,改造成如下:

public class MyItemDecoration extends RecyclerView.ItemDecoration{
    protected int halfSpace;
    private Paint paint;
    /**
     * @param space item之間的間隙
     */
    public MyItemDecoration(int space, Context context) {
        setSpace(space);
        paint = new Paint();
        paint.setAntiAlias(true);//抗鋸齒
        paint.setColor(context.getResources().getColor(R.color.gray));//設(shè)置背景色
    }
    public void setSpace(int space) {
        this.halfSpace = space / 2;
    }
    /**
     *
     * 重寫onDraw 方法以實(shí)現(xiàn)recyclerview的item之間的間隙的背景
     * @param c 畫布
     * @param parent 使用該 ItemDecoration 的 RecyclerView 對(duì)象實(shí)例
     * @param state 使用該 ItemDecoration 的 RecyclerView 對(duì)象實(shí)例的狀態(tài)
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int outLeft, outTop, outRight, outBottom,viewLeft,viewTop,viewRight,viewBottom;
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            viewLeft = view.getLeft();
            viewTop = view.getTop();
            viewRight = view.getRight();
            viewBottom = view.getBottom();
// item外層的rect在RecyclerView中的坐標(biāo)
            outLeft = viewLeft - halfSpace;
            outTop = viewTop - halfSpace;
            outRight = viewRight + halfSpace;
            outBottom = viewBottom + halfSpace;
//item 上方的矩形
            c.drawRect(outLeft, outTop, outRight,viewTop, paint);
//item 左邊的矩形
            c.drawRect(outLeft,viewTop,viewLeft,viewBottom,paint);
//item 右邊的矩形
            c.drawRect(viewRight,viewTop,outRight,viewBottom,paint);
//item 下方的矩形
            c.drawRect(outLeft,viewBottom,outRight,outBottom,paint);
        }
    }
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.top = halfSpace;
        outRect.bottom = halfSpace;
        outRect.left = halfSpace;
        outRect.right = halfSpace;
    }
}

其實(shí)就是增加了onDraw方法,在item的間隙畫上了帶背景色的矩形,于是我們想要的效果又回來了:

這時(shí)打開“顯示過度繪制”功能:


已經(jīng)是可以接受的效果了。

總結(jié)

解決過度繪制現(xiàn)象,可以從這兩個(gè)方法入手:

  • 利用 Hierarchy Viewer 觀察整個(gè)界面的ViewTree,刪掉無用的圖層,找到能合并的view合并,找到紅點(diǎn)圖層分析原因;
  • 查看各圖層的background,去掉不必要的background;
  • 對(duì)于列表中Item之間的間隙顏色,不要在列表的 background 設(shè)置,應(yīng)該在列表應(yīng)用的 ItemDecoration 中設(shè)置

后記

本文已上傳至GitHub https://github.com/EKwongChum/OverDraw
歡迎指出問題,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,115評(píng)論 25 708
  • Tangram是阿里出品、用于快速實(shí)現(xiàn)組合布局的框架模型,在手機(jī)天貓Android&iOS版 內(nèi)廣泛使用 該框架提...
    wintersweett閱讀 3,344評(píng)論 0 1
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,877評(píng)論 22 665
  • 香玉年幼時(shí)父母雙亡,她跟著哥哥嫂子一起生活。哥嫂的孩子多,從小到大,她一直給哥嫂做家務(wù),帶孩子,稍不留心,就會(huì)...
    欣然_bd23閱讀 391評(píng)論 1 11
  • “今年過年不回家了。”她掛斷電話,只記得了這一句。 一間小屋陷入一片沉寂。 春節(jié)的戰(zhàn)斗沒打響之前,鄰居偶爾還看到她...
    墨先森閱讀 402評(píng)論 0 0