移动端性能指标
性能指标是对应用完成任务的速度、流畅程度和资源消耗的量化描述。“启动只需 500 毫秒”还不是一个完整结论:从点击图标还是进入页面开始计时,结束时只有骨架屏还是已经能看内容,都会改变这个数字的含义。
面试中说明一次优化,需要把指标、测量条件和用户体验连起来。先确定测量的对象,再用相同条件比较改动前后;Android、iOS、Flutter 和 React Native 的工具不同,这个判断顺序相同。
1. 面试核心
先记住这几条,再读正文- 一项指标需要明确对象、起止点、单位和测量条件。单次最快结果不能代表整类设备或用户。卡 · 测量对象与实验条件
- 首次显示与业务可用是不同阶段,冷启动、已有进程的启动和页面返回需要分别记录,不能混成同一个启动时间。卡 · 启动与业务可用时间
- 流畅度要看帧的连续性,平均 FPS 不能代替慢帧分布和输入响应时间;帧预算还取决于实际刷新率。卡 · 帧耗时与交互延迟
- 内存既要看峰值,也要看重复操作后保留的对象;堆分配量、进程驻留量和平台报告的内存占用不是同一指标。卡 · 内存占用与分配
- 功率表示消耗能量的速度,能耗还与持续时间有关;降低瞬时功率不一定降低一次任务的总耗电。卡 · 功率与能耗
- 网络耗时应覆盖用户等待的完整操作,请求排队、连接、传输、解析和界面更新不能互相替代。卡 · 网络与端到端耗时
- 改动前后需要同条件对照,分位数、失败率、资源代价与样本规模一起解释,避免用一个变好的均值掩盖其他退化。卡 · 综合题
2. 测量对象与实验条件
一份可以复现的性能记录,至少应回答以下问题。
| 要说明的内容 | 示例 |
|---|---|
| 业务操作 | 登录后打开有 100 条记录的消息列表 |
| 时间区间 | 点击入口至首屏内容显示且按钮可操作 |
| 构建与环境 | 应用版本、发布或性能分析构建、系统版本、设备型号 |
| 初始状态 | 进程是否存在,账号、数据库和缓存是否准备好 |
| 外部条件 | 网络、屏幕刷新率、温度、充电状态和后台负载 |
| 结果与样本 | 每次耗时、失败与超时、总次数及汇总方法 |
“低端机”不是足够明确的设备条件。同一台手机在温度升高、系统降频时,也可能得到不同结果。做对照实验应尽量固定这些条件,并记录无法控制的变化。
调试构建和发布构建的执行成本可能不同。Flutter 的移动应用通常在真机的 profile 模式分析性能;React Native 应检查 release 构建,避免把开发检查和调试环境的成本当成发布表现。原生应用也需要选择接近发布配置的测量对象。模拟器适合验证功能和复现部分逻辑,但它使用宿主机资源,不能直接代表目标手机的性能。
采样工具本身也有成本。详细记录每次分配或每个方法调用,可能改变原来的时序。应保留采集配置,并用较轻量的指标确认问题在真实使用条件下仍然存在。
3. 启动与业务可用时间
首次显示时间衡量用户何时看到应用画面;业务可用时间衡量何时能完成当前页面的主要操作。它们可以共用起点,但终点不同。
例如消息列表先显示标题和骨架屏,再读取缓存并显示消息。前一个时刻能说明应用有了视觉反馈,后一个时刻才说明消息可读。若把读缓存的工作从首帧前移到首帧后,首次显示可能变快,业务可用时间却未必改善。两项应同时观察。
Android 用 TTID 表示首次显示时间,用 TTFD 表示完整显示时间。系统可以识别首帧;业务何时就绪需要应用正确报告。不能在 onCreate 结束时无条件调用 reportFullyDrawn(),就把它当作内容已经可用的证据。iOS 或跨平台的自定义启动埋点也应明确结束条件,不把相似名称当成完全相同的系统口径。
启动还取决于初始状态:
- 进程尚未创建,需要承担进程初始化和页面创建的成本。
- 进程已存在,但目标页面需要重新创建。
- 页面仍在,只需回到前台并恢复交互。
这些状态对应不同的工作量。报告最好明确进程和页面状态,并注明工具采用的冷、温、热启动定义。进程冷启动不等于磁盘缓存、服务端缓存和系统缓存全部为空;首次安装后的初始化也要单独识别。
4. 帧耗时与交互延迟
帧耗时描述生成或提交某一帧所需的时间,具体起止点由工具定义。FPS 是每秒帧数,需要说明计的是应用产出、提交还是实际呈现的帧。
在固定刷新率下,相邻刷新机会的间隔可以用 1000 / 刷新率 毫秒估算:
| 刷新率 | 刷新间隔 |
|---|---|
| 60 Hz | 约 16.67 ms |
| 90 Hz | 约 11.11 ms |
| 120 Hz | 约 8.33 ms |
这不是“每个主线程方法都能执行这么久”的许可。输入处理、布局、绘制、渲染提交以及系统调度要共同赶上帧的截止时间,CPU 与 GPU 还可能处理不同帧。高刷新率或可变刷新率场景,应以实际帧时间线和截止时间判断。
平均帧率会掩盖不均匀的等待。下面是用来说明统计差异的人工数据,不是设备测试:两组各有 100 个帧间隔,一组全部为 16 ms;另一组有 99 个 15 ms 和 1 个 115 ms。两组总时长都为 1600 ms,按这个口径算出的平均帧率都为 62.5 FPS,但第二组包含一次明显更长的间隔。
因此还需要慢帧比例、帧耗时分位数、最长停顿以及停顿发生的业务位置。静止页面不需要持续生成新画面,不能只因 FPS 低就判断卡顿;点击后没有及时响应,也不能只因滚动动画仍流畅就判断交互正常。React Native 的 JS 工作与原生 UI 工作、Flutter 的 UI 与光栅化工作,都可能提供不同线索,最终要对应用户看到和操作到的结果。
5. 内存占用与分配
内存指标首先要区分“曾经分配过多少”和“现在仍占用多少”。一个操作可以累计分配大量短命对象,却没有留下泄漏;也可以每次只留下少量对象,经过多次进出页面后持续增长。
| 指标或观察 | 能帮助回答的问题 |
|---|---|
| 当前占用与峰值 | 操作过程是否出现过很高的资源压力 |
| 分配次数与分配速率 | 是否反复创建临时对象、触发额外回收工作 |
| 操作完成后保留的对象 | 页面、回调或缓存是否仍持有不再需要的内容 |
| 托管堆、原生与图形分类 | 增长主要出现在哪一类内存 |
| 进程整体内存统计 | 除语言对象以外,映射、线程栈和图形资源等的总体影响 |
Android 的 RSS 统计进程驻留的内存页,共享页会计入各进程;PSS 按共享进程数分摊共享页。Java 堆的分配量不等于其中任何一个。iOS 的内存报告和 Allocations 也有各自的统计范围,不能拿一端的堆分配量与另一端的进程内存数值直接比较。
释放对象后,分配器或运行时可能保留空间用于后续分配,因此进程内存没有立刻下降,不足以证明仍存在泄漏。反过来,短时内存曲线平稳也不能证明所有对象的生命周期正确。诊断需要结合引用关系、分配记录与重复操作后的趋势。Android 的具体判断见Android 内存泄漏。
6. 功率与能耗
功率表示单位时间消耗的能量,通常用瓦或毫瓦表示;能耗表示一段时间内消耗的总能量,可以用焦耳或毫瓦时表示。功率随时间变化时,需要对整个观察区间累积。
在恒定功率的简化例子中,2 W 持续 10 秒消耗 20 J,1 W 持续 30 秒消耗 30 J。后者的瞬时功率更低,完成任务消耗的能量却更多。真实设备需要考虑屏幕、CPU、GPU、无线电和其他系统活动,不能把整机测量都归到一段应用代码。
比较耗电时,应固定业务量与观察时长,并记录亮度、网络、信号、温度和充电条件。短时间内电池百分比没有变化,只能说明这个粗粒度读数没有变化,不能推出任务没有消耗能量。工具给出的相对评分、模型估算和实际电流测量,也不能直接混成同一种单位。
网络节能不只看下载字节数。频繁唤醒无线电、短间隔发送小请求,可能延长高功耗状态;合并可延后的任务可以减少这类成本。但推迟用户正在等待的操作会影响响应,预取过多还会浪费流量与内存,需要一起比较。
7. 网络与端到端耗时
端到端耗时是从用户操作开始到约定结果可用的完整时间。一个“打开详情”操作,可能经历排队、域名解析、连接建立、TLS 握手、服务端处理、响应传输、解析和界面更新。
客户端从发出请求到收到响应的时间,不等于服务端纯处理时间,也不等于用户等待时间。应保留能串起这些阶段的操作标识;服务端耗时需要服务端的对应证据,不能把客户端剩余时间都猜成服务端计算。
这些阶段并不总是各出现一次并且首尾相接。连接复用可以省去新建连接;缓存可能省去网络;重定向和重试可能产生多轮;并发请求与界面渲染还会重叠。OkHttp 的 EventListener 就允许同一调用中的部分连接事件缺失或重复。因此不能把所有阶段耗时机械相加后当成总耗时。
除延迟外,还应记录成功率、取消与超时、请求次数、传输字节及缓存命中条件。压缩传输字节和解压后的内容字节是不同口径;“缓存命中率”也要说明按请求数还是按字节数计算。取消过时搜索请求可以减少浪费,但不能把被取消请求从统计中悄悄删去后宣称成功率提高。
8. 分布与改动前后比较
性能结果应该呈现分布和失败情况。平均值适合概括总量,但无法说明最慢的一部分用户。分位数及失败的处理方式见性能测试。
比较时先把设备、系统、版本、网络和初始状态分组。两次总体均值不同,可能只是高性能设备占比变化。各组的 p95 也不能直接取平均当作总体 p95;合并需要原始样本或可合并的分布数据,并保持一致的指标定义。
样本数也影响结论。仅测 5 次就报告 p99,即使工具能给出一个数字,也无法据此稳定描述真实用户中最慢的 1%。应保留每次结果、重复批次和波动,并按所需判断决定样本规模。测量期间的崩溃、超时和没有完成的启动要单独报告,不能只统计成功且较快的部分。
一次改动应有可以核对的因果链:某段工作占据关键路径,调整后该段时间减少,相同业务的目标指标改善,其他资源与功能没有超出可接受范围。例如增加图片缓存后,页面更快但内存峰值上升,需要明确这项交换,不能只展示启动时间。
9 · 面试题卡片
本页题目与参考回答
9.1 怎样让“页面加载时间为 500 毫秒”成为可验证的结论?
考察重点:测量区间、初始状态、构建环境及统计方式。
相关内容:测量对象与实验条件。
参考回答
先说明从哪一个事件开始,到什么可观察结果结束,以及在哪些条件下得到这个数值。 需要记录设备、系统、构建、数据和缓存状态、网络、重复次数及失败情况,并说明 500 毫秒是单次、均值还是某个分位数。只有条件和口径一致,才适合比较改动前后。
9.2 首帧提前出现,是否就能说明启动体验改善?
考察重点:视觉反馈、业务可用与启动初始状态。
相关内容:启动与业务可用时间。
参考回答
只能说明首次视觉反馈可能提前,还要检查主要内容何时可用。 把工作延后到首帧后可能只改变计时终点之前的工作量。应同时观察首次显示与业务就绪,并分别比较进程不存在、页面重建和页面返回等状态。业务就绪埋点必须对应实际可操作条件。
9.3 平均 FPS 一样,为什么用户仍可能感觉一组更卡?
考察重点:帧间隔分布、刷新率和交互响应。
相关内容:帧耗时与交互延迟。
参考回答
平均值没有说明帧是否均匀出现。 少量长停顿可以被其他较短间隔抵消,因此还要观察慢帧、尾部耗时和具体时间线。60 Hz 与 120 Hz 的刷新间隔分别约为 16.67 ms 与 8.33 ms,截止时间不同。输入到反馈的延迟也应单独观察,不能仅看动画帧率。
9.4 内存分配量减少,是否就说明内存泄漏已经解决?
考察重点:累计分配、当前保留、峰值与统计范围。
相关内容:内存占用与分配。
参考回答
不能。分配量减少与不再保留过期对象是两件事。 应检查重复操作后相关对象是否仍被引用,以及保留量是否持续增长,同时观察峰值。堆、原生分配和进程内存各有统计范围;对象释放后进程占用未立即下降,也不足以单独证明泄漏。
9.5 一个任务的平均功率下降,是否一定更省电?
考察重点:功率、执行时长、总能耗与测量范围。
相关内容:功率与能耗。
参考回答
不一定,还要比较完成相同任务消耗的总能量。 恒定 2 W 执行 10 秒是 20 J,恒定 1 W 执行 30 秒是 30 J。实际比较还需控制屏幕、网络、温度等条件,并区分应用估算与整机测量;短时电池百分比不变不能证明零耗电。
9.6 网络库记录的请求时间能否直接作为页面加载时间?
考察重点:网络调用边界、并发与用户等待的完整路径。
相关内容:网络与端到端耗时。
参考回答
通常不能,两者的起止点不同。 页面还可能经历调用前的排队、多个请求、解析和渲染。连接复用、缓存、重试和并发会改变阶段数量与重叠关系,应保留端到端埋点,再用网络阶段指标定位其中的成本,避免重复相加。
9.7 怎样证明一次性能优化的结果值得采用?
题型:综合题
考察重点:同条件对照、分布、失败与资源交换。
参考回答
在相同业务、设备、构建和初始状态下重复比较,并解释目标指标为什么改善。 保留分布、失败与样本规模,用时间线或资源记录确认预期成本确实减少。还应检查功能、内存、流量和能耗等代价,避免只选择最有利的一项结果;线上观察按相同用户条件分组验证。
10. 参考资料
- Android:App startup time
- Android:Slow rendering
- Android:dumpsys 与内存统计
- Apple:Gathering information about memory use
- Apple:Improving your app's performance
- Flutter:Performance profiling
- React Native:Performance Overview
- Android:Minimize the effect of regular updates
- OkHttp 4.12.0:EventListener
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明