Android 性能诊断
性能诊断是把“启动慢”“滑动卡”这样的现象,缩小到具体时间区间、线程和工作,再验证哪项改变能减少用户等待。优化手段只有和原因对应,才有解释力:把任务放到后台可以释放主线程,也可能因为排队、锁或 CPU 竞争,让页面更晚就绪。
回答 Android 性能问题,需要说明发现问题的指标、定位时看到的证据,以及改动后的同条件结果。启动、帧和资源指标的口径见移动端性能指标,本文重点讨论 Android 的采集与推断过程。
1. 面试核心
先记住这几条,再读正文- 从可复现的业务操作出发,先找到变慢的区间,再选择能回答当前问题的工具,避免一开始就同时修改布局、线程和缓存。卡 · 综合题
- Perfetto 的系统时间线和方法分析承担不同责任,耗时长不等于 CPU 计算多;执行、可运行但未调度、阻塞等待要分别解释。卡 · 执行与等待
- 启动优化围绕首帧和业务可用的关键路径,延后非必要工作不能破坏依赖,也不能只移动计时终点。卡 · 综合题
- 帧异常需要关联主线程、RenderThread 与合成过程,布局层数或过度绘制颜色只是线索,必须结合耗时判断。卡 · 帧、布局与绘制
- 方法与分配记录可能扰动运行,采样中没出现不等于从未执行,详细记录中的耗时也不能无条件代表发布表现。卡 · 采样与方法记录
- 线上卡顿监控需要定义观测范围,一次消息处理超时无法覆盖所有渲染、调度与输入延迟。卡 · 综合题
- 能耗分析要关联整机子系统和业务时段,CPU 使用率不能单独说明耗电来源。卡 · 能耗与唤醒
- 优化完成需要重复测量和回归检查,基准测试负责发现变化,时间线负责解释变化。卡 · 同条件回归
2. 从现象到采集方案
诊断的第一步是把现象变成可以重复执行的操作。例如“消息页卡顿”应补充为:在某型号手机、某构建、某份数据下,从顶部连续滑动到第 100 条消息,首次经过图片消息时出现停顿。没有这层约束,改动前后可能运行的是不同工作。
先记录应用版本、设备与系统、数据量、缓存状态、刷新率和温度,再选择工具:
| 当前问题 | 优先需要的证据 |
|---|---|
| 启动的哪段时间变长 | 启动指标、系统 trace、应用自定义区间 |
| 某帧为什么没按时完成 | FrameTimeline、主线程与 RenderThread、CPU 调度和相关工作 |
| 正在运行的代码花在哪里 | 调用栈采样,必要时短时间方法记录 |
| 哪些对象分配或保留过多 | 分配记录、堆转储、引用路径 |
| 哪段业务导致持续耗电 | 电源或电池数据、CPU 与无线电等活动的对应时间线 |
| 修复后是否再次退化 | 固定场景的基准测试与版本间结果 |
采集应覆盖问题发生前、发生时和结束后,太晚启动工具可能只看到主线程已经恢复空闲。还要检查缓冲区、数据源和记录范围是否完整。只看到一张截屏中的长条,不足以复核其他线程和依赖。
3. 系统时间线与应用区间
3.1 执行与等待
系统 trace 把多个线程、CPU 调度以及启用的数据源放在同一时间轴上。Perfetto 可以帮助判断某个长区间里,线程是在执行代码,还是在等待。
| 观察 | 可以继续检查什么 | 不能直接推出什么 |
|---|---|---|
| 线程正在 CPU 上运行 | 热点调用栈、重复计算、布局与序列化 | 区间内每一行代码都同样昂贵 |
| Runnable,但尚未运行 | CPU 竞争、调度、其他繁忙线程及频率 | 线程正在等待 Java 锁 |
| Sleeping 或阻塞 | 锁持有者、条件等待、I/O、同步 Binder 依赖 | 所有睡眠都是异常 |
| 主线程等待,工作线程繁忙 | 等待的是哪个结果,是否在关键路径 | 再增加线程一定更快 |
这里的 Runnable 指调度时间线中的可运行状态。它与 Java 线程转储里的 RUNNABLE 不是同一套观察口径,不应直接互换。一次方法的墙钟耗时包含等待和调度延迟;只有结合 CPU 执行与依赖,才能决定是减少计算、缩短锁范围,还是取消不必要的同步等待。
同步 Binder 调用还可能等待另一个进程。只检查客户端主线程,无法知道服务端是计算慢、被锁阻塞还是等待下游。应沿依赖继续查看,具体机制见Binder。
3.2 同步 Trace 区间
Trace.beginSection() 与 Trace.endSection() 为一段同步工作添加名字,采集时可以把业务操作与系统事件对齐。下面的例子要求 Android 4.3(API 18)及以上:
import android.os.Trace
fun buildVisibleTitles(titles: List<String>): List<String> {
Trace.beginSection("build-visible-titles")
try {
return titles.map { it.trim() }.filter { it.isNotEmpty() }
} finally {
Trace.endSection()
}
}
buildVisibleTitles() 返回去掉首尾空白和空标题后的列表。无论处理成功还是抛出异常,finally 都结束该区间;区间显示的是整个函数中这段工作的经过时间,不是一次独立的 CPU 计时。
同步区间必须在同一线程正确嵌套。不要在开始后直接切换线程,或把可能挂起的协程操作包进去:协程可能在别处恢复,也可能让同一线程交错执行其他任务,破坏区间的线程与嵌套含义。名称应稳定且简短,避免每条消息都生成高开销、包含用户内容的动态名字。
3.3 异步操作的关联
Android 10(API 29)提供 beginAsyncSection() 与 endAsyncSection(),用相同名称和 cookie 关联一次异步操作。cookie 用于区分同名且同时存在的操作,必须由调用方保证在活动操作之间不冲突。
import android.os.Trace
import androidx.annotation.RequiresApi
@RequiresApi(29)
suspend fun <T> traceRefresh(
cookie: Int,
operation: suspend () -> T
): T {
Trace.beginAsyncSection("refresh-feed", cookie)
try {
return operation()
} finally {
Trace.endAsyncSection("refresh-feed", cookie)
}
}
这里记录 operation() 从开始到完成、失败或取消退出的经过时间。它不会让工作自动切到后台,也不会证明期间一直使用 CPU。调用方仍需处理 API 可用性,并保证同一操作只结束一次;进程被终止时,不能指望 finally 一定执行。
4. 启动关键路径
关键路径是当前目标必须等待的一串依赖。只有减少这条路径上的时间,才会直接提前该目标。一个后台初始化任务即使耗时很长,只要首屏不等待它,就不能把它的全部耗时当作首屏延迟。
启动诊断先区分进程和页面初始状态,再看 TTID 与 TTFD。查看系统启动区间,并用自定义区间识别初始化、数据库打开、数据转换和首屏构建。不要只测 Activity.onCreate():进程初始化、组件初始化和后续绘制都可能在它之外。
根据证据处理不同原因:
- 对首屏无用的 SDK 或业务初始化,可以延后,但要明确首次使用时如何等待或失败。
- 首屏必需的独立任务可以并发,但需要限制并发,避免抢占主线程所需的 CPU 或 I/O。
- 重复读取、解析和构建可以减少或缓存,但必须说明失效条件与内存成本。
- 类加载与编译状态影响启动时,应结合 ART 的执行方式评估;Baseline Profile 不能修复主线程上的同步网络等待。
把黑屏改成正确的启动画面改善了视觉反馈,但不等于关键工作完成得更快。报告业务可用时,应在主要内容和交互实际就绪后调用或触发完整显示报告,不能为了让数值更短提前报告。
5. 帧、布局与绘制
Android 12 及以上的 FrameTimeline 可以把预期与实际的帧时间线对应起来。预期区间表示系统给应用完成该帧的时间,实际区间包含应用完成帧并交给合成过程的工作。它还提供应用与 SurfaceFlinger 的关联,帮助区分应用未赶上截止时间和后续合成问题;实际可见的数据仍受系统、渲染路径和采集配置影响。
定位时先找到异常帧,再检查同一时间段的 Choreographer#doFrame、主线程、RenderThread 和 CPU 调度。根据观察继续缩小范围:
- 主线程正在测量和布局:检查重复请求布局、复杂层级、单项内容与数据变化。
- 列表绑定耗时:检查同步图片解码、重复转换、无效全量刷新及复用是否正确,参见 RecyclerView。
- RenderThread 或 GPU 相关工作较长:检查图层、透明混合、过度绘制和大图等证据。
- 线程迟迟没有运行:检查调度竞争,不把整段时间都算成布局执行。
过度绘制指同一帧中同一像素被绘制多次,例如不必要的背景被后面的不透明内容盖住。开发者选项的颜色可以提示重复绘制的位置,但透明叠加有时就是界面效果需要;颜色深不等于已经证明它是当前瓶颈。
布局优化同样需要条件。<include> 用于复用布局,本身不会自动减少层级;<merge> 可以在合适的父容器上下文省去一层包装;ViewStub 可延迟加载暂时不用的 View,但首次展开仍有成本。对于具体页面,应验证减少了哪次测量、布局或绘制,而不是要求所有布局都越扁越好。View 各阶段的职责见 Android View。
6. 方法、分配与能耗
6.1 采样与方法记录
调用栈采样按采样时刻记录正在执行的调用栈,适合寻找持续占用 CPU 的路径。短小且偶发的方法可能没有被采到;样本多也需要区分方法自身工作和包含子调用的总成本。
方法记录通过记录方法进入和退出,提供更细的调用时序,但会增加运行开销。适合围绕已缩小的问题短时间采集,不能把插桩后明显变慢的运行直接当作发布性能。Java/Kotlin 方法视图也不能自动覆盖所有 native 或 GPU 工作。
先用系统时间线定位阶段,再用适合的调用栈或方法数据解释该阶段,通常比一开始录下全部方法更容易判断。还需保留对应构建及符号、混淆映射,避免把无法还原的方法名误当成可读调用链。
6.2 分配与内存
分配记录可以显示对象类型、数量、大小、分配线程和调用栈,用于检查滑动期间是否反复创建临时集合、字符串或大对象。Java/Kotlin 完整分配记录可能显著增加开销,并且需要相应的可调试配置;采样记录与完整记录也有不同的覆盖范围。
如果问题是页面退出后对象仍存活,需要堆与引用链证据;如果问题是短时分配峰值或回收频繁,需要操作区间内的分配与 GC 时间线。两者都可能影响体验,但处理方式不同,见Android 内存泄漏。不能只根据“出现 GC”就认定该帧一定由 GC 导致超时。
6.3 能耗与唤醒
耗电可能来自 CPU、GPU、屏幕、无线电、定位或唤醒等活动。先定义业务区间,再关联电源数据与这些活动。Power Profiler 在支持的设备上提供电源轨数据;电源轨表示硬件子系统的消耗,并不自动等于某个应用独占的耗电,设备支持范围也需要核对。
例如后台反复请求网络,即使每次 CPU 计算不多,也可能持续触发无线电活动。可以检查请求合并、缓存和后台任务条件,再比较业务时效和总耗电。长时间持有唤醒锁、频繁定位或无意义轮询也应依据实际记录判断。功率、总能耗和电池百分比的区别见移动端性能指标。
7. 监控与回归验证
7.1 线上卡顿信号
卡顿监控必须先定义要发现什么。记录主线程一次消息分发的开始与结束,可以发现其中的长任务,但不代表覆盖了所有帧问题:线程迟迟未获调度、渲染线程或 GPU 工作、帧提交后的排队,都可能不在这一次消息执行时长中。
帧回调或平台帧指标可以提供流畅度信号,但也有窗口、版本和渲染路径的适用范围。为了让慢帧可定位,还需要关联页面、操作、设备、版本和必要的线程证据。仅在卡顿结束后抓一次堆栈,可能只抓到空闲位置。
设计时还要控制采样与上报成本,聚合重复事件,并避免在主线程同步写日志或上传。自定义慢任务阈值是监控规则,不能据此宣布系统发生了 ANR。
7.2 同条件回归
Macrobenchmark 适合启动、滚动等完整应用场景;Microbenchmark 更适合较小的代码片段。Macrobenchmark 由独立测试模块驱动目标应用,目标构建应接近发布配置,且满足工具要求的可分析条件。它能够保留结果和 trace,便于同时查看变化幅度与原因。
回归应固定启动模式、数据、缓存准备、编译状态、操作脚本和设备条件。不能用优化后的热启动对比改动前的冷启动,也不能用一轮已经预热的数据对比初次读取。随机波动较大时,保留多轮结果,检查温度及后台负载,而不是只挑最快的一次。
一份完整结论应说明:原来的慢区间是什么,为什么采用这项修改,相同场景下指标怎样变化,以及功能、内存、能耗与首次使用成本是否可接受。若没有真实设备或对应采样,只能说明方案、API 编译结果或逻辑验证,不能声称已经测得启动或帧率提升。
8 · 面试题卡片
本页题目与参考回答
8.1 怎样发现和定位 Android 应用的性能问题?
题型:综合题
考察重点:从业务现象到指标、工具与因果证据。
参考回答
先建立可复现的场景与指标,再定位发生问题的时间段和具体工作。 启动和帧问题可结合系统 trace、线程调度与应用区间;热点代码用调用栈或方法数据,内存用分配及引用证据。根据原因做有范围的修改,再用相同条件验证目标指标和资源代价,不能只列出工具名或优化清单。
8.2 主线程上的一个区间耗时很长,能否直接判断 CPU 计算太多?
考察重点:经过时间、CPU 时间、调度与同步依赖。
相关内容:执行与等待。
参考回答
不能,长区间可能包含锁、I/O、Binder 等待或未获调度的时间。 应查看调度状态、调用栈及等待对象;若等待另一个线程或进程,还要继续沿依赖分析。只有确认主要成本是执行计算,减少计算或移动计算才是有依据的方向。
8.3 项目中的 Android 启动优化可以怎样展开?
题型:综合题
考察重点:启动状态、关键路径、初始化依赖与业务可用。
参考回答
先分别测量首次显示与业务可用,再找到它们必须等待的初始化和渲染工作。 可以减少重复工作、延后首屏不需要的初始化,或在依赖允许时并发执行,但要说明首次使用与失败处理。启动画面和计时埋点必须诚实反映体验;改动后用相同启动、缓存和编译状态重复验证。
8.4 Android 布局可以从哪些方面优化?
考察重点:测量布局成本、复用、延迟加载与过度绘制。
相关内容:帧布局与绘制。
参考回答
围绕已确认的布局或绘制成本,减少无效层级、重复布局、无用刷新与不必要绘制。 include 复用布局,merge 在适合的上下文省去包装层,ViewStub 延迟加载暂时不需要的内容。列表还应检查绑定工作与更新范围。最终要用异常帧的时间线验证,不能只靠层数或颜色判断收益。
8.5 调用栈采样与方法记录的结果应怎样解释?
考察重点:覆盖范围、记录开销和统计含义。
相关内容:采样与方法记录。
参考回答
采样更适合识别持续热点,方法记录更适合查看具体调用时序,但后者通常带来更多扰动。 采样没看到某方法不能证明它没有执行;方法总耗时还要区分自身、子调用和等待。分析应保留构建与符号信息,并结合系统 trace 和接近发布条件的结果判断。
8.6 怎样设计一个定位 Android UI 绘制变慢原因的监控方案?
题型:综合题
考察重点:慢任务与慢帧的范围、现场证据和监控成本。
参考回答
先定义慢帧或响应延迟,再把信号与业务操作及线程证据关联。 主线程消息分发计时能提示长任务,但无法单独覆盖调度、RenderThread 和 GPU 等问题;应按平台条件补充帧指标及采样。记录页面、版本和设备,限制采集上报开销,并避免只在问题结束后取一次空闲堆栈。
8.7 CPU 使用率下降,为什么应用仍可能更耗电?
考察重点:硬件活动、任务时间与能耗归因。
相关内容:能耗与唤醒。
参考回答
耗电还取决于任务时长、屏幕、无线电、定位、GPU 和唤醒等活动。 少量但频繁的后台请求可能延长无线电活跃时间。应在相同业务与设备条件下关联电源数据和活动记录,并区分整机子系统消耗与应用归因,不能只看 CPU 百分比。
8.8 性能修复后,怎样建立能发现退化的验证?
考察重点:测量对象、基准场景、控制变量与证据范围。
相关内容:同条件回归。
参考回答
把真实问题整理成固定场景,在接近发布的构建和目标设备上重复测量。 启动、滚动可用 Macrobenchmark,小段逻辑可用 Microbenchmark;保留结果和 trace,并固定数据、缓存、编译与温度等条件。还要回归功能和资源成本。编译通过或模拟器结果不能替代目标设备上的性能证据。
9. 参考资料
- Android:Trace API
- Perfetto:CPU Scheduling
- Perfetto:FrameTimeline
- Android:App startup time
- Android:Slow rendering
- Android:Record Java/Kotlin methods
- Android:Record Java/Kotlin allocations
- Android:Power Profiler
- Android:Write a Macrobenchmark
- Android:Reuse layouts
- Android:Load views on demand
- Android:Reduce overdraw
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明