过度绘制分析及解决方案

过度绘制

绘制原理

Android系统要求每一帧都要在 16ms 内绘制完成,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。这个速度允许系统在动画和输入事件的过程中以约 60 帧每秒( 1秒 / 0.016帧每秒 = 62.5帧/秒 )的平滑帧率来渲染。



如果应用没有在 16ms 内完成这一帧的绘制,假设你花了 24ms 来绘制这一帧,那么就会出现掉帧的情况。



系统准备将新的一帧绘制到屏幕上,但是这一帧并没有准备好,所有就不会有绘制操作,画面也就不会刷新。反馈到用户身上,就是用户盯着同一张
图看了 32ms 而不是 16ms ,也就是说掉帧发生了。

掉帧

掉帧是用户体验中一个非常核心的问题。丢弃了当前帧,并且之后不能够延续之前的帧率,这种不连续的间隔会容易会引起用户的注意,也就是我们
常说的卡顿、不流畅。
掉帧的原因很多,比如:

  • ViewTree非常庞大,花了很多时间重新绘制界面中的控件,这样非常浪费CPU周期:


  • 过度绘制严重,在绘制用户看不到的对象上花费了太多的时间:


  • 大量动画多次重复,消耗CPU和GPU
  • 频繁地触发GC机制
    目前我们的项目的 APP 卡顿现象主要是由于 ViewTree 过于庞大和过度绘制严重造成

UI绘制机制

在现在的设备上,UI绘制主要由CPU和GPU协作完成,其工作原理如下图:


其实这图我也看得不太懂,但知道那么两个解决问题的方法:

  • 利用 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节点后有72个子view,还可以读出数据:

阶段 耗时
Measure 0.028ms
Layout 0.434ms
Draw 9.312ms

在ViewTree中查找可删减或合并的view,找到耗时严重的view加以改良,可以减轻过度绘制现象,例如:

例如图中的两个LinearLayout只要保留一个就够了,而前面这个RecyclerView的item只有一个,没必要使用RecyclerView,可以考虑用其它view来替代

用Show GPU Overdraw方法来检测

过度绘制的检测

按照以下步骤打开ShowGPUOverrdraw的选项:
设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制

打开后,屏幕会有多种颜色,切换到需要检测的应用程序,对于各个色块,有一张参考图:

其中蓝色部分表示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;
    }
}

现在看起来的效果是这样的:


图中,我们需要上方展示的部分背景为白色,而下方列表Item之间的颜色为灰色,item的背景为白色。
打开显示过度绘制功能后是这样的:


图中可以看到很多区域出现了三重或四重的过度绘制现象。那么我们开始去掉不必要的background。

  • 不必要的background 1:

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

  • 不必要的background 2:

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

去掉这两个background后,我们重新安装一下应用程序,发现界面上方过度绘制现象明显改善:


但界面下方仍存在过度绘制现象,若把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));//设置背景色
    }
    public void setSpace(int space) {
        this.halfSpace = space / 2;
    }
    /**
     *
     * 重写onDraw 方法以实现recyclerview的item之间的间隙的背景
     * @param c 画布
     * @param parent 使用该 ItemDecoration 的 RecyclerView 对象实例
     * @param state 使用该 ItemDecoration 的 RecyclerView 对象实例的状态
     */
    @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的间隙画上了带背景色的矩形,于是我们想要的效果又回来了:

这时打开“显示过度绘制”功能:


已经是可以接受的效果了。

总结

解决过度绘制现象,可以从这两个方法入手:

  • 利用 Hierarchy Viewer 观察整个界面的ViewTree,删掉无用的图层,找到能合并的view合并,找到红点图层分析原因;
  • 查看各图层的background,去掉不必要的background;
  • 对于列表中Item之间的间隙颜色,不要在列表的 background 设置,应该在列表应用的 ItemDecoration 中设置

后记

本文已上传至GitHub https://github.com/EKwongChum/OverDraw
欢迎指出问题,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,870评论 25 707
  • Tangram是阿里出品、用于快速实现组合布局的框架模型,在手机天猫Android&iOS版 内广泛使用 该框架提...
    wintersweett阅读 3,270评论 0 1
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,739评论 22 665
  • 香玉年幼时父母双亡,她跟着哥哥嫂子一起生活。哥嫂的孩子多,从小到大,她一直给哥嫂做家务,带孩子,稍不留心,就会...
    欣然_bd23阅读 370评论 1 11
  • “今年过年不回家了。”她挂断电话,只记得了这一句。 一间小屋陷入一片沉寂。 春节的战斗没打响之前,邻居偶尔还看到她...
    墨先森阅读 396评论 0 0