(二)布局渲染流程与原理 —— 优化卡顿

版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、CPU 和 GPU

1.区别

CPU 需要很强的通用性来处理逻辑计算、内存管理、显示操作等,都使得CPU的内部结构异常复杂。因此,在没有 GPU 的时代,CPU 的实际运算性能跟不上今天复杂三维游戏的要求,这时候 GPU 的设计出来了。GPU 面对的是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。

这里写图片描述

黄色的 Control 为控制器,用于协调控制整个 CPU 的运行,包括取出指令、控制其他模块的运行等;
绿色的 ALU ( Arithmetic Logic Unit )是算术逻辑单元,用于进行数学、逻辑运算;
橙色的 Cache 和 DRAM 分别为缓存和 RAM ,用于存储信息。

从结构图可以看出, CPU 的控制器较为复杂,而 ALU 数量较少。因此 CPU 擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。

这里写图片描述

2.在绘制中作用

1.这是一个在布局文件中的按钮。

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

2.通过 LayoutInflater 加载到内存中,变成一个 Button 对象,这个对象含有 left、top、right、buttom、width、height 等信息。

3.CPU 经过计算,把 Button 对象处理成类似 svg 的多维向量图像。

4.CPU 将向量图像交给 GPU,GPU 进行栅格化,然后绘制,负责像素填充。

栅格化:就是讲向量图形格式表示的图像转换成位图以用于显示器。
这里写图片描述

二、卡顿原理

1.60Hz 刷新频率由来

12 fps :由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约 10-12 帧的时候,就会认为是连贯的

24 fps :有声电影的拍摄及播放帧率均为每秒 24 帧,对一般人而言已算可接受

30 fps :早期的高动态电子游戏,帧率少于每秒 30 帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低

60 fps 在与手机交互过程中,如触摸和反馈 60 帧以下人是能感觉出来的。 60 帧以上不能察觉变化

当帧率低于 60 fps 时感觉的画面的卡顿和迟滞现象

2.绘制计算

从上面可以知道,一个 UI 对象,需要经过 CPU 转化成多维向量图像,再经过 GPU 进行栅格化。

栅格化完成后进行刷新显示。但是界面不是已计算好就进行实时刷新的,需要等待底层 UI 渲染。

Android 系统每隔 16ms 发出 VSYNC 信号 (1000ms/60=16.66ms) ,触发对 UI 进行渲染, 如果每次计算都成功这样就能够达到流畅的画面所需要的 60fps ,为了能够实现 60fps ,这意味着计算绘制内容的大多数操作都必须在 16ms 内完成。

3.卡顿原理

当某一帧画面的渲染计算时间超过 16ms 的时候,垂直同步机制会让显示器硬件等待 CPU 和 GPU 计算完成后再进行渲染操作。

这样会让这一帧画面多停留 16ms 甚至更多,看起来的效果就像是卡顿。

举个例子:下方绿色这一帧计算需要 16ms, 红色的这一帧计算需要用到 20ms,当时间到 16ms 时,UI 进行渲染,把绿色这一帧绘制出来,当时间到达 32ms 的时候,发现红色这一帧还没计算好,所以显示的还是绿色这一帧,直到 48ms 的时候,发现红色这一帧已经计算好了,才会显示红色这一帧。

这里写图片描述

注:虽然红色这一帧在 36ms 就计算好了,但是不会马上进行显示,需要等待下一次 UI 渲染。

4.卡顿优化

通过上面分析,我们可以知道,在 16ms 内,渲染计算没有完成的话,将造成卡顿。这个过程主要做两件事情:

第一件:将 UI 对象转换为一系列多边形和纹理
第二件: CPU 传递处理数据到 GPU 。

所以很明显,我们要缩短这两部分的时间,也就是说需要尽量减少对象转换的次数,以及上传数据的次数。

CPU 减少 xml 转换成对象的时间。
GPU 减少重复绘制的时间。

三、嵌套布局过深

这个主要就是尽量减少嵌套布局,对于较较复杂的布局,使用 RelativeLayout 替代多层次的 LinearLayout。

1. Hierarchy Viewer

我们可以借助一些工具来查看我们的布局层级。现在 Hierarchy Viewer 在 SDK 里找不到了,google 已经把它隐藏起来。并且 Hierarchy Viewer 只能在开发版手机或模拟器运行的限制。

目前的 Hierarchy Viewer 工具已经处于废弃状态,试了很多次用不了。我们可以前往
http://mirrors.zzu.edu.cn/android/repository/ 下载旧版本的tools。

这里写图片描述

2.Layout Inspector

现在 Android Studio 已经不支持 Hierarchy Viewer,也可以使用 Layout Inspector 进行替代查看布局。

正常情况下,一个 ViewGroup 下如果只有一个 ViewGroup(scrollview 嵌套例外),那就要考虑这层布局是否需要,不需要的话就应该去掉,以减少 CPU 对 xml 解析转话为对象的时间。

四、过度绘制

1.过度绘制

GPU 的绘制过程,就跟刷墙一样,一层层的进行。刷墙的时候,假如先刷一层红色,接着再刷一层白色。虽然我们看到的是白色,但是墙是刷了两遍的。

GPU 的绘制过程中也会出现这样的情况,先绘制了一个图层,然后再绘制一个图层覆盖在上面,即无用的图层被绘制在底层,造成不必要的浪费。

过渡绘制主要分为两种情况:

1.自定义控件中,onDraw 方法做了过多的重复绘制。

2.布局层次太深,重叠性太强,多次进行绘制。

2.过度绘制查看工具

在手机端的开发者选项里,有 OverDraw 监测工具,调试 GPU 过度绘制工具,其中颜色代表渲染的图层情况,分别代表1层,2层,3层,4层覆盖。
这里写图片描述

蓝色:绘制一次,无过度绘制
淡绿:绘制两次
淡红:绘制三次
深红:绘制四次及以上

这是我的模拟器上的设置,有色盲的可以选择第三个选项。
这里写图片描述

这是开启后的界面效果。
这里写图片描述

3.计算渲染的耗时

当 View 中的绘制内容发生变化时,会重新执行创建 DisplayList,渲染 DisplayList,更新到屏幕上等一 系列操作。这个流程的表现性能取决于你的 View 的复杂程度,View 的状态变化以及渲染管道的执行性能。

举个例子,当View 的大小发生改变,DisplayList 就会重新创建,然后再渲染,而当 View 发生位移,则 DisplayList 不会重新创建,而是执行重新渲染的操作。

打开设置中 GPU 呈现模式分析:
这里写图片描述

打开后效果:
这里写图片描述

界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。

每一条柱状线都包含三部分,

蓝色:测量绘制Display List的时间。
红色:OpenGL 渲染 Display List 所需要的时间。 
黄色:CPU等待GPU处理的时间。

注: OpenGL ES 是手持嵌入式设备的 3DAPI,跨平台的、功能完善的 2D 和 3D 图形应用程序接口 API,有一套固定渲染管线流程。

DisplayList 在 Android 把 XML 布局文件转换成 GPU 能够识别并绘制的对象。这个操作是在 DisplayList 的帮助下完成的。DisplayList 持有所有将要交给 GPU 绘制到屏幕上的数据信息。

4.安卓系统的优化

CPU 转移到 GPU 是一件很麻烦的事情,所幸的是 OpenGL ES 可以把那些需要渲染的纹理 Hold 在 GPU Memory 里面,在下次需要渲染的时候直接进行操作。所以如果你更新了 GPU 所 hold 住的纹理内容,那么之前保存的状态就丢失了。

在 Android 里面那些由主题所提供的资源,例如 Bitmaps , Drawables 都是一起打包到统一的 Texture 纹理当中,然后再传递到 GPU 里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着 UI 组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过 CPU 的计算加载到内存中,然后传递给 GPU 进行渲染。文字的显示比较复杂,需要先经过 CPU 换算成纹理,然后交给 GPU 进行渲染,返回到 CPU 绘制单个字符的时候,再重新引用经过 GPU 渲染的内容。动画则存在一个更加复杂的操作流程。

为了能够使得 App 流畅,我们需要在每帧 16ms 以内处理完所有的 CPU 与 GPU 的计算,绘制,渲染等等操作。

5.自定义控件过度绘制

比较典型的就是自拍叠加效果。

这里写图片描述

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (DroidCard c : mDroidCards){
            drawDroidCard(canvas, c);
        }

        invalidate();
    }

    /**
     * 绘制DroidCard
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }

这个自定义控件没有进行计算,直接连续绘制多张图片,后面绘制的图片会盖住前面绘制图片的部分区域,导致过度绘制。对于这样的,可以进行逻辑计算,对第一张图片要显示的内容进行裁剪,然后再进行绘制。

优化后代码:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < mDroidCards.size() - 1; i++){
            drawDroidCard(canvas, mDroidCards,i);
        }

        drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
        invalidate();
    }

    /**
     * 绘制最后一个DroidCard
     * @param canvas
     * @param c
     */
    private void drawLastDroidCard(Canvas canvas,DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }

    /**
     * 绘制DroidCard
     * @param canvas
     * @param mDroidCards
     * @param i
     */
    private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
        DroidCard c = mDroidCards.get(i);
        canvas.save();
        canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.restore();
    }

优化后效果:
这里写图片描述

5.布局过度绘制

布局过度绘制造成的原因,主要是给深层次的布局控件设置了多个背景颜色,导致其进行多次绘制。

1.去掉主题样式的默认背景

    <style name="Theme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowBackground">@null</item>
    </style>

如果 Activity 的背景样式不是必要的,把他置空,这样会少绘制一层。

2.去掉容器背景

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="内容"
        android:background="@android:color/white"/>

</LinearLayout>

效果:
这里写图片描述

这是一个很简单的布局,可以看到,除了 TextView 外的区域全部被进行了一次绘制,TextView 除了文字区域,被绘制了两次,呈现绿色。如果说我们只是为了单纯设置背景为白色的话,TextView 是不需要再进行背景颜色的设置,因为他的背景已经是白色的了。

修改后布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="内容"/>

</LinearLayout>

修改后效果:
这里写图片描述

这两个显示出来的效果是一样的,但是 TextView 所在的区域,少进行了一次绘制。

小结:我们应该尽量保证界面大部分呈现为蓝色、绿色。

五、总结

布局优化:

1.布局是否有可以删除的多层嵌套

2.布局是否有多层背景,过度绘制

3.自定义控件是都有进行裁剪
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值