简介
之所以有这一章节内容,主要还是考虑APP优化性能这一块。对于优化性能一般从:渲染、运算和内存、电量 这三个方面进行考量的。所以还是有必要要了解一下Android的渲染机制。
我们知道Android系统每隔16ms就会重新绘制一次页面,也就是说,我们的应用必须要在16ms 内完成屏幕刷新的全部逻辑操作,即每一帧只能停留16ms,渲染机制理清楚后再去考虑如何优化UI卡顿,才是正确的探索之路。
16ms机制
为什么是16ms?
16ms意味着 1000/60Hz ≈ 16.6,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。 24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps 是电影 胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能 够最大的减少费用支出。 但是低于 30fps 是 无法顺畅表现绚丽的画面内容的,此时就需要 用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内 完成屏幕刷新的全部逻辑操作,就会发生卡顿。
为什么16ms未完成绘制会卡顿?
Android 系统每隔16ms就发出一次 VSYNC
信号,触发对 UI 进行渲染。VSYNC 是 Vertical Synchronization(垂直同步)的缩写,是一种在 PC 上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在 Android 4.1(JB)中已经开始引入 VSync 机制。
上图所示是 VSync
机制下的绘制流程图。从上图可以看出,CPU 和 GPU 的处理时间都是少于一个 VSync 的间隔的,即16.6ms。如果每个间隔都有绘制的情形下,当前的 fps即为 60帧。
当CPU和GPU处理时间都比较慢,或者因为其他原因,导致主线程中 do heavy work ,那么就会出现如下图所示的状况:
如上图所示,CPU 和 GPU 的第一个处理时间因为各种原因均大于一个 VSync 间隔(16.6ms),所以在第二个 VSync 还在处理 1 区域的绘制时,就不可能实现理论上的 FPS60,同时也出现了丢帧(SF: Skipped Frame)情况,试想用户盯着同一张图看了32ms而非16ms,当然很容易察觉出卡顿,哪怕是出现仅有的一次,用户也能感觉得到不流畅,因此找出导致没能完成绘制的罪魁祸首就能进行实质性的性能优化工作,做到事半功倍。
渲染原理
旋绕操作通常依赖于两个核心组件:CPU 和 GPU。CPU负责包括 Measure,Layout,Record,Execute 的计算操作;GPU负责 Rasterization(栅格化) 操作。所谓的栅格化就是绘制那些 Button、Shape、Path、String、Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行展示,说得通俗一点,就是解决那些复杂的XML布局文件和标记语言,使之转化为用户可以看得懂的图像,但是这不是直接转换的,XML 布局文件需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行格栅化,而对于栅格化,这里跟 OpneGL 有关。格栅化是一个特别费时的操作。
因此,16ms 的时间主要是被两件事所占用,第一:将UI对象转换为一系列多边形和纹理操作;第二:CPU传递处理数据到GPU。所以很明显,只要缩短这两部分时间即可,也就是说需要尽量减少对象转换次数,以及传递数据的次数。
在 CPU 方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中 进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多, 二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他 缓存 GPU 资源时导致 CPU 工作过度。在 GPU 方面,最常见的问题是我们所说的过度绘制 (overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了 GPU 处理 时间。下面我们对 GPU 和 CPU 产生的两大问题进行优化。
CPU 产生的问题:不必要的布局和失效
GPU 产生的问题:过度绘制(overdraw)
过渡绘制(overdraw)检测
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次 的 UI 结构里面, 如果不可见的 UI 也在做绘制的操作,这就会导致某些像素区域被绘制了 多次。这就浪费大量的 CPU 以及 GPU 资源。
按照以下步骤打开 Show GPU Overdraw 的选项:设置 -> 开发者选项 -> 调试 GPU 过度绘 制 -> 显示 GPU 过度绘制
Overdraw 的处理方案
Overdraw 处理方案一:去掉 window 的默认背景
当使用 Android 自带某些主题时,window 会被默认添加一个纯色背景,这个背景是被 DecorView 所持有的。
当我们的自定义布局时又添加了一张背景 图或者设置背景色,那么 DecorView 的 background 此时对我们来说是无用的, 但是它会产生一次 Overdraw,带来绘制性能损耗。
处理方法:
可以在 onCreate() 中 setContentView() 之后调用 getWindow().setBackgroundDrawable(null); 或者在 theme 中添加 android:windowbackground="null";
Overdraw 处理方案二:去掉其他不必要的背景
有时为了方便会先给 layout 设置一个整体的背景,再给子 View 设置背景,这时也会造成重叠绘制,如果子 View 宽度为 match_parent,则会覆盖 layout 的部分,因此这里可以通过分别设置背景来减少重绘;
再比如如果采用的是 selector 作为背景,将 normal 状态的 color 设置为
“@android:color/transparent”
,也可以解决同样问题;等等…
Overdraw 处理方案三:clipRect 的使用
通过 canvas.clipRect() 来帮助系统识别可见区域。
处理方法:
指定一块矩形区域,并且只可以在这个区域操作绘制,其他区域忽视。
方案优势:
这个系统API,可以很好的帮助处理那些有多组重叠组件的自定义 View 来控制显示的区域 同时clipRect 还可以帮助节约 CPU 和 GPU 的资源开销,在 clipRect 区域之外的绘制指令是不会执行的。
Overdraw 处理方案四:ViewStub
ViewStub
又称为 “延迟化加载”,程序无需显示 ViewStub 所 指向的布局文件,只有在特定的某些条件下,此时 ViewStub 所指向的布局 文件才需要被 inflate,且此布局文件直接将当前 ViewStub 替换掉,具体是通 过 viewStub.infalte()或 viewStub.setVisibility(View.VISIBLE)来完成;示例 xml 如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/network_error_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/empty_view" />
</RelativeLayout>private void showNetError() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}
ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
networkErrorView = stub.inflate();
Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
Button refresh = (Button)findViewById(R.id.network_refresh);
}
private void showNormal() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}Overdraw 处理方案五:Merge 标签
Merge 标签可以干掉一个 view 层级。
Merge 的作用很明显,但是也有一些使用 条件的限制。有两种情况下我们可以使用 Merge 标签来做容器控件。
使用场景:
第一种:子视图不需要指定任何针对父视图的布局属性,也就是说父容器仅仅作为容器, 子视图只需要直接添加到父视图上用于显示就行 第二种:子视图的 “根” 布局 和 父布局(视图)一样的,这样就多一层无用的嵌套。
【注1】
只可以作为xml layout的根节点。 【注2】 当需要扩充的xml layout本身是由 merge 作为根节点的话,需要将被导入的xml layout置于viewGroup中,同时需要设置 attachToRoot 为True。
Hierarchy Viewer - 优化布局层次
HV 的使用
Hierarchy Viewer 接触过 Android 的人估计都用过,如果在真机上可以 使用 ViewServer 这个第三方库:https://github.com/romainguy/ViewServer, 配置步骤比较简单,主要分为如下三步:
(1)在根 build.gradle 文件中加入allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
(2)在 Module 的 build.gradle 文件中加入dependencies {
...................................
compile 'com.github.romainguy:ViewServer:017c01cd512cac3ec054d9eee05fc48c5a9d2de'
}
(3)加上访问网络权限,在 Activity 添加下列代码public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set content view, etc.
ViewServer.get(this).addWindow(this);
}
public void onDestroy() {
super.onDestroy();
ViewServer.get(this).removeWindow(this);
}
public void onResume() {
super.onResume();
ViewServer.get(this).setFocusedWindow(this);
}
但是,它只能在root过的机器上才能使用,可以帮我们减少 View 层,在 Hierarchy Viewer 窗口中,所有的子 View 上面都有3个圈圈(取色范围是红、黄、绿),这三个圈圈分别代表 measure,layout、draw 的速度,并且可以看到实际的运行速度,如果发现某个 View 的圈是红色的,那么说明这个 View 相对其他的 View,其操作运行较慢,当然这只是相对别的 View。
布局常见问题及优化建议
没有用的父布局时指没有背景绘制或者没有大小限制的父布局,这样的布局不会 对 UI 效果产生任何影响。我们可以把没有用的父布局,通过
标签合并 来减少 UI 的层次; 使用线性布局 LinearLayout 排版导致 UI 层次变深,如果有这类问题,我们就使用相对布局 RelativeLayout 代替 LinearLayout,减少 UI 的层次;
不常用的 UI 被设置成 GONE,比如异常的错误页面,如果有这类问题,我们需要用
标签,代替 GONE 提高 UI 性能。