Android 渲染性能优化——你需要知道的一切!

本文将分以下几个部分来分析和总结渲染性能:帧率和渲染性能问题、渲染性能问题产生的原因、渲染性能优化方法、分析工具。

帧率和渲染性能问题


前文《Android 渲染机制——原理篇(显示原理全过程解析)》。中,介绍了 Android 的渲染机制以及卡顿等性能问题产生的原因,这里简单回顾一下帧率和卡顿是什么?

Android 在设计的时候,把帧频限定在了每秒 60 帧,当我们的 APP 的帧频 60fps 时,画面就会非常的流畅。但是通常由于各种各样的原因,帧频很可能会小于 60fps,这样就会出现丢帧现象,用户端表现为可感知的卡顿等现象。那面我们的帧频可以高于 60fps 吗,答案是否定的,这是因为界面刷新渲染依赖底层的 VSYNC 信号,VSYNC 信号以每秒 60 次的频率发送给上层,并且高于 60fps 的帧频也是没有必要的,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。(大多数手机的屏幕刷新频率是 60hz,所以太高也没有意义)

开发 app 的性能目标就是保持 60fps,这意味着每一帧你只有 1000/60=16.67ms 的时间来处理所有的任务。如果 16ms 内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。

卡顿

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能,也就是 16ms 内没有办法把这一帧的任务执行完毕,发生了丢帧的现象。从设计师和产品的角度,他们希望 App 能够有更多的动画、图片等时尚元素来实现流畅的用户体验。但是 Android 系统很有可能无法及时完成那些复杂的界面渲染操作。Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的 60fps,为了能够实现 60fps,这意味着程序的大多数操作都必须在 16ms 内完成。

在这里插入图片描述

如果 App 的某个操作花费时间是 24ms,系统在得到 VSYNC 信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在 32ms 内看到的会是同一帧画面。

在这里插入图片描述

冻结的帧

卡顿我们已经了解了,那么什么是冻结的帧呢?

冻结的帧是渲染时间超过 700ms 的界面帧。这是一个问题,因为你的应用在帧的呈现过程中几乎有一秒钟的时间卡住,对用户输入无响应。我们通常建议应用在 16ms 内呈现帧,以确保界面流畅。但是,当你的应用启动或转换到其他屏幕时,初始帧的绘制时间通常会超过 16ms,这是因为你的应用必须扩充视图,对屏幕进行布局并从头开始执行初始绘制。因此,Android 将冻结的帧与呈现速度缓慢分开跟踪。你应用中的任何帧的呈现时间都不应超过 700ms。

冻结的帧是渲染速度缓慢的一种极端形式,因此诊断和解决问题的过程是相同的。

ANR

相对于卡顿和冻结的帧,更极端的情况就是 ANR 了。

如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,ANR 对话框会为用户提供强行退出应用的选项。

显示 ANR 发生时,通常的原因是,UI 线程被其他任务占用(非渲染任务,例如,I/O 操作、锁等待等),长时间对用户输入等操作没有相应,ANR 问题是非常严重的性能问题,我们应该极力避免。

由于 ANR 产生通常不是单纯由渲染性能问题导致的,所以这里不进行详细讲解,在今后单独写一章进行分析。

渲染性能问题产生的原因


我们来想想渲染性能问题产生的原因是什么?

有很多原因可以导致丢帧,产生卡顿等问题。也许是因为你的 layhout 太复杂,无法在 16ms 内渲染,也有可能是因为你的 UI 上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致 CPU 或 GPU 负载过重。

这里,我总结有以下几个主要原因:

  1. 过度绘制
  2. 重复布局
  3. 重复渲染
  4. 布局层级过深
  5. CPU资源占用过多、频繁GC

过度绘制

过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的 CPU 以及 GPU 资源。

在这里插入图片描述

关于过度绘制更详细的分析–>《Android渲染性能——过度绘制》

重复布局

在视图树中,任何一个 View 一旦发生一些属性变化,都可能引起很大的连锁反应。例如某个 button 的大小突然增加一倍,有可能会导致兄弟视图的位置变化,也有可能导致父视图的大小发生改变。当大量的 layout() 操作被频繁调用执行的时候,就很可能引起丢帧的现象。

在这里插入图片描述

例如,在 RelativeLayout 中,我们通常会定义一些类似 alignTop,alignBelow 等等属性,如图所示:

在这里插入图片描述

为了获得视图的准确位置,需要经过下面几个阶段。首先子视图会触发计算自身位置的操作,然后 RelativeLayout 使用前面计算出来的位置信息做边界的调整的操作,如下面两张图所示:

在这里插入图片描述

经历过上面2个步骤,RelativeLayout 会立即触发第二次 layout() 的操作来确定所有子视图的最终位置与大小信息。

除了 RelativeLayout 会发生两次 layout 操作之外,LinearLayout 也有可能触发两次 layout 操作,通常情况下 LinearLayout 只会发生一次 layout 操作,可是一旦调用了 measureWithLargetChild() 方法就会导致触发两次 layout 的操作。另外,通常来说,GridLayout 会自动预处理子视图的关系来避免两次 layout,可是如果 GridLayout 里面的某些子视图使用了 weight 等复杂的属性,还是会导致重复的 layout 操作。

如果只是少量的重复 layout 本身并不会引起严重的性能问题,但是如果它们发生在布局的根节点,或者是 ListView 里面的某个 ListItem,这样就会引起比较严重的性能问题。如下图所示:

在这里插入图片描述

重复渲染

通常来说,对于不透明的 View,显示它只需要渲染一次即可,可是如果这个 View 设置了 alpha 值来实现半透明效果,这会导致视图至少需要渲染两次。原因是包含 alpha 的 View 需要事先知道混合 View 的下一层元素是什么,然后再结合上层的 View 进行 Blend 混色处理。

在某些情况下,一个包含 alpha 的 View 有可能会触发该 View 在 HierarchyView 上的父 View 都被额外重绘一次。下面我们看一个例子,下图演示的 ListView 中的图片与二级标题都有设置透明度。

在这里插入图片描述

大多数情况下,屏幕上的元素都是由后向前进行渲染的。在上面的图示中,会先渲染背景图(蓝,绿,红),然后渲染人物头像图。如果后渲染的元素有设置 alpha 值,那么这个元素就会和屏幕上已经渲染好的元素做 blend 处理。很多时候,我们会给整个 View 设置 alpha 的来达到 fading 的动画效果,如果我们图示中的 ListView 做 alpha 逐渐减小的处理,我们可以看到 ListView 上的 TextView 等等组件会逐渐融合到背景色上。但是在这个过程中,我们无法观察到它其实已经触发了额外的绘制任务,我们的目标是让整个 View 逐渐透明,可是期间 ListView 在不停的做 Blending 的操作,这样会导致不少性能问题。

那么,如何解决这类问题呢?我们稍后在性能优化部分,进行介绍。

布局层级过深

通常我们无法避免重复 layout,在这种情况下,我们应该尽量保持 View Hierarchy 的层级比较浅,这样即使发生重复 layout,也不会因为布局的层级比较深而增大了重复 layout 的倍数。另外还有一点需要特别注意,在任何时候都请避免调用 requestLayout() 的方法,因为一旦调用了 requestLayout,会导致该 layout 的所有父节点都发生重新 layout 的操作。

在这里插入图片描述

CPU资源占用过多、频繁GC

如果系统的 CPU 资源占用非常紧张,就会造成 UI 线程分配的 CPU 时间变少,从而影响 UI 线程的执行效率,造成卡顿等问题。

如果我们在 APP 中使用了大量的线程,并且线程优先级为默认,这时非常容易造成大量的子线程和 UI 一同抢占 CPU 资源,造成卡顿等问题。

另外一点,如果我们在界面显示过程中,频繁、并且大量的创建临时对象,就会造成系统频繁的 GC,进行垃圾回收操作。

垃圾回收操作会造成卡顿吗?答案是肯定的,这是因为,创建和销毁对象都需要执行时间和资源,并且这类问题中,创建对象通常是在 UI 线程中,这就造成每一帧执行时都会有大量对象创建的时间消耗。并且一旦开启了 GC,GC 线程也会占用一部分 CPU 资源,造成和 UI 线程抢占资源的问题,同时,GC 在执行过程中,通常会造成一定时间的暂停操作(不同 Android 版本有所区别,早起版本会暂停2次,版本越高,系统性能越好),会导致 UI 线程短暂停顿,产生卡顿。

渲染性能优化


优化过度绘制

过度绘制根本的原因是,在一个像素位置上,层叠了多个像素,等待绘制,我们可以用以下几种方法来进行优化:

  • 移除不必要的背景。
  • 通过 clipRect 和 clipPath 方法来减少绘制的层数。
  • 使用主题时,别忘了移除主题背景。

引起性能问题的一个很重要的方面是因为过多复杂的绘制操作。我们可以通过工具来检测并修复标准 UI 组件的 Overdraw 问题,但是针对高度自定义的 UI 组件则显得有些力不从心。

对非可见的 UI 组件进行绘制更新会导致 Overdraw。例如 Nav Drawer 从前置可见的 Activity 滑出之后,如果还继续绘制那些在 Nav Drawer 里面不可见的 UI 组件,这就导致了 Overdraw。为了解决这个问题,Android 系统会通过避免绘制那些完全不可见的组件来尽量减少 Overdraw。那些 Nav Drawer 里面不可见的 View 就不会被执行浪费资源。

在这里插入图片描述

然而,对于那些复杂的自定义 View(重写了onDraw方法),Android 系统就无法监控并自动优化,也就无法避免过度绘制了。

但是,我们可以在自定义组件,重写 onDraw 方法时,使用 canvas.clipRect() 方法来帮助系统识别可见区域,该方法可以指定一块矩形区域,只有在矩形区域内才会被绘制,其他区域会被忽视。这个 API 可以很好的帮助那些有多组重叠组件的自定义 View 来控制显示的区域。同时 clipRect 方法还可以帮助节约 CPU 和 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行,但那些部分内容在矩形区域内的组件,仍然会得到绘制。

在这里插入图片描述

除了 clipRect 方法之外,我们还可以使用 canvas.quickreject() 来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

减少重复测量、布局

在布局层次一样的情况下,使用LinearLayout替代RelativeLayout。因为默认情况下LinearLayout只会测量一次,而RelativeLayout会测量2次;但是,当LinearLayout的view有weight属性的时候,它也会测量2次,这个时候,就建议使用RelativeLayout了。

层级深度一样的情况下,优先级如下:

  1. FrameLayout
  2. LinearLayout(不设置weight属性)
  3. RelativeLayout

在LinearLayout中不使用weight属性,将只进行一次measure的过程
如果使用了weight属性,LinearLayout在第一次测量时避开设置过weight属性的子View,之后再对它们做第二次measure。由此可见,weight属性对性能是有影响的。
布局中设置了weight的话,那么LinearLayout的话会测量两次,这样明显影响了性能,所以我们应该能不适用weight的时候就少用。

RelativeLayout会进行两次的测量

(1)RelativeLayout慢于LinearLayout是因为它会让子View调用2次measure过程,而后者只需一次,但是有weight属性存在时,后者同样会进行两次measure。

(2)RelativeLayout的子View如果高度和RelativeLayout不同,会引发效率问题,可以使用padding代替margin以优化此问题。

(3)在不响应层级深度的情况下,使用Linearlayout而不是RelativeLayout。

重复渲染优化

在讲解渲染性能问题产生原因时,我们已经知道,一个不透明的 View,显示它只需要渲染一次即可,而 View 如果设置了alpha 值,会至少需要渲染两次(原因是包含alpha的view需要事先知道混合View的下一层元素是什么,然后再结合上层的View进行Blend混色处理。),甚至在某些情况下,一个包含 alpha 的 View 有可能会触发该 View 视图树中的父 View 都被额外重绘一次。

那么,我们如何来优化这类问题呢?

嗯嗯,第一个方法非常简单,尽量少用半透明效果,减少 View 的 alpha 的使用。

但是,我们有时候确实是需要半透明效果的,又如何来避免重复渲染问题呢?

接着上文的例子(ListView 做 alpha 逐渐减小的处理)我们来分析解决方法:

我们可以先按照通常的方式把 View 上的元素按照从后到前的方式绘制出来,但是不直接显示到屏幕上,而是使用 GPU 预处理之后,再又 GPU 渲染到屏幕上,GPU 可以对界面上的原始数据直接做旋转,设置透明度等等操作。使用 GPU 进行渲染,虽然第一次操作相比起直接绘制到屏幕上更加耗时,可是一旦原始纹理数据生成之后,接下去的操作就比较省时省力。

在这里插入图片描述

如何才能够让 GPU 来渲染某个 View 呢?我们可以通过 setLayerType 的方法来指定 View 应该如何进行渲染,从 SDK 16 开始,我们还可以使用 ViewPropertyAnimator.alpha().withLayer() 来指定。如下图所示:

在这里插入图片描述

另外一个例子是包含阴影区域的 View,这种类型的 View 并不会出现我们前面提到的问题,因为他们并不存在层叠的关系。

在这里插入图片描述

为了能够让渲染器知道这种情况,避免为这种 View 占用额外的 GPU 内存空间,我们可以做下面的设置:

在这里插入图片描述

通过上面的设置以后,性能可以得到显著的提升,如下图所示:

在这里插入图片描述

布局层级过深优化

我们应该尽量保持 View Hierarchy 的层级比较浅,这样即使发生重复 layout,也不会因为布局的层级比较深而增大了重复 layout 的倍数。

避免调用 requestLayout()

在任何时候都请避免调用 requestLayout() 的方法,因为一旦调用了 requestLayout,会导致该 layout 的所有父节点都发生重新 layout 的操作。布局越复杂、布局层级越深,性能损耗越大。

requestLayou 的原理请查看《View 的 requestLayout 发起的重绘流程源码分析(Android Q)》

使用 merge 标签

使用 merge 可以减少不必要的层级。

使用场景:

  1. merge 最常见的是和 include 标签一块使用。这样在复用布局的时候,也就不会增加多余的布局嵌套了,解决了只有 include 标签带来的问题。
  2. 另外,在自定义组合 View 的时候(例如,继承自 FrameLayout、LinearLayout 等),我们通常会创建一个自定义一个布局,并且通过索引 id 添加到自定义 View 中,这时如果不用 merge 标签,无形中会增加了一层的嵌套。

例如,我们自定义了一个容器 View,类名为 TestLayout,继承自 LinearLayout,该自定义控件的填充布局就可以使用 merge 标签。

<?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">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</merge>

TestLayout 继承自 LinearLayout,所以它本身就是一个 LinearLayout,如果我们将布局中的 merge 修改为 LinearLayout,就会出现2个 LinearLayout 嵌套,其中一个就是多余的。

使用 ViewStub 标签

该标签的作用是用于懒加载布局,当系统碰到 ViewStub 标签的时候是不进行任何处理(measure、layout等),比设置 View 隐藏、不可见更高效。当我们真正需要显示某一个布局的时候才去渲染。

当 ViewStub 变得可见或 inflate() 的时候,布局才会被加载(替换 ViewStub)。

ViewStub 标签懒加载有以下几个好处:

  1. 可以避免 ViewStub 所对应的视图 measure、layout 等性能消耗,只要在使用时才会执行,例如在 APP 启动时,使用它可以提升启动速度。
  2. 加载的视图是层叠的,设置不当,会造成过度绘制等问题。而使用 ViewStub 懒加载,则不会执行渲染。
  3. 避免了视图树中,调用 requestLayout 时的性能消耗。
  4. 避免了 View 创建、初始化等资源消耗,因为 ViewStub 是一个轻量级的组件,占用很少的资源。

如果我们不使用 ViewStub 而是设置 View 为 invisible 时,View 在 layout 布局文件中会占用位置,但是 View的状态是不可见的,该 View 还是会创建对象,会被初始化,会占用资源。如果,设置 View 为 gone,View 在 layout 布局文件中不占用位置,但是还是会创建对象,会被初始化,会占用资源。

使用自定义 View 来优化布局

有些复杂的 UI 设计,我们如果使用系统原生的视图组件,有时需要使用几层的嵌套布局来实现,加之我们的布局如果非常复杂,就容易导致渲染性能问题。

这时,更好的办法可能是设计一个自定义的 View 来实现相关功能。定制的 View 更适合我们的业务,如果存在复杂的交互逻辑也会更容易控制。

但是,自定义 View 如果编码不当,也容易造成性能问题。

通常来说,针对自定义 View,我们可能犯下面三个错误:

  • 调用 View.invalidate() 会触发 View 的重绘,有两个原则需要遵守,第1个是仅仅在 View 的内容发生改变的时候才去触发 invalidate 方法,第2个是尽量使用 ClipRect 等方法来提高绘制的性能。
  • 减少绘制时不必要的绘制元素,对于那些不可见的元素,我们需要尽量避免重绘。
  • 对于不在屏幕上的元素,可以使用 Canvas.quickReject 把他们给剔除,避免浪费 CPU 资源。另外尽量使用 GPU 来进行 UI 的渲染,这样能够极大的提高程序的整体表现性能。

优化线程、减少 GC

线程优化的建议

线程优化主要集中在2点:线程数量的控制和线程优先级的控制。

更多内容请参考《Android性能优化之——线程性能》

GC 优化的建议:

  1. 减少对象分配,避免短时间内大量的对象创建。
  2. 对象的复用,对于频繁分配的对象需要使用复用池。
  3. 尽早释放无用对象的引用,特别是大对象和集合对象,通过置为,及时回收。
  4. 控制 finalize 方法的使用,在高频率函数中使用重写了 finalize 的类,会加重 GC 负担。

下面以常见的 onDraw 造成的性能问题举例说明:

onDraw() 方法是执行在 UI 线程的,在 UI 线程尽量避免做任何可能影响到性能的操作。虽然分配内存的操作并不需要花费太多系统资源,但是这并不意味着是免费无代价的。设备有一定的刷新频率,导致 View 的 onDraw 方法会被频繁的调用,如果 onDraw 方法效率低下,在频繁刷新累积的效应下,效率低的问题会被扩大,然后会对性能有严重的影响。

在这里插入图片描述

如果在 onDraw 里面执行内存分配的操作,会容易导致内存抖动,GC 频繁被触发。

优化方法也很简单,通常情况下,我们会把 onDraw() 里面 new Paint 的操作移动到外面,如下面所示:

在这里插入图片描述

渲染性能分析工具


我们既然了解了渲染相关的性能问题、也了解了应该如何解决,但还有一个重点,就是如何发现渲染性能是否出现问题。

我在这里推荐一些非常实用的工具。

CPU耗时分析:CPU profiler

《Android Studio CPU profiler性能分析工具介绍和使用详解》

布局层级分析:Layout Inspector

《Android布局分析工具Layout Inspector(解决布局产生的性能问题)》

GPU 呈现模式

《如何使用“GPU 呈现模式”进行卡顿问题定位》

耗时分析:Systrace

《性能分析工具Systrace的使用详解》

耗时分析:Perfetto

《Android 性能分析工具——Perfetto 介绍》

总结


本文将分以下几个部分来分析和总结渲染性能:帧率和渲染性能问题、渲染性能问题产生的原因、渲染性能优化方法、分析工具。

开发 app 的性能目标就是保持 60fps,这意味着每一帧你只有 1000/60=16.67ms 的时间来处理所有的任务。如果 16ms 内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。

渲染性能产生的原因大致分为:

  1. 过度绘制
  2. 重复布局
  3. 重复渲染
  4. 布局层级过深
  5. CPU资源占用过多、频繁GC

针对不同的原因,文中给出了相应的优化建议,最后推荐了好用的性能检测工具。

希望本文能对大家有所帮助,可以帮助你提升自己 APP 的渲染性能。


**PS:更多精彩内容,请查看 --> 《Android 性能优化》
**PS:更多精彩内容,请查看 --> 《Android 性能优化》
**PS:更多精彩内容,请查看 --> 《Android 性能优化》

猜你喜欢

转载自blog.csdn.net/u011578734/article/details/110979830