過度繪制分析及解決方案

過度繪制

繪制原理

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



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



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

掉幀

掉幀是用戶體驗中一個非常核心的問題最域。丟棄了當前幀谴分,并且之后不能夠延續(xù)之前的幀率,這種不連續(xù)的間隔會容易會引起用戶的注意镀脂,也就是我們
常說的卡頓牺蹄、不流暢。
掉幀的原因很多薄翅,比如:

  • ViewTree非常龐大沙兰,花了很多時間重新繪制界面中的控件,這樣非常浪費CPU周期:


  • 過度繪制嚴重翘魄,在繪制用戶看不到的對象上花費了太多的時間:


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

UI繪制機制

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


其實這圖我也看得不太懂训措,但知道那么兩個解決問題的方法:

  • 利用 Android Studio 自帶的 Hierarchy Viewer 去檢測各 View 層的繪制時間,刪除或合并圖層
  • 打開手機的 ShowGPUOverdraw去檢測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/

簡單使用

打開ViewTree視圖后怀大,點擊任意一個view,然后點擊Profile Node即可展示每個view在各個階段的耗時情況呀闻,如:



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

階段 耗時
Measure 0.028ms
Layout 0.434ms
Draw 9.312ms

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

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

用Show GPU Overdraw方法來檢測

過度繪制的檢測

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

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

其中藍色部分表示1層過度繪制憎账,紅色表示4層過度繪制套硼。

解決方案

移除不必要的background

下面舉個簡單的例子:

  • 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="對唔住,我喺差人胞皱。" />
</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"可以去掉;

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


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


于是,我們需要對RecyclerView的ItemDecoration類進行改造壶运,改造成如下:

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 方法以實現(xiàn)recyclerview的item之間的間隙的背景
     * @param c 畫布
     * @param parent 使用該 ItemDecoration 的 RecyclerView 對象實例
     * @param state 使用該 ItemDecoration 的 RecyclerView 對象實例的狀態(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中的坐標
            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;
    }
}

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

這時打開“顯示過度繪制”功能:


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

總結(jié)

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

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

后記

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市牌里,隨后出現(xiàn)的幾起案子含衔,更是在濱河造成了極大的恐慌,老刑警劉巖二庵,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贪染,死亡現(xiàn)場離奇詭異,居然都是意外死亡催享,警方通過查閱死者的電腦和手機杭隙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來因妙,“玉大人痰憎,你說我怎么就攤上這事∨屎” “怎么了铣耘?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長以故。 經(jīng)常有香客問我蜗细,道長,這世上最難降的妖魔是什么怒详? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任炉媒,我火速辦了婚禮,結(jié)果婚禮上昆烁,老公的妹妹穿的比我還像新娘吊骤。我一直安慰自己,他們只是感情好静尼,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布白粉。 她就那樣靜靜地躺著传泊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸭巴。 梳的紋絲不亂的頭發(fā)上眷细,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天往堡,我揣著相機與錄音棚菊,去河邊找鬼。 笑死正卧,一個胖子當著我的面吹牛惯豆,可吹牛的內(nèi)容都是我干的池磁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼楷兽,長吁一口氣:“原來是場噩夢啊……” “哼地熄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起芯杀,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤端考,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后揭厚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體却特,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年筛圆,在試婚紗的時候發(fā)現(xiàn)自己被綠了裂明。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡太援,死狀恐怖闽晦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情提岔,我是刑警寧澤仙蛉,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站碱蒙,受9級特大地震影響荠瘪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜振亮,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一巧还、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坊秸,春花似錦、人聲如沸澎怒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至星瘾,卻和暖如春走孽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背琳状。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工磕瓷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人念逞。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓困食,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翎承。 傳聞我的和親對象是個殘疾皇子硕盹,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,116評論 25 707
  • Tangram是阿里出品、用于快速實現(xiàn)組合布局的框架模型叨咖,在手機天貓Android&iOS版 內(nèi)廣泛使用 該框架提...
    wintersweett閱讀 3,296評論 0 1
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,759評論 22 665
  • 香玉年幼時父母雙亡瘩例,她跟著哥哥嫂子一起生活。哥嫂的孩子多甸各,從小到大垛贤,她一直給哥嫂做家務(wù),帶孩子趣倾,稍不留心聘惦,就會...
    欣然_bd23閱讀 374評論 1 11
  • “今年過年不回家了√茏茫”她掛斷電話部凑,只記得了這一句。 一間小屋陷入一片沉寂碧浊。 春節(jié)的戰(zhàn)斗沒打響之前涂邀,鄰居偶爾還看到她...
    墨先森閱讀 396評論 0 0