文章参考:https://developer.android.com/studio/profile/inspect-gpu-rendering#debug_overdraw

文章参考:https://www.jianshu.com/p/4f44a178c547

Android优化典范:https://www.youtube.com/watch?v=CaMTIgxCSqU

https://www.youtube.com/watch?v=T52v50r-JfE

Android应用就是将我们的UI布局渲染成界面展示给用户。用户和界面之间进行交互。所以界面的流畅度就非常重要。因此,有个良好的UI布局会对App的性能产生比较大的影响,如果布局写得糟糕,显而易见App的表现不可能流畅。就会使得应用比较卡顿,甚至出现丢帧的情况存在。

UI布局中有很多原因可以导致丢帧,这里列举一些常见的:

  • 1、layout 太过复杂,层次过多,无法在16ms内完成渲染;
  • 2、UI 上有层叠太多的绘制单元,过度绘制,使得某些像素被绘制多次。
  • 3、CPU 或者 GPU 负载过重
  • 4、同一时间内动画执行的次数过多,导致CPU和GPU负载过重
  • 5、频繁 GC,主要是内存抖动(短时间内有大量的对象创建和销毁,导致GC占用产生大量的暂停时间)
  • 6、UI 线程执行耗时操作

等等

60fps VS 16ms

Google官方出品的《Android性能优化典范》,60帧每秒是目前最合适的图像显示速度,事实上绝大多数的Android设备也是按照每秒60帧来刷新的。要想画面保持在60fs。我们就需要屏幕在1秒之内刷新60次,也就是每隔16ms刷新一次。Android系统也会每隔16ms发出VSYNC信号。对UI进行渲染。

我们可以想象一下。假设我们16ms无法完成一帧的刷新。那么Android系统在渲染的时候,可能会出现两次渲染的是同一帧画面。这样就会显示画面比较卡顿。

所以我们务必要保证稳定的帧率来避免卡顿。

避免过度绘制

我们知道View的绘制流程主要有三个步骤:分别measure、layout和draw 他们主要运行在系统的应用框架层。而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger服务来完成的。

绘制过程主要由CPU来进行Measure、Layout、Record、Excute的数据计算。然后GPU来进行栅格化、渲染。

而CPU和GPU则是通过图形驱动层来进行连接的。图形驱动层维护了一个队列。CPU将display list添加到这个队列中。这样GPU就可以从这个队列中拉取出数据进行绘制。

View的绘制是需要消耗CPU和GPU的。所以我们要极力避免View的过度绘制。理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制。就是产生过度绘制。

过度绘制使得CPU和GPU的负载加重,导致界面卡顿甚至丢帧。

查看过度绘制

我们可以通过调试工具来检测Overdraw:设置——开发者选项——调试GPU过度绘制——显示过度绘制区域。

  • 1、原色 – 没有过度绘制 – 这部分的像素点只在屏幕上绘制了一次。
  • 2、蓝色 – 1次过度绘制– 这部分的像素点只在屏幕上绘制了两次。
  • 3、绿色 – 2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。
  • 4、粉色 – 3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。
  • 5、红色 – 4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。

在实际项目中,一般认为蓝色即是可以接受的颜色(即最多绘制次数不要超过两次)。

我们从实际的项目中,可以基本判断引起过度绘制的原因:

  • 1、最好是去掉容器布局的背景颜色,背景颜色只由子View布局来实现绘制。
  • 2、着重分析View的层叠布局,确定层叠布局中只有一层View设置背景颜色。

一个容易忽略的点是我们的Activity使用的Theme可能会默认的加上背景色,不需要的情况下可以去掉。

而有些过度绘制可能是无法避免的,这个就需要根据时机的业务场景进行分析。

减少嵌套层次及控件个数

Android的布局文件的加载是LayoutInflater利用pull解析方式来解析,然后根据节点名通过反射的方式创建出View对象实例;

同时嵌套子View的位置受父View的影响,类如RelativeLayout、LinearLayout等经常需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长;

由此得到结论:那么随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。

LayoutInspector的使用

在最新的Android Studio我们可以通过Layout Inspector

关于关于LayoutInspector的使用,这里也不再赘述。

Hierarchy Viewer

Hierarchy Viewer这个方便可视化的工具,可以得到:树形结构总览、布局view、每一个View(包含子View)绘制所花费的时间及View总个数。

备注: Hierarchy Viewer不能连接真机的问题可以通过ViewServer这个库解决;

关于Hierarchy Viewer的使用方式,这里不再赘述:

https://juejin.im/post/5ac0ea1af265da23884d3403

https://blog.csdn.net/u012792686/article/details/72921379

但是,Hierarchy Viewer在最新的版本Android Studio中已经废弃不用。 这个需要注意!!

Profiling GPU Rendering

根据Android性能优化典范,打开设备的GPU配置渲染工具->在屏幕上显示为条形图,可以协助我们定位UI渲染问题。

image

从Android M版本开始,GPU Profiling工具把渲染操作拆解成如下8个详细的步骤进行显示。

  • 1、Swap Buffers(橙色条形图):表示处理任务的时间,是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复。也可以说是CPU等待GPU完成任务的时间,线条越高,表示GPU做的事情越多;
  • 2、Command Issue(红色条形图):表示执行任务的时间,这部分主要是Android进行2D渲染显示列表的时间,为了将内容绘制到屏幕上,Android需要使用Open GL ES的API接口来绘制显示列表,红色线条越高表示需要绘制的视图更多;
  • 3、Sync & Upload(蓝色条形图):表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小;
  • 4、Draw(深蓝色条形图):表示测量和绘制视图列表所需要的时间,蓝色线条越高表示每一帧需要更新很多视图,或者View的onDraw方法中做了耗时操作;
  • 5、Measure/Layout:表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题;
  • 6、Animation:表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等;
  • 7、Input Handling:表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作;
  • 8、Misc Time/Vsync Delay:表示在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况;出现该线条的时候,可以在Log中看到这样的日志:

布局优化

善于使用XML标签

1.Merge标签

merge可以用来合并布局,减少布局的层级。merge多用于替换顶层FrameLayout或者include布局时,用于消除因为引用布局导致的多余嵌套。

例如:需要显示一个Button,布局如下;

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge标签演示" />
</LinearLayout>

我们通过UiAutoMatorViewer(无需root,相比Hierarchy Viewer只能查看布局层次,不能得到绘制时间)看一下布局的层次。

我们使用Merge标签对代码进行修改;

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?> 
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge标签演示" />
</merge >

可以看到使用Merge标签进行优化之后布局嵌套就少了一层,Button作为父视图第三层FrameLayout的直接子视图。

注意:merge标签常用于减少布局嵌套层次,但是只能用于根布局。

2.ViewStub标签

推迟创建对象、延迟初始化,不仅可以提高性能,也可以节省内存(初始化对象不被创建)。Android定义了ViewStub类,ViewStub是轻量级且不可见的视图,它没有大小,没有绘制功能,也不参与measure和layout,资源消耗非常低。

1
2
3
4
5
<ViewStub
android:id="@+id/mask"
android:layout="@layout/b_me_mask"
android:layout_width="match_parent"
android:layout_height="match_parent" />
1
2
ViewStub viewStub = (ViewStub)view.findViewById(R.id.mask);
viewStub.inflate();

App里常见的视图如蒙层、小红点,以及网络错误、没有数据等公共视图,使用频率并不高,如果每一次都参与绘制其实是浪费资源的,都可以借助ViewStub标签进行延迟初始化,仅当使用时才去初始化。

3.include标签

include标签和布局性能关系不大,主要用于布局重用,一般和merge标签配合使用,因和本文主题关联不大,此处不展开讨论。

其他优化方式

1.自定义控件时,注意在onDraw不能进行复杂运算;以及对待三方UI库选择高性能;

2.内存对布局的影响:如同Misc Time/Vsync Delay步骤产生的影响,在之后内存优化的篇章详细讲。

3.在LinearLayout和RelativeLayout都可以完成布局的情况下,优先选择LinearLayout。可以见啥View的层级,但是注意相同组件可能RelativeLayout绘制时间长。

布局优化总结

布局优化的通用套路:

  • 调试GPU过度绘制,将Overdraw降低到合理范围内;
  • 减少嵌套层次及控件个数,保持view的树形结构尽量扁平(可以配合Hierarchy Viewer或者Layout Inspector可以方便的查看),同时移除所有不需要渲染的view;
  • 使用GPU配置渲染工具,定位出问题发生在具体哪个步骤,使用TraceView精准定位代码;
  • 使用标签,Merge减少嵌套层次、ViewStub延迟初始化。

经过这几步的优化之后,一般就不会再有布局的性能问题,同时还是要强调:优化是一个长期的工作,同时也必须结合具体场景:有取有舍!