想让安卓app不再卡顿?看这篇文章就够了

50次阅读

共计 7639 个字符,预计需要花费 20 分钟才能阅读完成。

欢迎大家前往腾讯云 + 社区,获取更多腾讯海量技术实践干货哦~
本文由 likunhuang 发表于云 + 社区专栏

实现背景
应用的使用流畅度,是衡量用户体验的重要标准之一。Android 由于机型配置和系统的不同,项目复杂 App 场景丰富,代码多人参与迭代历史较久,代码可能会存在很多 UI 线程耗时的操作,实际测试时候也会偶尔发现某些业务场景发生卡顿的现象,用户也经常反馈和投诉 App 使用遇到卡顿。因此,我们越来越关注和提升用户体验的流畅度问题。
已有方案
在这之前,我们将反馈的常见卡顿场景,或测试过程中常见的测试场景使用 UI 自动化来重复操作,用 adb 系统工具观察 App 的卡顿数据情况,试图重现场景来定位问题。
常用的方式是使用 adb SurfaceFlinger 服务和 adb gfxinfo 功能,在自动化操作 app 的过程中,使用 adb 获取数据来监控 app 的流畅情况,发现出现出现卡顿的时间段,寻找出现卡顿的场景和操作。
方式 1:adb shell dumpsys SurfaceFlinger
使用‘adb shell dumpsys SurfaceFlinger’命令即可获取最近 127 帧的数据,通过定期执行 adb 命令,获取帧数来计算出帧率 FPS。
优点:命令简单,获取方便,动态页面下数据直观显示页面的流畅度;
缺点:对于静态页面,无法感知它的卡顿情况。使用 FPS 在静态页面情况下,由于获取数据不变,计算结果为 0,无法有效地衡量静态页面卡顿程度;
通过外部 adb 命令取得的数据信息衡量 app 页面卡顿情况的同时,app 层面无法在运行时判断是否卡顿,也就无法记录下当时运行状态和现场信息。
方式 2:adb shell dumpsys gfxinfo
使用‘adb shell dumpsys gfxinfo’命令即可获取最新 128 帧的绘制信息,详细包括每一帧绘制的 Draw,Process,Execute 三个过程的耗时,如果这三个时间总和超过 16.6ms 即认为是发生了卡顿。
优点:命令简单,获取方便,不仅可以计算帧率,还可以观察卡顿时每一帧的瓶颈处于哪个维度(onDraw,onProcess,onExecute);
缺点:同方式 1 拥有一样的缺点,无法衡量静态页面下的卡顿程度;app 层面依然无法在发生卡顿时获取运行状态和信息,导致跟进和重现困难。
已有的两种方案比较适合衡量回归卡顿问题的修复效果和判断某些特定场景下是否有卡顿情况,然而,这样的方式有几个明显的不足:
1、一般很难构造实际用户卡顿的环境来重现;
2、这种方式操作起来比较麻烦,需编写自动化用例,无法覆盖大量的可疑场景,测试重现耗时耗人力;
3、无法衡量静态页面的卡顿情况;
4、出现卡顿的时候 app 无法及时获取运行状态和信息,开发定位困难。
全新方案
基于这样的痛点,我们希望能使用一套有效的检测机制,能够覆盖各种可能出现的卡顿场景,一旦发生卡顿,能帮助我们更方便地定位耗时卡顿发生的地方,记录下具体的信息和堆栈,直接从代码程度给到开发定位卡顿问题。我们设想的 Android 卡顿监控系统需要达到几项基本功能:
1、如何有效地监控到 App 发生卡顿,同时在发生卡顿时正确记录 app 的状态,如堆栈信息,CPU 占用,内存占用,IO 使用情况等等;
2、统计到的卡顿信息上报到监控平台,需要处理分析分类上报内容,并通过平台 Web 直观简便地展示,供开发跟进处理。
如何从 App 层面监控卡顿?
我们的思路是,一般主线程过多的 UI 绘制、大量的 IO 操作或是大量的计算操作占用 CPU,导致 App 界面卡顿。只要我们能在发生卡顿的时候,捕捉到主线程的堆栈信息和系统的资源使用信息,即可准确分析卡顿发生在什么函数,资源占用情况如何。那么问题就是如何有效检测 Android 主线程的卡顿发生,目前业界两种主流有效的 app 监控方式如下,在《Android 卡顿监控方式实现》这篇文章中我将分别详细阐述这两者的特点和实现。
1、利用 UI 线程的 Looper 打印的日志匹配;
2、使用 Choreographer.FrameCallback
方式 3:利用 UI 线程的 Looper 打印的日志匹配判断是否卡顿
Android 主线程更新 UI。如果界面 1 秒钟刷新少于 60 次,即 FPS 小于 60,用户就会产生卡顿感觉。简单来说,Android 使用消息机制进行 UI 更新,UI 线程有个 Looper,在其 loop 方法中会不断取出 message,调用其绑定的 Handler 在 UI 线程执行。如果在 handler 的 dispatchMesaage 方法里有耗时操作,就会发生卡顿。

只要检测 msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分 UI 线程是否有耗时的操作,从而判断是否发生了卡顿,并打印 UI 线程的堆栈信息。
优点:用户使用 app 或者测试过程中都能从 app 层面来监控卡顿情况,一旦出现卡顿能记录 app 状态和信息,只要 dispatchMesaage 执行耗时过大都会记录下来,不再有前面两种 adb 方式面临的问题与不足。
缺点:需另开子线程获取堆栈信息,会消耗少量系统资源。
方式 4:利用 Choreographer.FrameCallback 监控卡顿
我们知道,Android 系统每隔 16ms 发出 VSYNC 信号,来通知界面进行重绘、渲染,每一次同步的周期为 16.6ms,代表一帧的刷新频率。SDK 中包含了一个相关类,以及相关回调。理论上来说两次回调的时间周期应该在 16ms,如果超过了 16ms 我们则认为发生了卡顿,利用两次回调间的时间周期来判断是否发生卡顿(这个方案是 Android 4.1 API 16 以上才支持)。
这个方案的原理主要是通过 Choreographer 类设置它的 FrameCallback 函数,当每一帧被渲染时会触发回调 FrameCallback,FrameCallback 回调 void doFrame (long frameTimeNanos) 函数。一次界面渲染会回调 doFrame 方法,如果两次 doFrame 之间的间隔大于 16.6ms 说明发生了卡顿。

优点:不仅可用来从 app 层面来监控卡顿,同时可以实时计算帧率和掉帧数,实时监测 App 页面的帧率数据,一旦发现帧率过低,可自动保存现场堆栈信息。
缺点:需另开子线程获取堆栈信息,会消耗少量系统资源。
总结下上述四种方案的对比情况:

SurfaceFlinger
gfxinfo
Looper.loop
Choreographer.FrameCallback

监控是否卡顿



支持静态页面卡顿检测
×
×

支持计算帧率


×

支持获取 App 运行信息
×
×

实际项目使用中,我们一开始两种监控方式都用上,上报的两种方式收集到的卡顿信息我们分开处理,发现卡顿的监控效果基本相当。同一个卡顿发生时,两种监控方式都能记录下来。由于 Choreographer.FrameCallback 的监控方式不仅用来监控卡顿,也方便用来计算实时帧率,因此我们现在只使用 Choreographer.FrameCallback 来监控 app 卡顿情况。
痛点 1:如何保证捕获卡顿堆栈的准确性?
细心的同学可以发现,我们通过上述两种方案(Looper.loop 和 Choreographer.FrameCallback)可以判断是当前主线程是否发生了卡顿,进而在计算发现卡顿后的时刻 dump 下了主线程的堆栈信息。实际上,通过一个子线程,监控主线程的活动情况,计算发现超过阈值后 dump 下主线程的堆栈,那么生成的堆栈文件只是捕捉了一个时刻的现场快照。打个不太恰当的比方,相当于闭路电视监控只拍下了凶案发生后的惨状,而并没有录下这个案件发生的过程,那么作为警察的你只看到了结局,依然很难判断案情和凶手。在实际的运用中,我们也发现这种方式下获取到的堆栈情况,查看相关的代码和函数,经常已经不是发生卡顿的代码了。

如图所示,主线程在 T1~T2 时间段内发生卡顿,上述方案中获取卡顿堆栈的时机已经是 T2 时刻。实际卡顿可能是这段时间内某个函数的耗时过大导致卡顿,而不一定是 T2 时刻的问题,如此捕获的卡顿信息就无法如实反应卡顿的现场。
我们看看在这之前微信 iOS 主线程卡顿监控系统是如何实现的捕获堆栈。微信 iOS 的方案是起检测线程每 1 秒检查一次,如果检测到主线程卡顿,就将所有线程的函数调用堆栈 dump 到内存中。本质上,微信 iOS 方案的计时起点是固定的,检查次数也是固定的。如果任务 1 执行花费了较长的时间导致卡顿,但由于监控线程是隔 1 秒扫一次的,可能到了任务 N 才发现并 dump 下来堆栈,并不能抓到关键任务 1 的堆栈。这样的情况的确是存在的,只不过现上监控量大走人海战术,通过概率分布抓到卡顿点,但依然不是最佳的捕获方案。
因此,摆在我们面前的是如何更加精准地获取卡顿堆栈。为了卡顿堆栈的准确度,我们想要能获取一段时间内的堆栈,而不是一个点的堆栈,如下图:

我们采用高频采集的方案来获取一段卡顿时间内的多个堆栈,而不再是只有一个点的堆栈。这样的方案的优点是保证了监控的完备性,整个卡顿过程的堆栈都得以采样、收集和落地。
具体做法是在子线程监控的过程中,每一轮 log 输出或是每一帧开始启动 monitor 时,我们便已经开启了高频采样收集主线程堆栈的工作了。当下一轮 log 或者下一帧结束 monitor 时,我们判断是否发生卡顿(计算耗时是否超过阈值),来决定是否将内存中的这段堆栈集合落地到文件存储。也就是说,每一次卡顿的发生,我们记录了整个卡顿过程的多个高频采样堆栈。由此精确地记录下整个凶案发生的详细过程,供上报后分析处理(后文会阐述如何从一次卡顿中多个堆栈信息中提取出关键堆栈)。
采样频率与性能消耗
目前我们的策略是判断一个卡顿是否发生的耗时阈值是 80ms(5*16.6ms),当一个卡顿达 80ms 的耗时,采集 1~2 个堆栈基本可以定位到耗时的堆栈。因此采样堆栈的频率我们设为 52ms(经验值)。
当然,高频采集堆栈的方案,必然会导致 app 性能上带来的影响。为此,为了评估对 App 的性能影响,在上述默认设置的情况下,我们做一个简单的测试实验观察。实验方法:ViVoX9 上运行微信读书 App,使用卡顿监控与高频采样,和不使用卡顿监控的情况下,保持两次的操作动作相同,分析性能差异,数据如下:

关闭监控
打开监控
对比情况(上涨)

CPU
1.07%
1.15%
0.08%

Memory
Native Heap
38794
38894
100 kB

Dalvik Heap
25889
26984
1095 kB

Dalvik Other
2983
3099
116 kB

.so mmap
38644
38744
100 kB

没有线程快照
加上线程快照

性能指标
2.4.5.368.91225
2.4.8.376.91678
上涨

CPU
CPU
63
64
0.97%

流量 KB
Flow
28624
28516

内存 KB
NativeHeap
59438
60183
1.25%

DalvikHeap
7066
7109
0.61%

DalvikOther
6965
6992
0.40%

Sommap
22206
22164

日志大小 KB
file size
294893
1561891
430%

压缩包大小 KB
zip size
15
46
206%

从实验结果可知,高频采样对性能消耗很小,可以不影响用户体验。
监控使用 Choreographer.FrameCallback, 采样频率设 52ms),最终结果是性能消耗带来的影响很小,可忽略:
1)监控代码本身对主线程有一定的耗时,但影响很小,约 0.1ms/S;
2)卡顿监控开启后,增加 0.1% 的 CPU 使用;
3)卡顿监控开启后,增加 Davilk Heap 内存约 1MB;
4)对于流量,文件可按天写入,压缩文件最大约 100KB,一天上传一次
痛点 2:海量卡顿堆栈后该如何处理?
卡顿堆栈上报到平台后,需要对上报的文件进行分析,提取和聚类过程,最终展示到卡顿平台。前面我们提到,每一次卡顿发生时,会高频采样到多个堆栈信息描述着这一个卡顿。做个最小的估算,每天上报收集 2000 个用户卡顿文件,每个卡顿文件 dump 下了用户遇到的 10 个卡顿,每个卡顿高频收集到 30 个堆栈,这就已经产生 20001030=60W 个堆栈。按照这个量级发展,一个月可产生上千万的堆栈信息,每个堆栈还是几十行的函数调用关系。这么大量的信息对存储,分析,页面展示等均带来相当大的压力。很快就能撑爆存储层,平台无法展示这么大量的数据,开发更是没办法处理这些多的堆栈问题。因而,海量卡顿堆栈成为我们另外一个面对的难题。
在一个卡顿过程中,一般卡顿发生在某个函数的调用上,在这多个堆栈列表中,我们把每个堆栈都做一次 hash 处理后进行排重分析,有很大的几率会是 dump 到同一个堆栈 hash,如下图:

我们对一个卡顿中多个堆栈进行统计,去重后找出最高重复次数的堆栈,发现堆栈 C 出现了 3 次,这次卡顿很有可能就是卡在堆栈 3 反映的函数调用上。由于采样频率不低,因此出现卡顿后一般都有不少的卡顿,如此可找出重复次数最高的堆栈,作为重点分析卡顿问题,从而进行修复。
举个实际上报数据例子,可以由下图看到,一个卡顿如序号 3,在 T1~T2 时间段共收集到 62 个堆栈,我们发现大部分堆栈都是一样的,于是我们把堆栈 hash 后尝试去重,发现排重后只有 2 个堆栈,而其中某个堆栈重复了 59 次,我们可以重点关注和处理这个堆栈反映出的卡顿问题。

把一个卡顿抽离成一个关键的堆栈的思路,可以大大降低了数据量,前面提及 60W 个堆栈就可以缩减为 2W 个堆栈(2000101=2W)。
按照这个方法,处理后的每个卡顿只剩下一个堆栈,进而每个卡顿都有唯一的标识(hash)。到此,我们还可以对卡顿进行聚类操作,进一步排重和缩小数据量。分类前对每个堆栈,根据业务的不同设置好过滤关键字,提取出感兴趣的代码行,去除其他冗余的系统函数后进行归类。目前主要有两种方式的分类:
1、按堆栈最外层分类,这种分类方法把同样入口的函数导致的卡顿收拢到一起,开发修复对应入口的函数来解决卡顿,然而这种方式有一定的风险,可能同样入口但最终调用不同的函数导致的卡顿则会被忽略;
2、按堆栈最内层分类,这种分类方法能收拢同样根源问题的卡顿,缺点就是可能忽略调用方可能有多个业务入口,会造成 fix 不全面。
当然,这两种方式的聚类,从一定程度上分类大量的卡顿,但不太好控制的是,究竟要取堆栈的多少层作为识别分类。层数越多,则聚类结果变多,分类更细,问题零碎;层数越少,则聚类结果变少,达不到分类的效果。这是一个权衡的过程,实际则按照一定的尝试效果后去划分层数,如微信 iOS 卡顿监控采用的策略是一级分类按最内层倒数 2 层分类,二级分类按最内层倒数 4 层。

对于我们产品,目前我们没有按层数最内或最外来划分,直接过滤出感兴趣的关键字的代码后直接分类。这样的分类效果下来数据量级在承受范围内,如之前的 2W 堆栈可聚类剩下大约 2000 个(视具体聚类结果)。同时,每天新上报的堆栈都跟历史数据对比聚合,只过滤出未重复的堆栈,更进一步地缩减上报堆栈的真正存储量。
卡顿监控系统的处理流程

用户上报
目前我们的策略是:
1、通过后台配置下发,灰度 0.2% 的用户量进行卡顿监控和上报;
2、如果用户反馈有卡顿问题,也可实时捞取卡顿日志来分析;
3、每天灰度的用户一个机器上报一次,上报后删除文件不影响存储空间。
后台解析
1、主要负责处理上报的卡顿文件,过滤、去重、分类、反解堆栈、入库等流程;
2、自动回归修复好的卡顿问题,读取 tapd 卡顿 bug 单的修复结果,更新平台展示,计算修复好的卡顿问题,后续版本是否重新出现(修复不彻底)
平台展示
上报处理后的卡顿展示平台
http://test.itil.rdgz.org/wel…
主要展示卡顿处理后的数据:
1、以版本为维度展示卡顿问题列表,按照卡顿上报重复的次数降序列出;
2、归类后展示每个卡顿的关键耗时代码,也可查看全部堆栈内容;
3、支持操作卡顿记录,如搜索卡顿,提 tapd 单,标注已解决等;
4、展示每个版本的卡顿问题修复数据情况,版本分布,监控修复后是否重现等。

自动提单
实际使用中,为了增强跟进效果,我们设立一些规则,比如卡顿重复上报超过 100 次,卡顿耗时达到 1000ms 等,自动提 tapd bug 单给开发处理,系统也会自动更新卡顿问题的修复情况和数据,开发只需定期 review tapd bug 单处理修复卡顿问题即可,整个卡顿系统从监控,上报,分析,聚类,展示,提单到回归,整个流程自动化实现,不再需要人工介入。
实际应用效果
1、接入产品:微信读书,企业微信,QQ 邮箱
2、应用场景:现网用户的监控,发布前测试的监控,每天自动化运行的监控
3、发现问题:三个多月时间,归类后的卡顿过万,提 bug 单约 500,开发已解决超过 200 个卡顿问题
卡顿监控的组件化
考虑到 Android 卡顿监控的通用性,除了应用于 Android WeRead 中,我们也推广到广研的其他产品中,如企业微信,QQ 邮箱。因此,在开发 GG 的努力下,推出了卡顿监控库 http://git.code.oa.com/moai/m…,其他 Android 产品可快速接入卡顿监控的 SDK 来监控 app 卡顿情况。
目前 monitor 卡顿监控库主要有监控主线程卡顿情况,获取平均帧率使用情况,高频采样和获取卡顿信息等基本功能。这里要注意几点:
1、采样堆栈信息的频率和卡顿耗时的阈值均可在 SDK 中设置;
2、SDK 默认判断一个卡顿是否发生的耗时阈值是 80ms(5*16.6ms)
3、采样堆栈的频率是 52ms(约 3 帧 +,尽量错开系统帧率的节奏,堆栈可尽量落到绘制帧过程中)
4、启动监控后,卡顿日志就会不断通过内部的 writer 输出,实现 MonitorLogWriter.setDelegate 才能获取这些日志,具体的日志落地和上报策略因为各个 App 不同所以没有集成到 SDK 中
5、monitor start 后一直监控主线程,包括切换到后台时也会,直到主动 stop 或者 app 被 kill。所以在切后台时要主动 stop monitor, 切前台时要重新 start
1. 组件引入方式

2. 主线程卡顿监控的使用方式
1)启动监控

2)停止监控

3)获取卡顿信息

app 中加入监控卡顿 SDK 后,会实时输出卡顿的时间点和堆栈信息,我们将这些信息写入日志文件落地,同时每天固定场景上报到服务器,如每天上报一次,用户打开 app 后进行上报等策略。收集不同用户不同手机不同场景下的所有卡顿堆栈信息,可供分析,定位和优化问题。
特别致谢
此文最后特别感谢阳经理(ayangxu)、豪哥(veruszhong)、cginechen 对 Android 卡顿监控组件化的鼎力支持,感谢姑姑(janetjiang)悉心指导与提议!希望卡顿监控系统能越来越多地暴露卡顿问题,在大家的共同努力下不断提升 App 的流畅体验!

相关阅读 Javascript 框架设计思路图小程序优化 36 计【每日课程推荐】机器学习实战!快速入门在线广告业务及 CTR 相应知识

正文完
 0