UI优化

系统在渲染UI时会消耗大量的资源,一个好的UI不仅该具有良好的视觉效果,还应该具有良好的使用体验。布局优化的任务就让UI在使用过程中不产生卡顿,让APP有更好的用户体验。

UI渲染机制和检测工具

渲染机制

在Android中,系统通过VSYNC信号触发UI的渲染和重绘,信号发送的间隔时间是16ms。如果系统每次渲染的时间都控制在16ms之内,那么,UI界面会非常流畅,否则,就会造成丢帧,从而能使界面产生卡顿。例如,一次绘制任务耗时20ms,那么,在16ms系统发出VSYNC信号时就无法进行绘制,该帧只能等待下次VSYNC信号来临时才被绘制,这样,16*2ms内都是显示同一帧画面,用户在视觉上就会产生卡顿的感觉。

检测工具

Android系统提供了检测UI渲染时间的调试工具。打开“开发者选项”,选择 “Profile GPU Rendering” ,然后选中 “On screen as bars” 选项,这时在屏幕上会显示如下图形:

GPU渲染时间

图中每条柱状图由多种颜色组成,每种颜色的长度表示在渲染时,各个绘制过程消耗的时间。中间的绿色横线表示VSYNC间隔时间16ms,一个良好的UI需要尽量将所有条状图都控制在这条绿线之下。每种颜色表示的绘制过程如下图所示:

GPU渲染的各个过程

当知道了每个绘制过程的时间后,就可以针对特定的绘制过程进行优化。

Input Handing

输入处理(Input)颜色的长度表示APP在输入事件回调方法中,处理事件花费的时间长短。

如果这方面时间花费太多,表示在处理输入事件时做了太多工作,由于回调方法是在主线程上进行的,可以将繁重的工作放到子线程中进行,从而达到优化效果。

另外,RecyclerView的滑动也属于输入处理,因为滑动会消耗触摸事件,所以,要尽可能快地布局和填充子View。

Animation

动画(Anim)颜色的长度表示当前帧执行完所有动画花费的时间。这些常见的动画包括ObjectAnimator, ViewPropertyAnimator和Transitions.

如果这方面时间花费太多,一般都是执行动画引起属性变化造成的。比如,ListView或RecyclerView在执行fling动画时会快速滑动,引起大量子View的布局和重绘,从而造成执行动画时间过长。

Measurement和Layout

测量和布局(Measure)颜色表示系统在测量和布局View时花费的时间。

如果这方面花费时间太多,一般都是自定义View时,onLayout()和onMeasure()方法执行时间过长造成的。可以用Traceview和Systrace工具检查代码中这样的问题。

Drawing

绘制(Draw)颜色表示当前帧绘制View花费的时间,包括所有View执行onDraw()和dispatchDraw()方法的时间。

如果这方面花费时间太多,一般都是自定View时,onDraw()方法的逻辑太过复杂造成的。

Sync和Upload

同步和上传(Upload)颜色表示当前帧中,将Bitmap对象从CPU内存转移到GPU内存花费的时间。

Android系统在绘制Bitmap时,需要将Bitmap从CPU传递到GPU,然后GPU再进行绘制。注意,在5.0系统上,这个时间用紫色表示。

如果这方面花费时间太多,一般来说是由两种情况造成的:一个是当前帧显示了一个接近屏幕大小的Bitmap;另一个是当前帧显示了大量缩略图。

如果要减少这方面的时间,可以使用以下两种方法:

  • 在显示图片时,不要让高分辨率的图片在低分辨率的屏幕上显示。
  • 在CPU和GPU内存同步内存之前,利用prepareToDraw()方法异步上传Bitmap对象。

Issuing Commands

发布命令(Issue)颜色表示绘制屏幕时,发布所有绘制命令到显示列表(Display Lists)花费的时间。

由于系统绘制屏幕时,会向GPU发布必要的绘制命令,通常这些操作是通过OpenGL的API实现的。如果这方面花费的时间太多,表明在绘制时有太多的绘制命令需要发布。比如:

1
2
for (int i = 0; i < 1000; i++)
canvas.drawPoint()

完成以上绘制的发布的绘制命令就完成以下绘制的要多,这样就会在发布命令上花费更多时间。

1
canvas.drawPoints(mThousandPointArray);

所以,在自定义View时,尽量减少draw操作的数量,可以减少发布命令的时间。

Processing和Swapping Buffers

CPU发布绘制命令和GPU执行绘制命令的操作是并行的,当CPU发布的速度快于GPU执行的速度时,命令队列会被填满,这时CPU就会被堵塞。处理和交换缓冲(Swap)的颜色表示的这时CPU等待GPU花费的时间。一般CPU被堵塞发生在交换缓冲区(Swap Buffers)阶段,因为在这个阶段,绘制一帧的所有绘制命令会被一起提交给GPU,造成GPU没时间处理CPU接下来发布的绘制命令。

减少这方面时间消耗用到方法与 “Issuing Commands” 中用到的方法一样。

Miscellaneous

除了渲染UI,在主线程上还有很多其它操作,其它(Misc)颜色就是用来表示其它操作花费的时间。一般来说,其他时间就指在UI线程上连续绘制两帧之间的时间间隔。

如果这方面花费时间太多,表明在主线程中做了太多工作,可以将这些工作放到其它线程中。Method Tracing和Systrace工具可以可视化主线程中的任务,这些可视化的信息可以帮助进行相关优化工作。

避免过度绘制

过度绘制(Overdraw)会浪费很多系统资源,比如,系统会默认绘制Activity的背景,如果再给根布局绘制背景,那么默认的Activity背景就属于无效的过度绘制。

Android系统提供了 “Enable GPU Overdraw” 调试工具来检查UI的过度绘制。在“开发者选项”中打开这个选项后,如下图所示:

过度绘制

可以通过界面上的颜色来判断Overdraw的次数:

  • 没有颜色:没有过度绘制;
  • 蓝色:过度绘制1次;
  • 绿色:过度绘制2次;
  • 粉色:过度绘制3次;
  • 红色:过度绘制4次或更多次;

一般可以使用以下几种方式减少过度绘制次数:

  • 移除布局中无用的背景

可以使用Layout Inspector工具找出那些对用户不可见的背景,然后移除这些无用的背景属性。还可以将APP的window背景颜色设置为主背景,那么所有的根布局的就不需要背景色了,从而减少过度绘制次数。

  • 减少布局层级

在Android中,系统对View进行测量、布局和绘制时,都是通过对View树的遍历进行操作的。如果一个布局的层级太深,就会严重影响这些操作的速度,并且还会增加过度绘制次数。所以,在定义布局时,应该尽量减少布局层级,这样不但可以提高渲染速度,还能减少过度绘制次数。

  • 减少使用透明度

绘制具有透明度属性的像素会增加过度绘制,和一般的过度绘制不一样,带有透明度的对象要求先绘制对象原本的颜色,然后再去绘制对象的透明度值,这样,一个带透明度属性的对象就需要绘制两次。像透明度视觉效果的动画,比如淡入淡出和阴影就是涉及到透明度属性,会造成过度绘制。可以通过减少使用带有透明度属性的对象来改善这种形式的过度绘制。

避免无用布局

在“过度绘制”中已经提到过,布局层级太深会影响View进行测量、布局和绘制的速度,并且还会增加过度绘制次数。所以,要尽量避免无用布局,去减少布局的层级。下面介绍两种常用的避免无用布局的方法。

  • 使用merge标签

通常会使用include标签重用layout,但是,使用include标签一般会在子View上增加一层根布局,有时在使用include中的子View时,可能并不需要这层根布局,这时就可以使用merge标签代替这个根布局。这样,子View会直接填充到include的位置,不会再添加任何额外的布局结构。

  • 使用ViewStub标签

通常会在一个布局中隐藏一个子布局,在需要的时候再去显示。但是,即使将子布局进行隐藏,它们还是存在于布局中,在渲染UI时,还是会遍历这些隐藏的子布局,这时就可以使用ViewStub标签。

ViewStub虽然是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,将它放置在布局当中,基本上不会影响UI性能。

不过,ViewStub加载的布局是不能使用merge标签的,因此,有可能导致加载出来的布局存在多余的布局结构。具体如何去取舍要根据实际情况来决定,对于那些隐藏的布局文件结构相当复杂的情况,使用ViewStub还是一种不错的选择。

Hierarchy Viewer

可以使用Hierarchy Viewer工具查看布局的View树结构,并测量每个View的绘制速度。这些信息可以帮助我们对UI进行优化。

坚持原创技术分享,您的支持将鼓励我继续创作!