Android 之如何优化 UI 渲染(上)

Android 之如何优化 UI 渲染(上)_第1张图片

UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部分内容。


UI 优化系列专题
  • UI 渲染背景知识

《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》

  • 如何优化 UI 渲染

《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》


从产品和设计师的角度,他们自然希望应用可以使用丰富的图形元素、更炫酷的动画来实现流畅的用户体验。但是 Android 系统很有可能无法及时完成这些复杂界面的渲染操作,这个时候就会出现掉帧。也正因如此,我们才需要做 UI 优化。UI 优化要解决的核心问题是,由渲染性能本身造成用户感知的卡顿,可以把它理解为卡顿优化的一个子集。

工欲善其事,必先利其器,在做 UI 优化之前,我们必须先要找到由渲染性能引起卡顿的问题点,有哪些工具可以帮助我们分析和测量呢?今天我们先来聊一聊 UI 渲染的测量方法。

布局分析

在 Android 中,绝大多数视图元素都是通过 xml 布局完成的,接下来我们就从布局优化开始逐步的排查和分析。

1. Hierarchy Viewer

说到布局优化,不得不提 Hierarchy Viewer 这款利器,它是 Android Device Monitor 中内置的一种布局性能分析工具,它通过栅格化获取当前布局界面的显示基元,并将其转换为屏幕像素的过程。

Android 之如何优化 UI 渲染(上)_第2张图片
  • Tree View:显示视图层次结构的树状视图。通过鼠标或底层的缩放控件来拖动和缩放 View 树。每个节点都指示它的 View 类名和 ID 名称。

  • Tree Overview:应用界面完整视图层次结构。移动灰色矩形可更改 Tree View 中可见视图的窗口。

  • Layout View:显示布局线框视图。当前所选视图的轮廓为红色,其父视图为浅红色。

Hierarchy Viewer 可以测量每个视图节点相对于其他同级视图的性能,因此分析结果中总是会有红色节点,红色节点并不一定意味着该节点的布局性能表现不佳。不过它却是当前视图层级中表现最差的。

Android 之如何优化 UI 渲染(上)_第3张图片

所选节点的每个子级都有三个圆点,可以是绿色、黄色或红色

  • 左侧圆点表示渲染管道的绘制过程;

  • 中间圆点表示布局阶段;

  • 右边圆点表示执行阶段。

这些圆点大致对应绘制流程的测量、布局和绘制阶段。圆点的颜色表示在当前层级内相对于其他节点的性能对比。

  • 绿色:表示视图的渲染速度至少比其他一半的视图要快;

  • 黄色:表示视图渲染速度在当前层级处于中间阶段;

  • 红色:表示视图渲染速度在当前层级处于最慢之一。

Android 之如何优化 UI 渲染(上)_第4张图片

需要注意,如果应用的运行速度出乎意料的慢,则红色节点可能是有问题的;在同一个层级内,总有一个最慢的视图节点,但是我们只需要确保它是按照我们“预期”的结果即可。那该如何正确理解是符合预期的红色圆点呢?

  • 查找子节点中的红色圆点或仅包含少数子节点的视图容器。

  • 如果一个视图容器包含许多子视图和一个红色圆点,此时需要进一步了解子视图的渲染情况。

  • 如果一个视图容器具有红色测量阶段、红色布局阶段和黄色绘制阶段,这就属于比较典型的情况,因为它是所有子视图的父容器,并且要在所有子视图完成之后其布局才会完成。

  • 如果具有 20 个以上的视图容器中,某个子视图包含红色绘制阶段,则表示存在问题。需要进一步检查它的绘制操作。

虽然 Hierarchy Viewer 并不能真实反映设备上的实际渲染性能,但是我们可以通过多次分析以了解平均测量结果。总的来看, 它还是能够帮助我们检查布局结构中每个视图模块的渲染性能,以及查找出冗余或导致性能瓶颈的视图节点。

Layout Inspector

不过,在 Android Studio 3.1 及以后, Hierarchy Viewer 已经被弃用,此时 Android 推荐使用 Layout Inspector 来检查应用的视图层次结构。

使用 Layout Inspector 可以将应用布局与设计模型进行比较、显示应用的放大视图,并在运行时检查其布局细节。

Android 之如何优化 UI 渲染(上)_第5张图片
  • View Tree:布局中视图的层次结构;

  • Layout Inspector 工具栏:Layout Inspector 包含的工具;

  • 屏幕截图:设备上显示的应用布局的屏幕截图,其中显示了每个视图的布局边界;

  • Properties Table:选定视图的布局属性。

整体来看,Layout Inspector 并没有太多亮点功能,相反它还取消了 Hierarchy Viewer 中有关布局性能的分析信息。Hierarchy Viewer 接入真机调试也非常容易,具体你可以参考这里。

2. Use Lint

在布局文件中运行 Lint 工具,以搜索视图结构中可能潜在的问题,将始终是一个好的习惯!Lint 取代了早期 Layoutopt 工具,并且已经默认被集成到 Android Studio。

无论何时编译项目 Lint 都会自动运行,检查 Android 源文件是否存在潜在的错误,并提供修改建议以及可以直接跳转到问题代码进行审查。那 Lint 可以帮助我们检查哪些布局问题呢?

  • 使用复合绘图 — 仅包含一个 ImageView 和 TextView 的 LinearLayout 可以替换为更有效的复合 Drawable,具体可以参考 Compound Drawables。
TextView tv = (TextView) findViewById( R.id.textView );
tv.setCompoundDrawablesWithIntrinsicBounds( 0, R.drawable.ic_launcher, 0, 0 );
  • 合并根视图 — 如果 FrameLayout 是布局的根视图,并且不提供背景或填充,那么此时可以使用 merge 标签来替换它。

  • 无用的子视图 — 没有子元素或没有背景的布局通常可以被删除(因为它是不可见的),从而获得更加扁平、有效的布局结构。

  • 无用的父视图 — 一个没有子视图的的布局元素,如果不是 ScrollView 或者跟视图,也没有 Background 是可以被移除的。并且让它的子元素直接移动到父视图中。从而获得更加扁平、有效的布局结构。

  • 布局深度 — 布局层级越深展开花费时间就越长,应尽可能保持布局扁平化设计,如 RelativeLayout 或 GridLayout 来提高性能。它们默认最大深度为 10。不过现在 Android 更加推荐使用 ConstraintLayout。

下图展示了 Lint 工具如何检查应用源文件,源文件包括:Java、Kotlin 和 XML 文件、图标以及 ProGuard 配置文件等。有关 Lint 的更多配置你可以参考《使用 Lint 检查改进您的代码》

Android 之如何优化 UI 渲染(上)_第6张图片

渲染测量

1. Show GPU Overdraw

在 Android 4.2,系统增加了检测过渡绘制(Overdraw)的工具,通过对应用界面进行颜色编码来帮助我们识别过渡绘制。具体可以参考《检查 GPU 渲染速度和绘制过渡》。

当应用界面在同一帧内多次绘制同一个像素时,便会发生过渡绘制。这种可视化会工具可以显示出应用界面不必要的渲染工作;因为,这样的绘制任务往往对用户是不可见的。因此,我们应该尽可能避免过渡绘制的场景。

Android 之如何优化 UI 渲染(上)_第7张图片

过渡绘制颜色等级划分为 5 个阶段:

  1. 真彩色:没有发生过渡绘制;每个像素点仅绘制 1 次;

  2. 蓝色:过渡绘制 1 次,这部分像素在屏幕上绘制了 2 次;

  3. 绿色:过渡绘制 2 次,这部分像素在屏幕上绘制了3 次;

  4. 粉色:过渡绘制 3 次,这部分像素在屏幕上绘制了 4 次;

  5. 红色:过渡绘制 4 次或更多次,这部分像素在屏幕上绘制了 5 次以上。

注意,有些过渡绘制可能是不可避免的。在实际开发过程中,我们应尽可能保证更多的原色或蓝色区域,极少部分的绿色或粉色,最好不要出现红色。

  • 减少布局层级是根绝 Overdraw 最有效的手段
2. Profile GPU Rendering

我们还可以开启 Profile GPU Rendering 检查界面的渲染性能。该工具以滚动柱状图的形式,直观地展示了渲染界面每帧所花费的时间(以每帧 16ms 的速度作为对比基准)。

在 Android 6.0(API Level 23)之后,会输出下面的计算和绘制每个阶段的耗时:

Android 之如何优化 UI 渲染(上)_第8张图片

在 4.0(API Level 14)和 5.0(API Level 21)之间的 Android 版本具有蓝色、紫色、红色和橙色区段。低于 4.0 的 Android 版本只有蓝色、红色和橙色区段。

Android 之如何优化 UI 渲染(上)_第9张图片

如果我们把上面的步骤转化为线程模型,可以得到下面的流水线模型。CPU 将数据同步(sync)给 GPU 之后,一般不会阻塞等待 GPU 渲染完毕,而是通知结束后就返回。而 RenderThread 承担了比较多的绘制工作,分担了主线程很多压力,提高了 UI 线程的响应速度。

Android 之如何优化 UI 渲染(上)_第10张图片

通过上面的一些分析和测量工具,我们可以初步判断应用 UI 渲染的性能是否达标,例如经常出现的掉帧、掉帧主要发生在哪一个阶段、是否存在 Overdraw 等。


数据测量

虽然这些图形化界面工具非常好用,但是它们难以提供准确的测量数据,那有哪些测量方法可以更精确的实现渲染测量呢?

1. Systrace

在 Android 4.1,系统还新增了 Systrace 性能分析工具。Systrace 默认只能监控系统特定调用的耗时情况,例如跟踪系统的 I/O 操作、CPU 负载、Surface 渲染、GC 等事件。而且性能开销非常低。

Systrace 利用了 Linux 的 ftrace 调试工具,相当于在系统各个关键位置都添加了一些性能探针,也就是在代码里加了一些性能监控的埋点。Android 在 ftrace 的基础上封装了 atrace,并增加了更多特有的探针,例如 Graphics、Activity Manager、Dalvik VM、System Server 等。

Systrace 会生成一份包含多个部分的 HTML 文件,包括每个进程渲染界面沿时间轴所指明的渲染帧。具体该如何解读这份报告你可以参考 Systrace 报告。

Android 之如何优化 UI 渲染(上)_第11张图片

拿起笔,划重点了

由于系统预留了 Trace.beginSection 接口来监听应用程序的调用耗时,我们可以在 Systrace 上增加应用程序的耗时分析:通过编译时给每个函数插桩的方式来实现,也就是在重要函数的入口和出口分别增加 Trace.beginSection 和 Trace.endSection。当然出于性能考虑,我们需要过滤大部分指令数比较少的函数,这样就实现了在 Systrace 基础上增加应用程序耗时的监控。具体你可以参考为 Systrace 定义自定义事件。

public class MyAdapter extends RecyclerView.Adapter {
      @Override
      public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          Trace.beginSection("MyAdapter.onCreateViewHolder");
          MyViewHolder myViewHolder;
          try {
              myViewHolder = MyViewHolder.newInstance(parent);
          } finally {
              Trace.endSection();
          }
          return myViewHolder;
      }
}
2. Tracer for OpenGL ES

Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可以逐帧、逐函数的记录 App 用 OpenGL ES 的绘制过程。它提供了每个 OpenGL 函数调用的消耗时间,所以很多时候用来做性能分析。但因为其强大的记录功能,在分析渲染问题时,当 TraceView、Systrace 都显得棘手,还找不到问题所在时,此时这个工具就会派上用场了。

Graphics API Debugger

不过,正如前面文章中介绍 Android 渲染框架的演进非常快,在 Android Studio 3.1 之后,Android 推荐使用 Graphics API Debugger (GAPID)来替代 Tracer for OpenGL ES 工具。GAPID 可以说是它的升级版本,不仅支持跨平台,而且功能更加强大,支持 Vulkan 和回放。

通过上面的几个工具,我们可以进一步判断、分析应用 UI 渲染性能数据,并且能够找出潜在的渲染瓶颈点。


自动化测试场景

虽然上面这些工具已经非常强大,不过线下的测试无论如何也难以复现线上大数据用户场景,此时我们需要一套能够在产品上线后的自动化测量工具,那有哪些测量方法可以满足我们的需求呢?

1. gfxinfo

gfx info 可以输出包含各阶段的动画以及帧相关的性能分析,具体命令如下:

adb shell dumpsys gfxinfo packageName

除了渲染的性能之外,gfxinfo 还可以拿到渲染相关的内存和 View Hierarchy 信息。

View Hierarchy:

com.xxx.android.xxx/com.weex.app.DebugActivity/android.view.ViewRootImpl@f80e215 
14 views, 11.05kB of display lists

com.xxx.android.xxx/com.weex.app.WeexActivity/android.view.ViewRootImpl@d92a
84 views, 96.78kB of display lists

Total ViewRootImol:2
Total Views:       98
Total DisplayList: 107.82 kB

在 Android 6.0 之后,gfxinfo 命令新增了 framestats 参数,可以拿到最近 120 帧每个绘制阶段的耗时信息。

adb shell dumpsys gfxinfo packageName framestats

通过这个命令我们可以实现自动化统计应用的帧率,更进一步还可以实现自定义的 “Profile GPU Rendering” 工具,在出现掉帧的时候,自动统计分析是哪个阶段的耗时增长最快,同时给出相应建议。

2. SurfaceFlinger

除了耗时,我们还比较关心渲染使用的内存。在前面文章我们有讲过,Android 4.1 以后每个 Surface 都会有三个 Graphic Buffer,那如何查看 Graphic Buffer 占用的内存,系统是怎么样管理这部分的内存的呢?

你可以通过下面的命令拿到 SurfaceFlinger 相关的信息:

adb shell dumpsys SurfaceFlinger

以我的测试项目为例,应用使用了三个 Graphic Buffer 缓冲区,当前用在显示的第 0 个 Graphic Buffer,大小是 1080 * 1920。这样我们可以更好地理解三缓冲机制,而且可以看到这三个 Graphic Buffer 的确在交替使用。

Layer 0x7b5dfb7000 (com.xxx.android.bbt/com.xxx.android.xxx.MainActivity)
//  序号          // 状态         // 对象                // 大小
[00:0x7be3a490e0] state=ACQUIRED 0x7be3a2eaa0 frame=471 [1080x1920:1088,  1]
[02:0x7be3a499a0] state=FREE     0x7be3a50780 frame=469 [1080x1920:1088,  1]
[01:0x7be3a492a0] state=FREE     0x7b5e883e60 frame=470 [1080x1920:1088,  1]

看下三个 Buffer 分别占用的内存:

Allocated buffers:
0x7be1bf6360: 8160.00 KiB | 1080 (1088) x 1920 |    1 |        1 | 0x20001a00 
0x7be1bf63c0: 8160.00 KiB | 1080 (1088) x 1920 |    1 |        1 | 0x20001a00
0x7be1bf6420: 8160.00 KiB | 1080 (1088) x 1920 |    1 |        1 | 0x20001a00

这部分的内存其实真的不小,特别现在手机分辨率越来越大,而且应用可能还会存在其他 Surface 的情况,例如 SurfaceView 或者 TextureView。

3. Choreographer

帧率,业界一般都使用 Choreographer 来监控应用的帧率。但是需要排除页面在没有操作的情况,也就是说只在界面存在绘制的时候才做统计。

那么如何监听界面是否存在绘制行为呢?具体你可以参考《Android 之 ViewTreeObserver 全面解析》。

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener

我们经常用平均帧率来衡量界面流畅度,但事实上电影的帧率才 24 帧,用户对于应用的平均帧率是 40 帧还是 50 帧并不一定可以感受出来。对于用户来说,感觉最明显的是连续丢帧情况,Android Vitals 将连续丢帧超过 700 ms 定义为冻帧,即连续丢帧超过 42 帧。

因此,我们可以统计更有价值的冻帧率。冻帧率就是计算发生冻帧时间在所有时间的占比。出现丢帧的时候,我们可以获取当前的页面信息、View 信息和操作路径进行上报,降低二次排查的难度。

另外还可以进一步细化问题,按照 Activity、Fragment 或者某个操作定义场景,通过细化不同场景的平均帧率和冻帧率,进一步细化问题排查的范围。


最后

Android 渲染框架在快速的演进,可能随着版本的升级,有些工具也不再适用;工具只是帮助我们排查问题的一种手段,比工具本省更重要的是了解和学习它们背后的工作原理,这对我们的成长会有很大的帮助。

日常开发中我们不能只满足于完成需求就可以了,在实现的同时还要多去思考例如内存、卡顿、渲染等这些影响性能的点,日积月累我们的进步自然也会更快一些。


相信你也肯定有很多好的渲染分析或其他性能优化的思路和方法,欢迎大家留言分享你的性能优化“必杀技”。文中如有不妥或有更好的分析结果,欢迎您的指正。

文章如果对你有帮助,就请留个赞吧!


扩展阅读

关于 UI 渲染,你需要了解什么?
Android 之你真的了解 View.post() 原理吗?
深入 Activity 三部曲(3)之 View 绘制流程
...

其他系列专题

Android 存储优化系列专题
Android 之不要滥用 SharedPreferences
Android 存储选项之 SQLite 优化那些事儿
Android 对象序列化之追求完美的 Serial
...

你可能感兴趣的:(Android 之如何优化 UI 渲染(上))