一、布局优化
总是首先想到的也是最直观的优化方向。具体的优化方式有:
- 尽量减少布局中的控件层级,减少嵌套。布局中需要嵌套时不要使用 LinerLayout,改用 RelativeLayout。简单层级布局建议使用 FrameLayout、LinerLayout、RelativeLayout 次之。
要找到布局或页面中多余的 View,可以使用 Android Studio Monitor 里的 Hierarachy Viewer 工具。参考:Hierarchy Viewer使用详解
- 使用
<include>
,<merge>
进行布局的套用,ViewStub 按需加载。
<include>
无需多说,<merge>
标签一般配合<include>
使用。被 Include 的布局使用<merge>
标签,引入时<merge>
会被忽略让该布局跟随引用者的布局模式。
ViewStub 是一种轻量级的 View,需要显示时必须在 UI 线程加载。
- 相关工具:除了上文所说的 Hierarachy Viewer 工具,Android 还提供了 Lint 工具。Lint Tool 不仅会提示布局文件使用不当,还会提示其它的代码需要优化的地方,比如 项目中有无用的 import、某些代码可能产生不同版本适配问题等。
H Viewer 和 Lint Tool 的使用可以参考:Android App优化之Layout怎么摆
在此记录自己项目中有关 Lint Tool 的使用:
(1)打开 Lint Tool:
(2)选择要检测整个项目或者某个 Moudule 或具体文件的问题。
默认会选择当前打开的文件,我这里选择第一项查看整个项目存在的问题。
(3)查看问题
如上图,这个项目不算太大但是问题还真不少...上图中选了一个 XML 的问题点击链接到布局文件中存在无用的引用。
但是这里提一下,Lint 工具检测出来的很大一部分属于警告,只是提示开发者或许不该这样或那样做。具体怎样做还是需要开发者自行查看并根据具体的情况对代码和逻辑进行处理。
二、 绘制优化
- 一方面指 View 的 onDraw 方法要避免大量的操作,包括不要在 onDraw 中创建局部变量,因为 onDraw 是一个经常被调用的方法。同时也不要在 onDraw 中进行耗时任务。
- 另一方面指过度绘制,所谓的过度绘制是指 屏幕上某些像素点在一帧中被重复绘制多次,就是过度绘制。
查看是否过度绘制可以通过开启手机 开发者选项 -- 调试 GPU 过度绘制 -- 显示过度绘制区域 选项,下面举例说明:
(1)开启 "显示过度绘制区域" 选项:
可以看到屏幕变成花花绿绿的一片了,不同的颜色表示这块 View 绘制的次数:
- 原色:没有 OverDraw
- 蓝色:一次 OverDraw
- 绿色:两次 OverDraw
- 粉色:三次 OverDraw
- 红色:四次及以上 OverDraw
那么设置选项中存在原色、蓝色、绿色和粉色,一般来说不超过粉色或许不存在过度绘制的情况。因为整个设置选项比较简单,也就是选项的那些滑块进行了多次绘制也情有可原。
(2)查看 APP 绘制情况,那么就拿 简书APP 来举例吧
这个页面看到了系统状态栏(原色)、简书底部导航栏图片(蓝色)、标题栏 TitleBar(绿色)、标题栏文字以及背景(粉色)、Item 字体(红色)。
这样看的话个人猜测简书 APP 这个页面重复设置了一些 background,也不能说就是特别严重的过度绘制,因为这样做的原因是为了优化用户体验。
(3)过度绘制优化方法:
- 去除 Activity 自带的默认背景颜色:
具体做法是在 Activity 使用的主题中进行设置 Background 为 null。当然也要视情况而定,如果你的 Activity 自定义了背景色,那么就可以替换默认的背景色。如果不需要背景色就可以直接删除。
<style name="AppTheme" parent="android:Theme.Light.NoTitleBar">
<item name="android:windowBackground">@null</item>
</style>
或者在相关 Activity 中:
getWindow().setBackgroundDrawable(null);
- 使用 Canvas 的 clipRect() 和 clipPath() 方法限制 View 的绘制区域
一个 Activity 对应一个 Canvas 对象,用来绘制该 Activity 的所有内容。clipRect() 是 Canvas 提供的一个方法,用来裁剪画布上的一个矩形区域,该矩形区域用 Rect 对象来描述。调用该方法后 Canvas 的绘制范围会被限制在裁剪的范围内。
举个栗子:
上图是 Pixel XL 手机的文件管理器,左侧是打开的 DrawerLayout,背景布局用来显示具体文件。按照一般的绘制逻辑,背景布局绘制后打开 DrawerLayout,打开的布局绘制次数应该更多才是。但是打开的 DrawerLayout 明显比背景布局绘制次数少,那么原因就要到 DrawerLayout 的源码去找了。
View 绘制流程中,draw 绘制的过程中会执行 dispatchDraw() 方法。这个方法的主要作用是用来绘制绘制子 View,所以是由 ViewGroup 进行了重写,在 ViewGroup 中该方法遍历所有的子 View 并执行 drawChild() 方法让子 View 执行 draw。ViewGroup 默认的 drawChild() 方法直接调用了子 View 的 draw 方法,但是 DrawerLayout 重写了 drawChild() 方法按照自己的逻辑来绘制自己的子 View。
DrawerLayout#drawChild()
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final int height = getHeight();
final boolean drawingContent = isContentView(child);
// 记录 DrawerLayout 的左右边界
int clipLeft = 0, clipRight = getWidth();
// 保存画布
final int restoreCount = canvas.save();
// 判断是否绘制内容
if (drawingContent) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
if (v == child || v.getVisibility() != VISIBLE
|| !hasOpaqueBackground(v) || !isDrawerView(v)
|| v.getHeight() < height) {
// 如果child是内容视图/不可见/视图背景透明/不是抽屉视图/child高度小于父布局高度
// 跳过循环不做裁切
continue;
}
if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
// 抽屉在左侧时记录右侧边界
final int vright = v.getRight();
if (vright > clipLeft) clipLeft = vright;
} else {
// 反之抽屉在右侧,记录左侧边界
final int vleft = v.getLeft();
if (vleft < clipRight) clipRight = vleft;
}
}
// 进行裁切
canvas.clipRect(clipLeft, 0, clipRight, getHeight());
}
// 绘制其它子 View
final boolean result = super.drawChild(canvas, child, drawingTime);
// 画布返回之前状态
canvas.restoreToCount(restoreCount);
...
return result;
}
Android 屏幕绘制是一帧一帧来进行的,当 DrawerLayout 被打开时不断执行它的 drawChild() 方法来记录 DrawerLayout 的边界信息:left、top、right、bottom。当 DrawerLayout 打开时将未被挡住的背景内容区域裁切出来,裁切完毕后把储存着绘制信息的 canvas 交给自己的父 ViewGroup 进行绘制,所以会先绘制未被挡住的背景内容区域。最后再返回之前画布状态绘制其它内容。
- ImageView 的 src、background、ImageDrawable
通过 src 设置的 ImageView 图片不会拉伸,可以设置 scaleType 进行图片的比例设置;而通过 background 设置的背景图片会拉伸至整个 ImageView;ImageDrawable 一般用来设置需要加载的图片,一般在代码中使用,当 background 和 ImageDrawable 同时使用时会产生过度绘制的问题,解决方法为都使用 ImageDrawable。
三、UI 卡顿
Android 著名的 "16ms" 法则:
Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).
为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.
如果某页面本该在下一个 16ms 完成绘制而没有完成,而是在 30ms 的时候才绘制完成,那么在下一个 16ms 不会更新视图而是在第 32ms 更新,这就造成了丢帧的现象。
其实 UI 卡顿利用上面记录的一些方法来处理已经基本能解决了,如果想了解更多可以搜索其它工具来找出问题如:StrictMode、Systrace 等。
参考文章: