移动性能测试 Android 流畅度评估及卡顿定位、优化

在路上 · 2021年03月19日 · 最后由 在路上 回复于 2021年06月30日 · 22295 次阅读

原文见:在路上的博客:Android 流畅度评估及卡顿优化

导言:本文主要是关于 Android 流畅度和卡顿优化的全方位介绍,算是对 2020 部分工作的总结。
全文主要包括:

  • 1、渲染原理和流畅概念
  • 2、卡顿的标准
  • 3、卡顿评估和判断
  • 4、卡顿定位工具和高效定位方法
  • 5、卡顿优化建议

1、渲染和流畅概念

Google 定义:界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与应用互动,应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度(为什么是 60fps?)。
如果应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉应用不流畅,我们将这种情况称为卡顿。

(1)为什么是 60fps 或 16ms?

来源于:Google Android 的为什么是 60fps?

16ms 意味着 1000/60hz,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps 是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于 30fps 是 无法顺畅表现绚丽的画面内容的,此时就需要用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内完成屏幕刷新的全部逻辑操作,就会发生卡顿。

(2)关于渲染原理

首先要了解 Android 显示 1 帧图像,所经历的完整过程。

如图所示,屏幕显示 1 帧图像需要经历 5 个步骤:

  • 定义布局中的组件
  • 将 ImageView 组件映射成 UI 对象,加载到内存中
  • CPU 将 UI 对象经过运算处理成多维矢量图形
  • GPU 栅格化处理
  • 显示器显示图像

常见的丢帧情况: 渲染期间可能出现的情况,渲染大于 16ms 和小于 16ms 的情况:

上图中应该绘制 4 帧数据 , 但是实际上只绘制了 3 帧 , 实际帧率少了一帧

2、卡顿的标准

判断 APP 是否出现卡顿,我们从通用应用和游戏两个纬度的代表公司标准来看,即 Google 的 Android vitals 性能指标和地球第一游戏大厂腾讯的 PrefDog 性能指标。

(1)通用应用界面卡顿标准

参考:使用 Android Vitals 监控应用的技术性能

以 Google Vitals 的卡顿描述为准,即呈现速度缓慢和帧冻结两个维度判断:

  • 呈现速度缓慢:在呈现速度缓慢的帧数较多的页面,当超过 50% 的帧呈现时间超过 16ms 毫秒时,用户感官明显卡顿。
  • 帧冻结:帧冻结的绘制耗时超过 700ms,为严重卡顿问题。
  • 卡顿忽略 FPS<=2 的页面:因为人的视觉暂留 100~400ms,即 FPS 在 2.5~10 之间时,所以当 FPS 低于 3 时,人眼看到的并不是连续动作,即使有丢帧现象,也不会察觉。

(2)游戏应用界面卡顿标准

来源:腾讯 PerfDog 使用说明书

PerfDog Jank 计算方法:

  • 普通卡顿 Jank(同时满足两条件):
    • 当前帧耗时>前三帧平均耗时 2 倍。
    • 当前帧耗时>两帧电影帧耗时 (1000ms/24*2=84ms)。
  • 严重卡顿 BigJank(同时满足两条件):
    • 当前帧耗时>前三帧平均耗时 2 倍。
    • 当前帧耗时>三帧电影帧耗时 (1000ms/24*3=125ms)。

(3)为什么 FPS 无法判断是否卡顿?

参考:APP&游戏需要关注 Jank 卡顿及卡顿率吗?

帧率 FPS 高并不能反映流畅或不卡顿。比如:FPS 为 50 帧,前 200ms 渲染一帧,后 800ms 渲染 49 帧,虽然帧率 50,但依然觉得非常卡顿。同时帧率 FPS 低,并不代表卡顿,比如无卡顿时均匀 FPS 为 15 帧。所以平均帧率 FPS 与卡顿无任何直接关系)

3、卡顿评估

当了解卡顿的标准以及渲染原理之后,可以得出结论,只有丢帧情况才能准确判断是否卡顿。

(1)如何获取丢帧信息?

参考: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 <PACKAGE_NAME> framestats可提供最近 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 之间的时间差,可以测量应用处理输入事件所花的时间。
    • 如果这个数字较高(> 2 毫秒),则表明应用处理 View.onTouchEvent() 等输入事件所花的时间太长,这意味着此工作需要进行优化或转交给其他线程。请注意,有些情况下(例如,启动新 Activity 或类似活动的点击事件),这个数字较大是预料之中并且可以接受的。
  • ANIMATION_START
    • 在 Choreographer 中注册的动画运行的时间戳。
    • 通过观察此时间戳与 PERFORM_TRANVERSALS_START 之间的时间差,可以确定评估正在运行的所有动画(常见动画有 ObjectAnimator、ViewPropertyAnimator 和 Transitions)所用的时间。
    • 如果这个数字较高(> 2 毫秒),请检查您的应用是否编写了任何自定义动画,或检查 ObjectAnimator 在对哪些字段设置动画并确保它们适用于动画。
  • PERFORM_TRAVERSALS_START:布局和度量阶段完成的时间 = PerformTraversalsStart - DrawStart。滚动或动画期间,期望接近 0。
  • SYNC_QUEUED
    • 将同步请求发送给 RenderThread 的时间。
    • 它标记的是将开始同步阶段的消息发送给 RenderThread 的时间点。如果该时间点与 SYNC_START 的时间差较大(约 > 0.1 毫秒),则意味着 RenderThread 正忙于处理另一帧。它在内部用于区分该帧是因作业负荷过大而超过了 16 毫秒的预算时间,还是该帧由于上一帧超过 16 毫秒的预算时间而停止。
  • SYNC_START
    • 绘制同步阶段的开始时间。
    • 如果此时间与 ISSUE_DRAW_COMMANDS_START 之间相差较大(约 > 0.4 毫秒),通常表示绘制了大量必须上传到 GPU 的新位图。
  • ISSUE_DRAW_COMMANDS_START
    • 硬件渲染器开始向 GPU 发出绘图命令的时间。
    • 此时间与 FRAME_COMPLETED 之间的时间差让您可以大致了解应用生成的 GPU 工作量。绘制过度或渲染效果不佳等问题都会在此显示出来。
  • FrameCompleted:帧的完整时间。帧耗时 = FrameCompleted - IntendedVsync,要求小于 16ms。

(2)如何判断是否卡顿?

通用应用卡顿评估

通过 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 截图示例):

4、卡顿定位工具和高效定位方法

通过第 3 部分的卡顿评估方法,我们可以定位到卡顿场景,但是如何定位到具体卡顿原因呢。

首先了解卡顿问题定位工具,然后再了解常见的卡顿原因,即可通过复现卡顿场景的同时,用工具去定位具体卡顿问题。

(1)卡顿问题定位工具

  • dumpsys gfxinfo :记录动画帧相关性能信息

  • Systrace 或 Perfetto :记录短时间内的设备活动,汇总了 Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。

    • Perfetto 是 Android 10 中引入的全新平台级跟踪工具
  • LayoutInspect :检测动态布局层次结构、调查资源属性值在源代码中的来源位置、在运行时对应用的视图层次结构进行高级 3D 可视化。

  • BlockCanary :检测主线程上的各种卡顿问题

  • CPU 性能剖析器 :监控应用进程中的每个线程,执行的方法 (Java) 或函数 (C/C++),以及每个方法或函数在其执行期间消耗的 CPU 资源。还可以使用方法和函数跟踪数据来识别调用方和被调用方。可以使用这些信息来确定哪些方法或函数过于频繁地调用通常会消耗大量资源的特定任务,并优化应用的代码以避免不必要的工作。

  • GPU 渲染模式分析工具 :以滚动直方图的形式直观地显示渲染界面窗口帧所花费的时间(以每帧 16 毫秒的速度作为对比基准),可定位动画渲染阶段的具体问题(比如:输入处理耗时问题、界面线程问题、视图绘制问题等)。

(2)如何高效定位卡顿问题

重点就是,充分利用 gfxinfo 输出的帧信息,对卡顿问题进行分类。

  • INTENDED_VSYNC
    • 线程问题:如果此值不同于 VSYNC,则表示界面线程中发生的工作使其无法及时响应 Vsync 信号。
    • 推荐定位工具:CPU 性能剖析器查看线程中耗时较多的方法或函数。
  • HANDLE_INPUT_START
    • 输出时间处理时间长:该值与 ANIMATION_START 差值>2ms,则表明应用处理 View.onTouchEvent() 等输入事件所花的时间太长,这意味着此工作需要进行优化或转交给其他线程。
    • 注意事项:有些情况下(例如,启动新 Activity 或类似活动的点击事件),这个数字较大是预料之中并且可以接受的。
    • 推荐定位工具:CPU 性能剖析器查看线程中 View.onTouchEvent(),并优化代码或转交给其他线程处理。
  • ANIMATION_START
    • 动画问题:该值与 PERFORM_TRANVERSALS_START 差值>2ms,自定义动画问题 或 不适合的字段设置动画问题。
    • 推荐定位工具:GPU 渲染模式分析工具,可定位输入处理耗时问题、界面线程问题、视图绘制问题等具体的问题范畴。
  • PERFORM_TRAVERSALS_START
    • 布局问题:该值与 DRAW_START 如果>0,表明完成布局和测量阶段耗时较多。
    • 推荐定位工具:使用 GPU 渲染分析工具确认是否 Draw 或 Measure/Layout 耗时较多,Draw 较多说明更新的视图太多或 View 的 OnDraw 方法做了耗时操作; Measure/Layout 耗时较多,说明布局可能有严重性能问题,使用 LayoutInspect 检查布局是否过于复杂,减少嵌套层次和控件个数。
  • SYNC_QUEUED
    • 帧作业负荷较大问题:该值与 SYNC_START 的时间差较大(约 > 0.1 毫秒),则意味着 RenderThread 正忙于处理另一帧。它在内部用于区分该帧是因作业负荷过大而超过了 16 毫秒的预算时间,还是该帧由于上一帧超过 16 毫秒的预算时间而停止。
    • 推荐定位工具:如果是因为当前帧作业负荷较大导致耗时较多,观察其他参数具体定位问题。
  • SYNC_START
    • 需要上传到 GPU 的新位图较多:如果此时间与 ISSUE_DRAW_COMMANDS_START 之间相差较大(约 > 0.4 毫秒),通常表示绘制了大量必须上传到 GPU 的新位图。
    • 推荐定位工具:GPU 渲染分析工具,具体定位渲染阶段问题。

(3)主要卡顿原因

主要参考:Android 卡顿检测及优化

了解了高效定位卡顿的方法和卡顿问题定位工具,再熟悉一下常见的卡顿原因,可以更熟练的定位和优化卡顿。

A. 系统层面卡顿原因

SurfaceFlinger 主线程耗时

SurfaceFlinger 负责 Surface 的合成,一旦 SurfaceFlinger 主线程调用超时,就会产生掉帧。
SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成,也会阻塞应用的 binder 调用,如 dequeueBuffer、queueBuffer 等。

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

后台进程活动太多,会导致系统非常繁忙,cpu \ io \ memory 等资源都会被占用,这时候很容易出现卡顿问题,这也是系统这边经常会碰到的问题。
dumpsys cpuinfo 可以查看一段时间内 cpu 的使用情况:

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

当线程为 Runnable 状态的时候,调度器如果迟迟不能对齐进行调度,那么就会产生长时间的 Runnable 线程状态,导致错过 Vsync 而产生流畅性问题。

System 锁

system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿。

Layer 过多导致 SurfaceFlinger Layer Compute 耗时

Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多,就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。

B. 应用层面卡顿原因

主线程执行时间长

主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超时都会导致卡顿 。

  • Measure \ Layout 耗时\超时
  • draw 耗时
  • Animation 回调耗时
  • View 初始化耗时
  • List Item 初始化耗时
  • 主线程操作数据库

主线程 Binder 耗时

Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作。

WebView 性能不足

应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿。

帧率与刷新率不匹配

如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅. 比如使用 90 Hz 的屏幕搭配 60 fps 的动画。

5、卡顿优化建议

由上面的分析可知对象分配、垃圾回收 (GC)、线程调度以及 Binder 调用 是 Android 系统中常见的卡顿原因,因此卡顿优化主要以下几种方法,更多的要结合具体的应用来进行:

(1)布局优化

  • 通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。
  • 用 ViewStub 替代在启动过程中不需要显示的 UI 控件。
  • 使用自定义 View 替代复杂的 View 叠加。
  • 减少嵌套层次和控件个数。
  • 使用 Tags:Merge 标签减少布局嵌套层次,ViewStub 标签推迟创建对象、延迟初始化、节省内存等。
  • 减少过度绘制

(2)减少主线程耗时操作

  • 主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。
  • sharepreference 尽量使用 apply,少使用 commit,可以使用 MMKV 框架来代替 sharepreference。
  • 网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。
  • 不要在 activity 的 onResume 和 onCreate 中进行耗时操作,比如大量的计算等。

(3)列表优化

  • RecyclerView 使用优化,使用 DiffUtil 和 notifyItemDataSetChanged 进行局部更新等。

(4)内存优化

  • 减少小对象的频繁分配和回收操作。
  • 被频繁调用的紧密的循环里,避免对象分配来降低 GC 的压力。

6、名词解释

(1)帧

在计算机和通信领域,帧是一个包括 “帧同步串行” 的数字数据传输单元或数字数据包。
在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,其中帧是指每一张画面。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 15 条回复 时间 点赞

感谢楼主,好帖子

客气啦老铁

厉害啊!

太强了,学习了

膜拜 学习了 希望以后可以用到

都是大佬自己写的吗,膜拜

chao 回复

东拼西凑的知识,重在实践

仅楼主可见

有理论,有实践,有分析,有建议。 深度好文,来扩充下视野

客气啦,我比较菜

乱舞 回复

珣哥过奖了

真的太强了,看完收获很大满满的干货!!

请问各位大佬,为什么我执行 adb shell dumpsys gfxinfo framestats ,显示的 PROFILEDATA 只有 10 行,不是 120 帧的吗?

wjxiao 回复

试了下,看起来是跟机型和版本有关系:
荣耀 10 android10.0 只有 10 帧
小米 note3 android8.1.0 能有 120 帧

wjxiao 回复

可以看一下是否跟安卓版本有关,比如拿一台小米 android10 的机器试试

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册