转载:在路上的博客:Android 流畅度评估及卡顿优化
导言:本文主要是关于 Android 流畅度和卡顿优化的全方位介绍,算是对 2020 部分工作的总结。
Google 定义:界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与应用互动,应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度(为什么是 60fps?)。
如果应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉应用不流畅,我们将这种情况称为卡顿。
来源于:Google Android 的为什么是 60fps?
16ms 意味着 1000/60hz,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps 是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于 30fps 是 无法顺畅表现绚丽的画面内容的,此时就需要用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内完成屏幕刷新的全部逻辑操作,就会发生卡顿。
首先要了解 Android 显示 1 帧图像,所经历的完整过程。
如图所示,屏幕显示 1 帧图像需要经历 5 个步骤:
判断 APP 是否出现卡顿,我们从通用应用和游戏两个纬度的代表公司标准来看,即 Google 的 Android vitals 性能指标和地球第一游戏大厂腾讯的 PrefDog 性能指标。
参考:使用 Android Vitals 监控应用的技术性能
以 Google Vitals 的卡顿描述为准,即呈现速度缓慢和帧冻结两个维度判断:
来源:腾讯 PerfDog 使用说明书
PerfDog Jank 计算方法:
普通卡顿 Jank(同时满足两条件):
严重卡顿 BigJank(同时满足两条件):
参考:APP&游戏需要关注 Jank 卡顿及卡顿率吗?
帧率 FPS 高并不能反映流畅或不卡顿。比如:FPS 为 50 帧,前 200ms 渲染一帧,后 800ms 渲染 49 帧,虽然帧率 50,但依然觉得非常卡顿。同时帧率 FPS 低,并不代表卡顿,比如无卡顿时均匀 FPS 为 15 帧。所以平均帧率 FPS 与卡顿无任何直接关系)
当了解卡顿的标准以及渲染原理之后,可以得出结论,只有丢帧情况才能准确判断是否卡顿。
参考:Android 开发者 | 测试界面性能
dumpsys 是一种在设备上运行并转储需要关注的系统服务状态信息的 Android 工具。通过向 dumpsys 传递 gfxinfo 命令,可以提供 logcat 格式的输出,其中包含与录制阶段发生的动画帧相关的性能信息。
# 查看帧时间数据
adb shell dumpsys gfxinfo < PACKAGE_NAME > framestats
# 帧数据重置
adb shell dumpsys gfxinfo < PACKAGE_NAME > reset
借助 Android 6.0(API 级别 23),该命令可将在整个进程生命周期中收集的帧数据的聚合分析输出到 logcat。例如:
Stats since: 752958278148ns
Total frames rendered: 82189
Janky frames: 35335 (42.99%)
90th percentile: 34ms
95th percentile: 42ms
99th percentile: 69ms
Number Missed Vsync: 4706
Number High input latency: 142
Number Slow UI thread: 17270
Number Slow bitmap uploads: 1542
Number Slow draw: 23342
这些总体统计信息可以得到期间的 FPS、Jank 比例、各类渲染异常数量统计。
命令adb shell dumpsys gfxinfo
可提供最近 120 个帧中,渲染各阶段带有纳秒时间戳的帧时间信息。
flags | intended_vsync | vsync | oldest_input_event | newest_input_event | handle_input_start | animation_start | perform_traversals_start | draw_start | sync_queued | sync_start | issue_draw_commands_start | swap_buffers | frame_completed |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 27965466202353 | 27965466202353 | 27965449758000 | 27965461202353 | 27965467153286 | 27965471442505 | 27965471925682 | 27965474025318 | 27965474588547 | 27965474860786 | 27965475078599 | 27965479796151 | 27965480589068 |
flags:FLAGS 为 0 时,总帧时间 (ms) = (FRAME_COMPLETED - INTENDED_VSYNC) / 1000000。 如果非零,则该行应该被忽略,因为该帧的预期布局和绘制时间超过 16ms,为异常帧。
INTENDED_VSYNC:帧的的预期起点,监测 UI 线程是否正常。如果与 VSYNC 值不同,是由于 UI 线程中的工作使其无法及时响应垂直同步信号所造成的。
HANDLE_INPUT_START:
ANIMATION_START:
PERFORM_TRAVERSALS_START:布局和度量阶段完成的时间 = PerformTraversalsStart - DrawStart。滚动或动画期间,期望接近 0。
SYNC_QUEUED:
SYNC_START:
ISSUE_DRAW_COMMANDS_START:
FrameCompleted:帧的完整时间。帧耗时 = FrameCompleted - IntendedVsync,要求小于 16ms。
通过 gfxinfo 输出的帧信息,通过定时 reset 和打印帧信息,可以得到 FPS(帧数/打印间隔时间)、丢帧比例((janky_frames / total_frames_rendered)*100 %)、是否有帧冻结 (帧耗时>700ms)。
根据第 2 部分的通用应用卡顿标准,可以通过丢帧比例和帧冻结数量,准确判断当前场景是否卡顿。并且通过定时截图,还可以根据截图定位卡顿的具体场景。
如上图所示,利用 gfxinfo 开发的检查卡顿的小工具,图中参数和卡顿说明如下:
FPS = total_frames_renderes:total_frames_renderes 为每秒的帧数量,即 FPS。(每秒 reset 并统计一次)
卡顿为什么去掉 FPS<2 的数据:人的视觉暂留 100~400ms,即 FPS 在 2.5~10 之间时,所以当 FPS 低于 3 时,人眼看到的并不是连续动作,即使有丢帧现象,也不会察觉。
UI_score:UI_score = 100 - (janky_frames / total_frames_rendered)*100,根据 Google Vitals 呈现速度缓慢的定义,当超过 50% 的帧呈现时间超过 16 毫秒,说明呈现速度缓慢。所以,当 UI_score<=50 时,页面卡顿。
帧冻结:通过每秒的 max_frame_time 判断,当帧冻结的绘制耗时超过 700ms,为严重卡顿问题。
根据上面对 gfxinfo 的帧信息解析,可以准确计算出每一帧的耗时。从而可以开发出满足腾讯 PerfDog 中关于普通卡顿和严重卡顿的判断。
依赖定时截图,即可准确定位卡顿场景。如下图所示(此处以 PerfDog 截图示例):
通过第 3 部分的卡顿评估方法,我们可以定位到卡顿场景,但是如何定位到具体卡顿原因呢。
首先了解卡顿问题定位工具,然后再了解常见的卡顿原因,即可通过复现卡顿场景的同时,用工具去定位具体卡顿问题。
Systrace 或 Perfetto :记录短时间内的设备活动,汇总了 Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。
LayoutInspect :检测动态布局层次结构、调查资源属性值在源代码中的来源位置、在运行时对应用的视图层次结构进行高级 3D 可视化。
CPU 性能剖析器 :监控应用进程中的每个线程,执行的方法 (Java) 或函数 (C/C++),以及每个方法或函数在其执行期间消耗的 CPU 资源。还可以使用方法和函数跟踪数据来识别调用方和被调用方。可以使用这些信息来确定哪些方法或函数过于频繁地调用通常会消耗大量资源的特定任务,并优化应用的代码以避免不必要的工作。
GPU 渲染模式分析工具 :以滚动直方图的形式直观地显示渲染界面窗口帧所花费的时间(以每帧 16 毫秒的速度作为对比基准),可定位动画渲染阶段的具体问题(比如:输入处理耗时问题、界面线程问题、视图绘制问题等)。
重点就是,充分利用 gfxinfo 输出的帧信息,对卡顿问题进行分类。
INTENDED_VSYNC
HANDLE_INPUT_START
ANIMATION_START
PERFORM_TRAVERSALS_START
SYNC_QUEUED
SYNC_START
主要参考:Android 卡顿检测及优化
了解了高效定位卡顿的方法和卡顿问题定位工具,再熟悉一下常见的卡顿原因,可以更熟练的定位和优化卡顿。
SurfaceFlinger 负责 Surface 的合成,一旦 SurfaceFlinger 主线程调用超时,就会产生掉帧。
SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成,也会阻塞应用的 binder 调用,如 dequeueBuffer、queueBuffer 等。
后台进程活动太多,会导致系统非常繁忙,cpu \ io \ memory 等资源都会被占用,这时候很容易出现卡顿问题,这也是系统这边经常会碰到的问题。
dumpsys cpuinfo 可以查看一段时间内 cpu 的使用情况:
当线程为 Runnable 状态的时候,调度器如果迟迟不能对齐进行调度,那么就会产生长时间的 Runnable 线程状态,导致错过 Vsync 而产生流畅性问题。
system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿。
Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多,就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。
主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超时都会导致卡顿 。
Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作。
应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿。
如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅. 比如使用 90 Hz 的屏幕搭配 60 fps 的动画。
由上面的分析可知对象分配、垃圾回收 (GC)、线程调度以及 Binder 调用 是 Android 系统中常见的卡顿原因,因此卡顿优化主要以下几种方法,更多的要结合具体的应用来进行:
在计算机和通信领域,帧是一个包括 “帧同步串行” 的数字数据传输单元或数字数据包。
在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,其中帧是指每一张画面。