Android 性能优化全景图 —— 从入门到不踩坑
写在前面:性能优化不是玄学,是一门可以量化的手艺。本文适合初中高级开发者,从发现问题到解决问题,工具到手,干货管够。
一、为什么要性能优化?
用户说: "这App怎么这么卡?"、"手机烫得能煎蛋"、"刚打开就崩了"
老板说: "竞品比我们快,用户都跑了"
面试官说: "讲讲你做过的性能优化"
你说: "我...我代码写得挺规范的啊?"
😄 性能优化,是区分"能写代码"和"写好代码"的分水岭。下面我们进入正题。
二、发现问题篇 —— 性能不好,你得先知道
2.1 用户反馈的信号
| 现象 | 可能原因 | 关键词 |
|---|---|---|
| 卡顿、掉帧 | UI线程阻塞、过度绘制 | Choreographer、16ms |
| 启动慢 | 主线程初始化太多 | 冷启动、懒加载 |
| 手机发热、耗电快 | 后台任务过多、频繁网络请求 | Doze、JobScheduler |
| 闪退、崩溃 | OOM、内存泄漏 | Memory Profiler、LeakCanary |
| 包体积大 | 资源冗余、未压缩 | ProGuard、资源压缩 |
2.2 监控手段
线上监控:
- Crashlytics / Bugly —— 崩溃收集,必备
- ANR监控 —— 捕获主线程卡死
- 自定义性能埋点 —— 关键页面加载时间、接口耗时
线下调试:
- Android Studio Profiler —— 官方全家桶
- LeakCanary —— 内存泄漏自动检测
- Perfetto —— 系统级性能分析
三、解决问题篇 —— 核心干货来了 🔥
3.1 内存优化
3.1.1 概念辨析
| 术语 | 解释 | 类比 |
|---|---|---|
| 内存泄漏 | 对象不再使用,但无法被回收 | 没关的水龙头,慢慢漏 |
| 内存溢出(OOM) | 申请内存超过可用空间 | 水桶满了还往里倒 |
| 内存抖动 | 频繁创建/销毁对象,GC压力山大 | 心电图一样的内存曲线 |
关系: 内存泄漏 → 可用内存减少 → 最终可能OOM 💥
3.1.2 Memory Profiler 实战
打开方式: Android Studio → View → Tool Windows → Profiler
核心功能:
| 功能 | 用途 | 怎么看 |
|---|---|---|
| Memory Chart | 实时内存曲线 | 看锯齿状是否频繁(内存抖动) |
| Heap Dump | 堆内存快照 | 分析对象数量、大小 |
| Allocation Tracking | 对象分配追踪 | 找到谁在疯狂new对象 |
实战步骤:
1. 启动App,打开Memory Profiler
2. 操作你怀疑有问题的功能模块
3. 点击"触发GC"(垃圾桶图标)
4. 观察:内存是否持续上涨不回落?
5. 如果是 → Dump heap,搜索可疑对象
6. 分析引用链:谁持有它不放?
3.1.3 LeakCanary 自动检测
集成方式:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}
使用方式: 集成后自动生效,检测到泄漏会发通知!
通知示例:
LeakCanary
├─ Activity泄漏: MainActivity
├─ 泄漏大小: 1.2 MB
└─ 引用链: MainActivity → Handler → Runnable → ...
一句话: LeakCanary 是你的内存保镖,有问题它第一时间告诉你。
3.1.4 常见泄漏场景 & 修复
| 场景 | 问题原因 | 修复方案 |
|---|---|---|
| Handler内存泄漏 | 非静态内部类持有Activity引用 | 改为静态内部类 + WeakReference |
| 单例持有Context | 单例生命周期 > Activity | 使用Application Context |
| 注册未取消 | EventBus/广播未unregister | onDestroy中取消注册 |
| 匿名内部类 | Runnable/Callback持有外部类引用 | 改为静态类或弱引用 |
| 静态View/Activity | 静态变量一直持有 | 避免静态持有,及时置空 |
Handler泄漏修复示例:
// ❌ 错误写法
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 隐式持有外部Activity引用
textView.setText("更新UI");
}
};
// ✅ 正确写法
private static class SafeHandler extends Handler {
private final WeakReference<MainActivity> activityRef;
SafeHandler(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity != null && !activity.isFinishing()) {
activity.textView.setText("更新UI");
}
}
}
3.2 启动速度优化
3.2.1 启动类型
| 类型 | 定义 | 用户体验 |
|---|---|---|
| 冷启动 | App进程不存在,从零开始 | 最慢,白屏时间最长 |
| 温启动 | 进程存在,Activity重建 | 中等速度 |
| 热启动 | 进程存在,Activity还在后台 | 最快,瞬间恢复 |
优化重点: 冷启动,因为这是用户的第一印象。
3.2.2 优化策略
1. 延迟初始化(懒加载)
// ❌ 一股脑全在Application初始化
@Override
public void onCreate() {
super.onCreate();
initSDK1();
initSDK2();
initSDK3();
initSDK4();
initSDK5();
// ... 用户已经等得不耐烦了
}
// ✅ 按需初始化
@Override
public void onCreate() {
super.onCreate();
// 只初始化必须的
initMustSDK();
// 其他的延迟到使用时再初始化
// 或者放到子线程
new Thread(() -> {
initLazySDK();
}).start();
}
2. 异步初始化 + 任务调度
使用 AppStartup 或自定义启动器,并行初始化多个SDK。
主线程: ────[核心任务]────[UI渲染]
↓
子线程1: ──[SDK1]──[SDK2]────
子线程2: ──[SDK3]──────────
3. 优化Splash页面
- 避免Splash页做耗时操作
- 使用
windowBackground提前展示主题色 - 尽快跳转到主页面
3.2.3 启动时间测量
方式1:代码打点
// 在Application.onCreate开始
long startTime = System.currentTimeMillis();
// 在主页面onWindowFocusChanged
long endTime = System.currentTimeMillis();
Log.d("Startup", "启动耗时: " + (endTime - startTime) + "ms");
方式2:ADB 命令
adb shell am start -W 包名/主Activity
# 输出: ThisTime, TotalTime, WaitTime
方式3:标记完全绘制(API 19+)
// 在主页面数据加载完成后调用
ActivityCompat.reportFullyDrawn(this);
// 可在 Logcat 过滤 "FullyDrawn" 查看耗时
3.3 UI流畅度优化
3.3.1 16ms 定律
原理: 屏幕刷新率60fps → 每帧必须在 16.67ms 内完成绘制
超时后果: 掉帧 → 用户感觉卡顿
理想情况:
Frame1(16ms) → Frame2(16ms) → Frame3(16ms) → ...
卡顿情况:
Frame1(16ms) → Frame2(35ms!!!) → Frame3...
↓
掉帧了!用户看到了卡顿
3.3.2 常见卡顿原因
| 原因 | 表现 | 解决方案 |
|---|---|---|
| 主线程耗时操作 | IO、网络、复杂计算 | 移到子线程 |
| 布局层级过深 | 测量/布局耗时长 | 扁平化布局、ConstraintLayout |
| 过度绘制 | 同一像素多次绘制 | 减少背景、使用Hierarchy Viewer |
| 频繁requestLayout | 触发重新测量布局 | 批量更新、避免频繁调用 |
3.3.3 优化技巧
1. 布局优化
<!-- ❌ 嵌套太深 -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- ✅ 扁平化布局 -->
<ConstraintLayout>
<TextView />
</ConstraintLayout>
2. 减少过度绘制
- 开发者选项 → 调试GPU过度绘制
- 颜色越深问题越严重:无色 < 蓝 < 绿 < 粉红 < 红色
优化方法:
- 移除不必要的
background - 使用
clipToPadding、clipChildren - 自定义View时使用
canvas.clipRect()
3. 使用 Perfetto 分析(推荐)
方式1:Perfetto Web UI(推荐)
- 访问 ui.perfetto.dev
- 手机连接电脑,在开发者选项中开启"系统跟踪"
- 录制trace后上传分析
方式2:命令行抓取
# 使用 adb 抓取
adb shell perfetto \
--out /data/misc/perfetto-traces/trace \
--time 5s \
gfx view res
# 拉取到本地
adb pull /data/misc/perfetto-traces/trace
打开 HTML 或上传到 Perfetto UI,看哪些帧超时了(红色标记)。
3.4 电量优化
3.4.1 耗电大户
| 元凶 | 场景 | 建议 |
|---|---|---|
| 频繁网络请求 | 轮询、心跳包 | 批量请求、增量同步 |
| GPS定位 | 持续定位 | 按需定位、降低精度 |
| 蓝牙扫描 | 持续扫描附近设备 | 按需扫描,设置超时 |
| WakeLock | 阻止休眠 | 及时release |
| 后台服务 | 常驻后台 | 使用JobScheduler |
3.4.2 优化方案
1. 使用 JobScheduler / WorkManager
// 系统会智能调度,批量执行后台任务
WorkManager workManager = WorkManager.getInstance(context);
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(
MyWorker.class,
15, // 重复间隔
TimeUnit.MINUTES
).build();
workManager.enqueue(workRequest);
2. 网络优化
- 批量上传数据,减少请求次数
- 使用 GZIP 压缩
- 预加载 + 缓存
3. 监控电量消耗
adb shell dumpsys batterystats > battery.txt
# 分析各App耗电情况
3.5 包体积瘦身
3.5.1 瘦身策略
| 方案 | 效果 | 实施难度 |
|---|---|---|
| ProGuard/R8 混淆 | 去除无用代码、混淆命名 | ⭐ |
| 资源压缩 | 移除无用资源 | ⭐ |
| WebP格式图片 | 图片体积减少50%+ | ⭐⭐ |
| 动态下发 | 非必须资源动态下载 | ⭐⭐⭐ |
| So动态加载 | 按需加载native库 | ⭐⭐⭐⭐ |
3.5.2 配置示例
android {
buildTypes {
release {
// 开启混淆
minifyEnabled true
// 开启资源压缩
shrinkResources true
// 开启R8
useProguard false
}
}
}
3.5.3 检查包体积
# 分析APK结构
./gradlew assembleRelease
# Android Studio → Build → Analyze APK
查看各模块占比:代码 / 资源 / So库,针对性优化。
3.6 网络优化
3.6.1 优化策略
| 策略 | 说明 | 收益 |
|---|---|---|
| 缓存 | OkHttp缓存、数据库缓存 | 减少重复请求 |
| 请求合并 | 多个接口合并成一个 | 减少RTT |
| 压缩 | GZIP / Protobuf | 减少传输量 |
| 预加载 | 提前请求可能用到的数据 | 用户体验提升 |
| 弱网适配 | 超时重试、离线模式 | 网络差时也能用 |
3.6.2 OkHttp 配置示例
OkHttpClient client = new OkHttpClient.Builder()
// 连接超时
.connectTimeout(15, TimeUnit.SECONDS)
// 读取超时
.readTimeout(20, TimeUnit.SECONDS)
// 缓存
.cache(new Cache(cacheDir, 10 * 1024 * 1024)) // 10MB缓存
// GZIP(默认开启)
.build();
四、工具箱速查表 📦
| 工具 | 用途 | 使用场景 |
|---|---|---|
| CPU Profiler | 方法耗时分析 | 找热点方法、优化性能瓶颈 |
| Memory Profiler | 内存分析 | 内存泄漏、内存抖动检测 |
| Heap Dump | 堆内存快照 | 分析对象分布、GC Root |
| LeakCanary | 自动泄漏检测 | 开发阶段自动发现泄漏 |
| Perfetto | 系统级性能分析 | UI卡顿、线程调度分析 |
| Layout Inspector | 布局层级查看 | 优化布局深度 |
| GPU Overdraw | 过度绘制检测 | 减少不必要的绘制 |
| APK Analyzer | 包体积分析 | 瘦身优化 |
五、面试高频题速记 🎯
Q1: 什么是内存泄漏?如何检测和避免?
内存泄漏:对象不再使用但无法被回收。 检测:Memory Profiler + LeakCanary 避免:注意Handler、单例、注册/反注册、静态变量等场景,使用弱引用。
Q2: 如何优化App启动速度?
- 延迟/懒加载非必须初始化
- 异步初始化 + 任务调度(AppStartup)
- 优化Splash页面
- 避免主线程IO/网络操作
Q3: UI卡顿怎么排查?
- 16ms定律,确认是否掉帧
- Perfetto分析哪一帧超时
- 检查主线程是否有耗时操作
- 检查布局层级、过度绘制
- 使用Choreographer监控帧率
Q4: 如何进行电量优化?
- 减少频繁网络请求,使用批量+缓存
- 避免长时间WakeLock
- 使用JobScheduler/WorkManager调度后台任务
- 按需使用定位,降低精度
Q5: 包体积瘦身有哪些方法?
- ProGuard/R8混淆
- shrinkResources资源压缩
- WebP图片
- 动态下发资源
- So动态加载
六、总结
性能优化是一门"度量"的艺术:
- 先发现问题 —— 监控 + 工具
- 定位原因 —— 分析 + 排查
- 制定方案 —— 策略 + 优先级
- 验证效果 —— 数据对比
- 持续迭代 —— 性能无止境
记住:没有度量的优化都是耍流氓 🔧
🐾 作者:不倒翁
📅 更新时间:2026-03-26
如果这篇文章对你有帮助,点个赞呗~ 你的支持是我持续输出的动力!
参考资料: