Android卡顿优化实战篇

声明:原创文章,转载请注明出处https://www.jianshu.com/p/8a296b75d95f

APP卡顿对用户体验有很大的影响,今天就来总结下Android应用卡顿相关内容。

要想知道应用卡顿是怎么产生的,我们首先得了解下手机屏幕的画面是怎么产生的,也就是Android的屏幕刷新机制。

Android 屏幕刷新机制

一级缓存

如上图所示,当需要显示一帧画面时,CPU会先计算相关数据,然后再通过GPU渲染,GPU渲染完之后会把这帧画面存入一个缓存中,CPU和GPU向缓存中存入画面的速率也叫帧率,帧率是变化的,比如APP中一个列表,当列表静止不动帧率就是0,如果滑动起来帧率就会变化。接着我们的屏幕会以一定的频率来从缓存中取数据,这个速率也叫做屏幕刷新率。这个刷新率是固定的,一般为60hz,当然现在也有90hz甚至120hz的屏幕。另外屏幕从缓存中取数据也不是瞬间取完的,它是有一个扫描的过程,也就是一个像素点一个像素点的去缓存中去取,取的顺序是逐行地从左到右逐个像素点扫描。不过这也带来了一个问题,就是可能会在屏幕取数据取到一半的时候,这时缓存中数据更新了,但是屏幕并不知道,会接着从缓存中取,当然接下来取到的数据其实的更新后的新的一帧,这样屏幕就同时显示了两帧画面,我们叫这种现象为画面撕裂,如下图所示:

画面撕裂(图片源于网络)

二级缓存

为了解决这个问题,可以增加一个缓存,如下:



可以看到上面有两个缓存,Back Buffer和Frame Buffer,每次CPU、GPU产生的帧数据都存入Back Buffer,而屏幕每次都是从Frame Buffer中取,然后需要更新画面的时候,就交换Back Buffer和Frame Buffer两个缓存中的数据。那么这个交换两个缓存的时间如何确定呢,我们知道屏幕显示一帧是逐个像素点显示,是需要时间,而取完一帧的最后一个点到显示下帧第一个,这个时间段正好可以作为切换这两个缓存的时间点,此时会发出 VSync 信号,即垂直同步信号。但是CPU和GPU计算画面的时间点不是确定的,也就是画面的产生和刷新是不同步的,这样就会有一个问题



上图我们可以看到,第一帧在垂直同步信号到来之前已经计算好了,所以当垂直同步信号到来的时候成功显示,但是当第二个垂直同步信号来的时候第二帧的画面还没计算完,所以会再次显示第一帧,也就出现卡顿,我们也叫这种为掉帧,显然掉帧不是少了一帧而是多了一帧。有啥办法可以解决?上图可以看到第二帧的计算是在第一个VSync和第二个VSync中间才开始计算的,那么能不能在第一个VSync来了之后立马开始计算为第二帧的显示做准备,这样就有足够的时间来准备这帧画面。答案是肯定的,从Android 4.1开始Google对这一问题进行了优化,就是每当同步信号过来的时候就开始为下一帧做准备,如下图:

但是有的时候我们一帧的计算非常耗时,时间超出了两个VSync的间隔,如下:


可以看到GPU在渲染B帧的时候太耗时了,再下次VSync到来的时候还没渲染好,这时就会重复显示上帧的内容即画面A,这样就又出现卡顿,直到下个Vsync信号到来才会显示B帧。

三级缓存

上面我们可以看到,当一个VSync到来的时候,GPU还在计算,但是CPU其实已经计算完了,那么为什么这时CPU不参加计算下一帧的画面呢?这样就可以节省很多时间。原因是CPU和GPU是公用的一个缓存,也就是上面提到的Back Buffer,所以为了解决这个问题,Android 又为CPU增加了一个独立的缓存。

上面可以看到第一个垂直同步信号到来的时候,GPU还在计算画面,但是CPU已经空闲了,所以已经开始计算下一帧了,这样就提高了时间利用率。

卡顿产生的原因

了解了Android屏幕的刷新机制,来总结下卡顿产生的原因,从上面我们也能知道,卡顿的本质就是一帧的展示时间大于了两个垂直同步信号的时间间隔。下面简单概括下一帧的画面是为啥变慢的,先从系统层面来看下,有以下几个原因:

  • SrufaceFlinger主线程耗时

  • 后台活动进程太多导致系统繁忙

  • 主线程调度不到,处于Runnable状态

  • System锁

  • Layer过多导致SurfaceFlinger Layer Compute耗时

从应用层角度有以几个原因:

  • 主线程执行时间长

  • 主线程Binder耗时

  • Webview性能不足

  • 帧率与刷新率不匹配

卡顿检测

往往造成卡顿的代码是很隐蔽的,所以对卡顿代码的检测和定位就显得尤为重要。当然对卡顿的检测需要借助一些分析工具,下面简单罗列了几种分析工具:

  • 使用dumpsys gfxinfo

  • 使用Perfetto

  • 使用LayoutInspector检测布局层次

  • 使用BlockCanary

dumpsys gfxinfo

这是一个adb命令,可以大致显示当前应用的一些显示数据,命令如下:

adb shell dumpsys gfxinfo 应用完整包名

部分显示内容如下

Stats since: 2136261710504255ns
Total frames rendered: 1 // 本次搜集了一帧的信息
Janky frames: 1 (100.00%)  // 卡顿的帧数为1,占比100%
50th percentile: 34ms
90th percentile: 34ms
95th percentile: 34ms
99th percentile: 34ms
Number Missed Vsync: 0  // 垂直同步失败的帧
Number High input latency: 0 // 处理input超时的帧
Number Slow UI thread: 0  //因UI线程超时导致卡顿的帧
Number Slow bitmap uploads: 0 // 因bitmap的加载耗时的帧数
Number Slow issue draw commands: 1 // 因绘制导致耗时的帧数
Number Frame deadline missed: 1

perffeto

perffeto是一个网页版的性能分析工具,地址是https://ui.perfetto.dev/#!/,分析时需要上传性能跟踪文件,当然这个文件可以通过Android手机自带的一个系统应用追踪录制生成。打开这个系统应用的方式是开发者模式->调试->系统跟踪,然后打开跟踪可调式的应用开关,这样你手机的上方快捷操作栏就会多出一个录制跟踪记录的快捷开关,点击后就会进行性能追踪录制,再次点击就会停止录制,并且会在通知栏中产生一个完成录制的提示,点击就可以分享录制的跟踪文件。

接下来简单演示下,创建一个默认空项目,然后在MainActivity中的onResume方法中加入3秒的sleep,如下:

override fun onResume() {
    super.onResume()
    Thread.sleep(3000)
}

接下来我们点击快捷栏的录制按钮,然后在手机上运行该demo,大概过个10秒左右点击停止录制,然后生成跟踪文件,我们把这个跟踪上传至上面的perffeto分析地址,然后会得到这样的分析结果:



可以看到上面这些五颜六色的可以理解成一个个事件,横坐标是时间,方块长度越长说明越耗时,接下来我们找到我们刚刚写的demo



可以看到我们这个demo中,有一个明显很长的事件,上面也可以看到事件的名字就是我们刚才写的onResume,可以看上面的时间轴刚好也是3秒,这样我们也就定位出问题所在了。

LayoutInspector

布局如果嵌套太多也会影响画面的渲染产生卡顿,所以可以借助LayoutInspector工具来查看布局的层级结构,这个是Android Studio内置的


LayoutInspector

如上图所示,左侧会显示该布局的层级结构。

BlockCanary

BlockCanary是一个非侵入式的三方库,它是模仿的LeakCanary。当某处有延迟时,就会在通知栏提示。他的原理是基于handler机制,对handler还不是很了解的可以参考之前的这篇文章Android 带你彻底理解跨线程通信之Handler机制,我们知道Android主线程中所有的事件都是通过handler处理的,而系统在handler每次处理消息的前后都预留了日志输出的接口,那么我们可以在预留的接口中增加时间记录,然后计算事件处理前后的时间差,如果大于某一阈值就产生告警提示开发者,这也就是BlockCanary的思路。

卡顿优化

在上面我们已经了解了卡顿产生的原因就是一帧的渲染大于规定的时间,那么优化的主要思路就是减少耗时,当然上面我们也说了一些耗时的代码往往是很隐蔽的,这就需要我们使用上面提到的卡顿检测和定位的工具,这也是卡顿优化的关键。此外我们还是简单罗列下优化的一些方向:

  • 布局优化:布局的过多嵌套会影响渲染效率

  • 减少主线程耗时:这个不多说了

  • 减少过渡绘制:界面的过渡绘制也会影响耗时

  • 列表优化 :比如某一个item更新,只需要更新这个item不需要整个列表全部更新

  • 对象分配和回收优化:因为Java垃圾回收会暂停线程,所以要避免频繁的创建对象销毁对象,减小垃圾回收频次。

你可能感兴趣的:(Android卡顿优化实战篇)