Android稳定性-45-Android 帧率、卡顿和响应延迟:从用户感知到系统时间线
帧率、卡顿和响应延迟是用户最容易感知、也最容易被争论的一类 Android 稳定性问题。用户说“卡”,可能是点击后没有反馈,可能是列表滑动掉帧,可能是动画不连贯,可能是页面打开慢,可能是键盘弹出迟,可能是系统已经接到触摸但应用主线程还没处理。不同现象背后的链路完全不同,如果测试报告只写“操作不流畅”,研发很难判断该看业务代码、渲染、Binder、I/O、调度、温控还是系统服务。
稳定性测试里的卡顿还常常不是单点性能缺陷。长稳后期内存回收变频繁,日志写入变多,温度升高触发降频,后台任务抢 CPU,Binder 调用排队,RenderThread 等 GPU,SurfaceFlinger 合成错过 vsync,这些因素叠在一起,就会让前台场景从偶发掉帧变成持续迟滞。看起来是滑动卡,实际上可能是资源退化的结果。
本文把帧率、卡顿和响应延迟放到“用户动作到系统时间线”的视角下讲:如何定义现象,如何采集帧数据和 Perfetto,如何区分输入延迟、主线程阻塞、渲染慢、合成慢和系统资源问题。文中包含完整案例、命令、表格、误判、检查清单、输出物模板和小结,目标是让卡顿问题能被复现、被量化、被评审。
一、先区分三种用户感知
“卡顿”这个词太宽。测试记录时最好先把它拆成三类用户感知:帧率不稳、响应延迟和长时间无响应。帧率不稳通常表现为滑动或动画不连续,用户仍能操作,但画面不顺。响应延迟是点击、输入、返回、打开页面后反馈慢,用户感到系统“慢半拍”。长时间无响应则接近 ANR 或卡死,用户动作多次没有结果。
这三类感知对应的证据不同。帧率不稳要看 frame time、jank、SurfaceFlinger、RenderThread、GPU、刷新率。响应延迟要看输入事件、主线程消息、业务耗时、Binder 调用、启动链路。长时间无响应要看 traces、锁等待、I/O、系统服务和调度。把它们混在一起,会导致证据和结论错位。
同一个场景也可能同时包含多种问题。比如打开相机后预览首帧慢属于响应延迟,预览过程中画面抖动属于帧率问题,点击快门后保存转圈 6 秒可能是 I/O 或业务等待。如果报告只写“相机卡”,每个模块都能解释一部分,却没人对完整体验负责。
因此问题单第一段就要写清楚用户动作、预期反馈、实际延迟、发生频率和影响范围。最好附一段短视频,但视频不能替代数据。视频用于说明用户感知,数据用于说明系统链路。
二、建立一条时间线
卡顿分析最重要的是时间线。一次点击从手指落下到用户看到反馈,中间可能经历 InputReader、InputDispatcher、应用主线程、Choreographer、RenderThread、GPU、BufferQueue、SurfaceFlinger、Display。任何一个环节慢,用户都可能说卡。
稳定性测试不需要每次都把 Android 渲染体系讲一遍,但要在证据里说明卡在哪个阶段。输入事件是否及时送达?应用主线程是否及时处理?业务代码是否阻塞?一帧是否按时提交?SurfaceFlinger 是否按时合成?CPU/GPU 是否因为温度或后台负载不足以支撑目标帧率?这些问题决定了责任边界。
Perfetto 是最适合做时间线分析的工具。它能把输入、调度、应用线程、Binder、SurfaceFlinger、频率、内存、I/O 放到同一个视图里。如果项目暂时没有 Perfetto 流程,也要用 logcat 时间戳、dumpsys gfxinfo、SurfaceFlinger --latency、业务埋点和脚本日志拼出近似时间线。
报告里建议用“用户动作 T0”作为锚点。例如 T0 点击消息列表,T0+35 ms 输入到达应用,T0+70 ms 主线程开始处理,T0+480 ms 数据库查询结束,T0+520 ms 第一帧提交,T0+760 ms SurfaceFlinger 显示结果。这样的描述一眼能看出主要延迟在业务查询,而不是合成。
三、基础命令:帧率、线程和系统状态
帧率和响应延迟的基础采集可以从轻量命令开始。先确认前台窗口、目标包、帧统计和线程状态,再决定是否抓 Perfetto。
1 | adb shell dumpsys window | grep -E "mCurrentFocus|mFocusedApp" |
如果出现接近 ANR 的响应延迟,要补 traces 和系统服务状态。
1 | adb shell kill -3 $(adb shell pidof com.example.app | tr -d '\r') |
Perfetto 采集可以使用项目统一配置。下面是一个简化思路,正式环境建议维护固定配置文件,避免每个人临时抓出来的 trace 不可比较。
1 | adb shell perfetto -o /data/misc/perfetto-traces/jank_trace.perfetto-trace \ |
命令采集要覆盖问题窗口。很多卡顿只发生在点击后 1 到 3 秒,抓晚了只剩恢复后的现场。自动化脚本最好能在检测到帧耗时或响应超阈值时立即打点并触发 trace。
四、指标表:不要只写平均帧率
平均帧率会掩盖很多问题。一个列表滑动 10 秒平均 58 fps,但其中连续 5 帧超过 100 ms,用户仍然会觉得明显卡。响应延迟也不能只写平均值,P95、P99、最大值和超阈值次数更接近用户感知。
| 指标 | 适用场景 | 说明 | 风险信号 |
|---|---|---|---|
| 平均 FPS | 连续动画、滑动 | 粗略衡量整体流畅度 | 均值下降明显 |
| Jank 次数 | 动画和滑动 | 超过帧预算的帧数 | 集中出现在关键动作 |
| P95 帧耗时 | 长稳流畅度 | 排除少量极端值后看尾部 | P95 超过 2 到 3 帧预算 |
| 最大连续掉帧 | 用户感知 | 连续不顺比单帧更明显 | 连续多帧超过 50 ms |
| 点击到首帧 | 响应延迟 | 用户动作到画面反馈 | 超过业务阈值 |
| 主线程最长消息 | 应用响应 | 单个 message 执行时间 | 超过 200 ms 需分析 |
| SurfaceFlinger 合成耗时 | 系统显示 | 合成是否错过 vsync | 多窗口或高负载异常 |
表格中的阈值要按场景定。60 Hz、90 Hz、120 Hz 的帧预算不同;相机预览、桌面动画、长列表、地图缩放、游戏场景的容忍度也不同。稳定性报告可以写统一红线,但问题单里要说明该场景为什么采用这个阈值。
另外,帧率指标需要和资源指标放在一起看。如果掉帧只在 CPU 降频后出现,根因可能在温控;如果掉帧伴随 kswapd 活跃和 GC 增多,可能是内存压力;如果点击后首帧慢且 traces 指向 SQLite,可能是数据库;如果 SurfaceFlinger 自身繁忙,就不能只推给应用。
五、完整案例:消息列表滑动卡顿
某版本在长稳业务遍历中发现消息列表滑动不稳定。测试视频里可以看到,列表从第 3 屏开始出现明显停顿,停顿后又恢复。最初问题被写成“消息列表掉帧”,应用开发看了业务日志后认为接口耗时正常,渲染同学看 GPU 指标也没有明显异常,问题一度无法推进。
测试重新设计复现:同一账号预置 5000 条消息,冷启动后等待同步完成,执行固定速度滑动 10 次,每次滑动 8 秒。采集 gfxinfo framestats、Perfetto、logcat 和内存。结果显示问题版本 P95 帧耗时 46 ms,最大连续掉帧 9 帧;基线版本 P95 为 22 ms,最大连续掉帧 3 帧。
Perfetto 里看到,掉帧窗口并不是 GPU 忙,而是应用主线程在滑动过程中频繁做消息已读状态写入,每次触发 SQLite 事务,同时还有日志模块同步写文件。RenderThread 有时在等主线程提交新帧,SurfaceFlinger 大部分时间正常。更关键的是,长稳 24 小时后数据库文件变大,checkpoint 更频繁,掉帧比刚刷机时明显。
修复方案不是简单“优化渲染”。业务把已读状态改成批量提交,滑动过程中只做内存标记,离开页面或空闲时合并写入;日志模块取消主路径同步写;测试侧增加 5000 条消息和 24 小时后复测两个场景。复测后 P95 帧耗时降到 24 ms,最大连续掉帧 3 帧,用户视频感知恢复。
这个案例说明,卡顿问题要避免先入为主。用户看到的是列表不顺,但证据链指向主线程数据库写入和日志同步,而不是 GPU 或布局复杂度。只有时间线能把争论压下来。
六、输入延迟怎么查
响应延迟不一定发生在应用内部。触摸事件从硬件到应用,需要经过 InputReader 和 InputDispatcher。如果系统负载高、目标窗口不可接收、应用主线程忙、窗口焦点异常,都可能导致输入排队。典型日志包括 Input dispatching timed out、Waiting because the touched window has not finished processing 等。
查输入问题时,先确认当前焦点和窗口状态,再看目标应用主线程是否忙。dumpsys input 可以看到 input dispatcher 状态,dumpsys window 可以确认焦点,traces 可以看到应用主线程当时在做什么。如果主线程正在执行长任务,输入慢只是结果;如果主线程空闲但事件没有送到,就要看窗口和系统输入链路。
常见场景包括:弹窗遮挡但脚本仍点击底层窗口,Activity 切换中窗口未准备好,应用主线程长时间执行同步初始化,system_server 的输入相关线程被锁或 I/O 阻塞,设备温控降频导致整体处理慢。测试记录里要写清楚点击坐标、目标控件、当前窗口和焦点,否则开发很难复现。
自动化脚本也可能制造输入延迟假象。比如连续点击太快、页面还没稳定就滑动、动画未结束就断言、ADB input 与真实触摸差异。这些都要通过固定节奏、录屏和窗口状态来排除。
七、主线程、RenderThread 和 SurfaceFlinger
应用卡顿常见在三条线上:主线程、RenderThread、SurfaceFlinger。主线程负责处理输入、生命周期、布局、绘制命令生成;RenderThread 负责部分渲染工作;SurfaceFlinger 负责系统合成。三者任何一处超时都会影响画面,但修复方向不同。
主线程问题常见原因是业务逻辑、锁等待、Binder 调用、数据库、I/O、复杂布局、图片解码、GC。证据通常来自 traces、Perfetto slice、主线程消息耗时和业务埋点。RenderThread 问题可能和硬件加速、纹理、动画、GPU 工作有关。SurfaceFlinger 问题则可能涉及多窗口合成、显示驱动、系统负载和刷新率策略。
报告里不要只写“UI 线程卡顿”,要写具体等待点。如果主线程等待 Binder 返回,对端是谁?如果等待锁,锁由哪个线程持有?如果是布局耗时,哪个页面、列表数量、图片尺寸?如果 RenderThread 忙,是否和特定动画或纹理上传相关?如果 SurfaceFlinger 掉帧,其他应用是否同时受影响?
这类分析不要求测试同学直接修代码,但要求把问题交给正确的人。应用主线程数据库写入、Framework 输入调度、SurfaceFlinger 合成异常、thermal 降频导致全局掉帧,是四个完全不同的处理路径。
八、长稳后期卡顿要看资源退化
有些卡顿刚刷机不复现,跑 24 小时或 72 小时后才出现。这类问题往往和资源退化有关。常见原因包括内存泄漏导致 GC 频繁、线程和 FD 增长、日志文件膨胀、数据库变大、缓存清理不及时、温度升高、后台任务堆积。
长稳后期的卡顿报告必须包含趋势数据。比如目标进程 PSS 从 500 MB 增到 1.6 GB,GC 次数增加,滑动 P95 帧耗时从 24 ms 增到 60 ms;或者温度从 32°C 升到 44°C 后 CPU 大核频率下降,点击响应从 300 ms 退化到 1.2 s。没有趋势,只贴最终卡顿窗口,很难说明为什么前期正常后期异常。
资源退化类问题也要做恢复验证。杀掉目标进程是否恢复?清理日志是否恢复?降温是否恢复?删除数据库缓存是否恢复?恢复动作不是修复方案,但能帮助判断方向。比如降温后帧率恢复,说明温控链路很关键;清理数据库后恢复,说明数据规模和查询写入要重点看。
稳定性测试的优势正是在长时间运行中发现这些变化。单次性能测试可能看不到,长稳数据能把“偶发卡”变成“随运行时长退化”的版本风险。
九、常见误判
| 误判 | 为什么错 | 更好的做法 |
|---|---|---|
| 平均 FPS 达标就认为不卡 | 尾部帧耗时和连续掉帧更影响感知 | 看 P95、P99、最大连续掉帧 |
| 看到掉帧就找 GPU | 主线程、I/O、温控都可能导致掉帧 | 先看时间线里等待发生在哪 |
| 点击慢一定是网络慢 | 首帧慢可能来自初始化、Binder、数据库 | 记录点击到各阶段耗时 |
| 只测刚刷机 | 长稳后资源退化更接近稳定性风险 | 加入 24h/72h 后复测 |
| 自动化失败等于卡顿 | 脚本节奏和断言也可能不合理 | 用录屏、窗口状态和帧数据确认 |
| ANR 才算严重 | 频繁 1 秒延迟不会触发 ANR 但用户很明显 | 设置响应延迟阈值 |
卡顿问题的关键不是给它贴一个“性能”或“稳定性”的标签,而是用数据说明它是否影响真实用户路径,是否随版本退化,是否会在长稳后放大。
十、检查清单
- 是否明确用户动作、目标页面、预期反馈和实际延迟。
- 是否区分帧率不稳、响应慢和长时间无响应。
- 是否有录屏、脚本日志和系统时间对齐。
- 是否采集
gfxinfo、SurfaceFlinger、traces 或 Perfetto。 - 是否记录刷新率、温度、CPU/GPU 频率和后台负载。
- 是否看 P95、P99、连续掉帧和点击到首帧,而不只看平均 FPS。
- 是否确认问题是刚刷机复现还是长稳后复现。
- 是否排除自动化脚本节奏、网络数据、账号数据规模差异。
- 是否给出主线程、RenderThread、SurfaceFlinger、Input 或资源退化方向。
- 是否完成修复前后同条件复测。
这份清单适合做成卡顿问题单的必填项。少一个字段不一定不能定位,但缺得越多,问题越容易变成主观争论。
十一、输出物模板
1 | 问题标题: |
十二、小结
十三、修复验证要回到用户路径
卡顿修复最容易出现“指标局部变好,用户仍然觉得慢”的情况。比如把某个方法耗时从 300 ms 降到 120 ms,但点击到首帧仍然超过 1 秒;把平均 FPS 提高到 58,但连续掉帧仍集中在页面进入动画;把日志同步写去掉后滑动恢复,但长稳 48 小时后数据库变大又退化。因此复测不能只验证研发给出的局部指标,要回到最初的用户路径。
复测用例要保持账号、数据量、运行时长、网络和设备状态一致。如果问题是在 5000 条消息、长稳 24 小时后出现,复测就不能只用新账号滑动 2 分钟。如果问题是在温升后出现,复测就要让设备进入同等热状态。如果问题和低存储、后台同步、弱网有关,也要把这些条件带回去。卡顿问题对上下文很敏感,条件一变,结论就可能失真。
复测结果建议同时给出用户视频和数据表。视频证明主观感知恢复,数据表证明 P95、P99、连续掉帧、点击到首帧、主线程最长消息等指标恢复。两者缺一不可:只有视频容易争议,只有数字又可能忽略真实感受。
十四、把卡顿纳入长稳日报
很多团队的日报只统计 Crash、ANR 和重启,卡顿只有在严重到脚本失败时才出现。这会让版本在“没有硬失败”的情况下带着明显体验退化进入灰度。稳定性日报至少应该记录几类轻量指标:关键路径点击响应、列表滑动 P95 帧耗时、前台启动耗时、设备温度、CPU 频率、长稳运行时长后的变化。
日报不需要每天给所有页面做完整性能报告,但要选高频路径做哨兵。比如桌面滑动、设置打开、相机预览、消息列表、通知栏下拉、键盘弹出。这些路径一旦退化,通常代表系统资源、输入链路或应用主路径出现变化。把它们作为长稳中的周期性检查,比最终用户反馈“版本变卡”更早发现风险。
趋势比单次数字重要。如果某页面 P95 帧耗时从第一天 24 ms、第二天 31 ms、第三天 55 ms,哪怕没有 ANR,也应该进入风险观察。长稳稳定性不是只看进程有没有死,还要看用户路径是否随时间变慢。
十五、跨团队协作时如何减少争议
卡顿问题天然跨团队。应用说系统调度慢,系统说应用主线程重,图形说 SurfaceFlinger 正常,性能说温控降频,测试说用户就是卡。减少争议的办法不是在会上反复描述感受,而是把同一条时间线给所有人看。T0 用户点击,T0+40 ms 输入送达,T0+80 到 T0+620 ms 主线程执行数据库写入,T0+650 ms 第一帧提交,SurfaceFlinger 合成正常。这样的证据能直接决定谁先处理。
如果证据还不足,日报或问题单里要写下一步补什么,而不是让每个团队各自猜。比如需要应用加主线程消息耗时,需要系统提供 input trace,需要图形同学看 SurfaceFlinger,需要性能同学确认 thermal 状态。测试同学可以不拥有所有工具,但要负责把问题推进到下一份证据。
跨团队结论还要避免绝对话。可以写“当前证据显示主要等待在应用主线程数据库写入,尚未发现 SurfaceFlinger 合成异常”,不要写“肯定不是系统问题”。稳定性定位经常随着证据增加而修正,报告要给出当前判断和证据边界。
十六、启动慢和页面切换慢也要纳入响应延迟
很多团队把卡顿只理解成滑动掉帧,忽略启动和页面切换。实际上用户最敏感的响应延迟往往发生在点击图标、打开页面、返回桌面、弹出键盘、切换相机模式这些动作上。它们不一定表现为连续掉帧,但会让用户觉得系统迟钝。
启动慢要拆成冷启动、温启动、热启动和页面首帧。冷启动受进程创建、类加载、资源加载、数据库初始化、网络同步影响;温启动可能受 Activity 恢复、缓存命中、后台任务影响;热启动更能暴露主线程和渲染路径。测试报告里不能只写“启动 2.3 秒”,要说明从哪个起点到哪个终点,是点击到进程创建、点击到首帧,还是点击到页面可操作。
页面切换慢也要看中间状态。有些页面首帧很快,但骨架屏后内容 3 秒才可用;有些页面动画流畅,但点击后业务请求阻塞;有些页面返回时等待保存状态,用户感知是返回键不灵。稳定性测试要选择用户真正关心的终点,比如首帧、首个可点击控件、数据加载完成、输入框可输入。
这些响应指标适合在自动化里长期采样。每次版本构建后,用固定账号和数据量跑启动、返回、切 tab、弹键盘、打开相机等动作,记录 P50、P95 和失败率。长稳后再跑一轮同样动作,可以发现运行时间带来的退化。
十七、刷新率和帧预算不能混用
现在很多设备支持 60 Hz、90 Hz、120 Hz 或动态刷新率。不同刷新率下帧预算不同:60 Hz 约 16.6 ms,90 Hz 约 11.1 ms,120 Hz 约 8.3 ms。如果测试报告不写刷新率,帧耗时判断就会混乱。同样 18 ms 的帧,在 60 Hz 下可能只是轻微超时,在 120 Hz 下已经错过多个刷新机会。
动态刷新率还会带来另一个问题:系统可能根据场景降低刷新率以省电,平均 FPS 下降不一定是卡顿。但如果用户正在快速滑动,刷新率却没有及时升高,或升高后应用帧提交跟不上,就会造成感知退化。报告里要记录测试时的刷新率策略、实际刷新率和目标场景。
对于高刷设备,建议同时看“是否达到目标刷新率”和“帧间隔是否稳定”。一个页面在 120 Hz 下平均 90 fps,不一定差;如果它在关键动画中帧间隔波动很大,用户仍然会看到不顺。反过来,系统把静态页面降到 60 Hz 是正常省电策略,不应该误判为卡顿。
测试平台可以把刷新率作为指标维度写入 CSV:设备、场景、目标刷新率、实际刷新率、P95 帧耗时、连续掉帧、温度、频率。没有这个维度,不同设备之间的结果很难比较。
十八、脚本稳定性和真实卡顿要分开
自动化稳定性测试里经常出现“脚本超时”,但脚本超时不一定等于用户卡顿。可能是控件定位失败,可能是页面文案变化,可能是网络数据没回来,可能是脚本点击过快,也可能是真实响应慢。日报和问题单要先把脚本失败与用户体验问题分开,否则会造成大量误报。
区分方法并不复杂。第一,看录屏中用户路径是否真的没有反馈。第二,看窗口焦点和当前 Activity 是否符合脚本预期。第三,看系统帧数据和主线程是否有异常。第四,手工按同样节奏复现一次,确认不是脚本等待条件写错。第五,如果脚本超时发生在固定运行时长后,要看资源趋势是否变化。
脚本也要适配卡顿检测。对于关键路径,不要只等待某个控件出现,还要记录从动作到控件出现的耗时;不要只在失败时截图,也要在超阈值但未失败时记录;不要一超时就立刻重启应用,先保留现场。这样脚本才能成为发现稳定性退化的工具,而不是只给出“用例失败”。
真实卡顿确认后,脚本失败可以作为影响证据。比如用户点击发送后 3 秒才出现消息,脚本等待 2 秒超时,这说明自动化阈值与用户体验阈值都被突破。此时问题就不是脚本不稳定,而是产品路径响应退化。
十九、关闭卡顿问题前的回归池
卡顿问题关闭后,最好把复现路径沉淀到回归池。回归池不一定很大,但要保留最容易暴露问题的条件:大数据账号、长稳后状态、低存储、热机、弱网、后台同步、固定滑动速度和固定点击节奏。以后只要应用框架、数据库、日志、渲染、温控或系统调度有改动,就能快速重跑。
回归池还要保留原始阈值。比如消息列表 P95 帧耗时不得超过 30 ms,最大连续掉帧不得超过 4 帧,点击到首帧不得超过 500 ms。阈值不是为了机械拦截所有变化,而是为了让版本退化早一点被看到。没有阈值的回归,只能靠人看视频,效率和一致性都不够。
回归池中的数据集也要定期刷新。真实用户的数据规模、图片尺寸、消息数量和业务入口会变化,如果测试一直使用几个月前的小样本,卡顿风险会被低估。稳定性团队可以每个大版本更新一次高频路径数据集,并保留旧数据集用于横向对比。
帧率、卡顿和响应延迟不能靠一句“流畅”或“不流畅”判断。稳定性测试要把用户动作、帧数据、线程状态、系统资源和长稳趋势连起来,说明卡顿发生在哪个阶段、影响多大、是否退化、修复后是否恢复。
一份好的卡顿分析,不一定一开始就找到代码行,但一定能缩小方向:输入链路、应用主线程、渲染线程、SurfaceFlinger、系统资源或温控。方向清楚,研发才能有效修复;数据清楚,版本评审才能决定是否放行。