性能问题的三类表现

用户感知到的性能问题基本只有三种:启动慢、用着卡、越用越费内存(最终被系统杀掉)。它们的技术原因不同,排查路径也不同。

  • 启动慢:Application 里做了太多初始化,或首屏需要等待网络数据。
  • 卡顿:主线程被耗时操作占用,或者界面层级过深导致绘制负担重。
  • 内存增长:泄漏(对象被意外持有)或资源未释放(图片、监听、连接)。
性能优化的第一步永远是复现与量化。没有基线数据,所有“优化完成”都只是主观感觉。

因此本文的顺序是:先讲测量工具与基线建立,再分别讲三类问题的定位与治理,最后给出可持续的流程。

启动优化:从 Application 到首帧

冷启动可分为三个阶段:进程创建与 Application 初始化、Activity 创建与布局加载、首帧绘制。优化目标是让首帧尽早出现,并在首帧之后再做非必要的初始化。

启动优化的四个手段
  • 延迟初始化把非首屏必需的 SDK 初始化挪到首帧之后,用懒加载或启动任务框架调度。
  • 并行初始化互相独立的初始化放到不同线程,缩短总时长。
  • 布局精简减少层级、避免嵌套过深,能显著缩短首帧前的测量与布局时间。
  • 启动窗口用主题背景与占位骨架保持一致,避免白屏带来的“更慢”观感。
性能优化示意:左侧半透明圆形仪表盘与发光指针,右侧面板上绘制先陡升后趋稳的性能曲线
把性能变成可度量的数字:仪表盘给出当前水位,曲线记录改动前后的差异。

一个实用的做法是给初始化加上约束:只有首屏确实需要的才允许在主线程、在首帧之前执行。其余全部改为懒加载,这条规则能解决大部分启动慢的问题。

内存泄漏:来源与检测

安卓的内存泄漏几乎都源于“生命周期更长的对象持有了生命周期更短的对象”,常见来源有四个。

  • 静态变量持有 Activity 或 View 引用。
  • 未注销的监听器与回调(广播接收器、事件总线、传感器、数据库观察者)。
  • 非静态内部类 / 匿名 Handler 隐式持有外部 Activity。
  • 未取消的协程或线程,其闭包持有界面引用。

检测方法很直接:在 Profiler 里抓两次堆快照,执行一段会创建再销毁界面的操作,比较对象数量;如果某个 Activity 或 Fragment 实例持续增长,就说明有泄漏。再按引用链回溯,找出是谁持有它。

预防手段比事后排查更划算:在 onDestroy 里统一注销监听、协程一律绑定生命周期作用域、Handler 使用静态内部类加弱引用、单例不持有 Context(需要时使用 applicationContext)。把这几条写进团队规范,泄漏会少很多。

卡顿分析:主线程的归因方法

卡顿的本质是主线程无法在 16.6 毫秒(60Hz 下)内完成一帧的工作。归因方法分三步。

  1. 确认现象:用掉帧统计或帧率工具,确认是持续掉帧还是偶发卡顿。
  2. 定位时段:用 CPU Profiler 录制卡顿发生的那几秒,看主线程上哪个方法占用了最多时间。
  3. 修正实现:把耗时操作移出主线程,或做缓存、预计算、批量处理。

常见的元凶包括:在主线程做文件或数据库读写、JSON 解析、图片解码、复杂的正则匹配、以及在 onDraw 里创建对象。这些问题在开发机上往往看不出(性能足够好),一到低端机就集中爆发——所以在目标机型上测试非常必要。

网络与图片:容易被忽略的耗时项

很多“卡顿”其实来自网络等待与图片处理,只是表现为界面不响应。

  • 请求合并与并发控制:首屏需要的多个接口并行发起,不要串行等待。
  • 数据预取与本地缓存:列表页滚动时提前加载下一页;重复数据先读本地缓存再后台刷新。
  • 图片按需解码:按控件实际尺寸采样,避免把 4000 像素的图解码成 200 像素的缩略图。
  • 列表图片复用:滚动中取消已离开屏幕的加载任务,避免无效解码与内存占用。
  • 启动阶段的网络请求要延后或做超时降级,绝不能让首屏卡在网络返回上。
首屏不要等网络。先渲染本地缓存或骨架屏,数据回来再刷新——用户感知的速度取决于“多快看到内容”,而不是“多快拿到数据”。

工具链:Profiler 与基准测试

工具用途输出
CPU Profiler查看主线程调用耗时调用火焰图与采样记录
Memory Profiler找内存增长与泄漏堆快照与引用链
Perfetto / 系统跟踪看整机时序(渲染、IO、线程)时间线视图
Macrobenchmark量化启动与滚动性能可比较的数值指标

Profiler 适合“找问题”,Macrobenchmark 适合“验证改善”。两者配合使用:先用 Profiler 定位瓶颈,改动后跑基准测试确认数值真的下降了,而不是凭感觉宣布优化成功。