性能与内存优化
本教程共 100 篇 · 第 99 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
99. 性能与内存优化
本节目标:理解内存泄漏的成因和排查方法,学会用 LeakCanary 自动检测泄漏、用 StrictMode 发现违规操作、用 Profiler 分析性能瓶颈、优化过度绘制和 Compose 重组。
应用用着用着就卡了,或者闪退了。大概率是性能问题—内存泄漏、主线程阻塞、过度绘制。这章讲怎么发现和解决这些问题。
什么是内存泄漏
打个比方,你租了一间仓库放东西。东西用完了该还回去,但钥匙被别人拿着,仓库管理员以为还在用,不收回来。仓库越占越多,最后没地方放了。
内存泄漏就是这回事。对象用完了该被垃圾回收(GC),但有人还持有它的引用,GC 不敢回收,内存越占越多,最后 OOM(Out of Memory)崩溃。
Android 上每个应用有内存上限(通常 192MB-512MB,取决于设备)。超了就被系统杀掉。
常见的内存泄漏场景
1. 静态变量持有 Activity
// 错误写法
companion object {
private var activity: MainActivity? = null // Activity 被 static 持有
}
Activity 本该随页面关闭被回收,但 static 变量的生命周期和应用一样长。Activity 被持有,它持有的所有 View、Bitmap 都回收不了。
修正:别把 Activity 存到 static 变量里。非要存,用弱引用(WeakReference)。
2. 内部类持有外部类
Kotlin 里匿名内部类和内部类默认持有外部类引用:
class MainActivity : AppCompatActivity() {
// 错误:非静态内部类持有 Activity 引用
inner class MyRunnable : Runnable {
override fun run() {
// 如果这里耗时很长,Activity 关了还在跑
// Activity 泄漏
}
}
fun startTask() {
Thread(MyRunnable()).start()
}
}
修正:内部类改成静态(companion object 或顶层类),或者用弱引用持外部类:
class MainActivity : AppCompatActivity() {
// 正确:静态内部类不持有外部引用
class MyRunnable(weakActivity: WeakReference<MainActivity>) : Runnable {
private val activity = weakActivity
override fun run() {
activity.get()?.let {
// 安全使用
}
}
}
}
3. 未取消的协程
class MainActivity : AppCompatActivity() {
fun loadData() {
// 错误:GlobalScope 协程生命周期和应用一样长
GlobalScope.launch {
val data = fetchData() // 网络请求
findViewById<TextView>(R.id.tvTitle).text = data // Activity 可能已关闭
}
}
}
修正:用 lifecycleScope 或 viewModelScope,页面销毁时自动取消:
lifecycleScope.launch {
val data = fetchData()
if (!isActive) return@launch
findViewById<TextView>(R.id.tvTitle).text = data
}
4. 未注销的广播接收器
class MainActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// ...
}
}
override fun onStart() {
super.onStart()
registerReceiver(receiver, IntentFilter("CUSTOM_ACTION"))
}
// 错误:忘了在 onStop 里 unregisterReceiver
}
修正:注册了就要注销:
override fun onStop() {
super.onStop()
unregisterReceiver(receiver)
}
5. 单例持有 Context
// 错误:单例持有 Activity Context
object MyManager {
private var context: Context? = null
fun init(context: Context) {
this.context = context // 如果传的是 Activity Context,泄漏
}
}
修正:用 Application Context,它的生命周期和应用一样长:
object MyManager {
private var context: Context? = null
fun init(context: Context) {
this.context = context.applicationContext // 安全
}
}
LeakCanary 检测泄漏
LeakCanary 是 Square 出的内存泄漏检测库,零配置自动检测,debug 包集成即可。
加依赖:
// build.gradle.kts
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
debugImplementation 表示只在 debug 包生效,不影响 release。
装上之后不用写任何代码。应用运行时,LeakCanary 自动监控 Activity、Fragment、ViewModel、View 的销毁。如果对象销毁后还没被回收,它会 dump 内存堆,分析引用链,发通知告诉你泄漏在哪。
NoteLeakCanary 检测到泄漏后会在通知栏弹出提示。点进去能看到完整的引用链,从泄漏对象一路追溯到是谁持有它。
引用链示例:
MainActivity 泄漏
├── MyRunnable 持有 MainActivity 引用
│ └── Thread 仍在运行
│ └── GC Root: 正在运行的线程
这条链告诉你:有个线程还在跑,它通过 Runnable 持有 Activity。解决方法就是取消线程或用静态内部类。
StrictMode 开发期监控
StrictMode 是 Android 框架自带的开发工具,能在主线程做耗时操作时给你警告。
在 Application 里开启:
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测主线程读磁盘
.detectDiskWrites() // 检测主线程写磁盘
.detectNetwork() // 检测主线程网络请求
.penaltyLog() // 日志输出
.penaltyDeath() // 直接崩溃(可选)
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectActivityLeaks() // 检测 Activity 泄漏
.detectLeakedClosableObjects() // 检测未关闭的 Closeable
.detectLeakedSqlLiteObjects() // 检测未关闭的 SQLite 对象
.penaltyLog()
.build()
)
}
}
}
WarningStrictMode 只在 debug 包开。release 包开了会拖性能。用
BuildConfig.DEBUG控制。
Android Studio Profiler
Profiler 是 Android Studio 内置的性能分析工具,能实时看 CPU、内存、网络、电池的使用情况。
打开方式:View > Tool Windows > Profiler,选择运行的设备和应用。
内存分析
Profiler 的 Memory 面板显示内存使用曲线。重点看:
- Java heap:Kotlin/Java 对象占的内存。
- Native heap:C/C++ 分配的内存。
- Graphics:图片相关的内存。
- GC 事件:垃圾回收发生的频率。频繁 GC 说明内存紧张。
操作流程:
- 运行应用,打开 Profiler。
- 反复操作页面(打开、关闭、滑动)。
- 点 “Capture heap dump” 抓取内存快照。
- 按类名排序,看哪个类的实例数异常多。
- 找到可疑对象,看它的引用链。
Tip关闭一个页面后等几秒,手动触发 GC(Profiler 里有按钮),再抓快照。如果页面相关对象还在,说明泄漏了。
CPU 分析
CPU 面板看主线程在忙什么:
- 录制一段操作(比如滑动列表)。
- 看火焰图(Flame Chart),哪个方法占的时间最多。
- 主线程上的耗时方法就是卡顿元凶。
常见问题:JSON 解析放在主线程、图片解码在主线程、数据库查询在主线程。这些都该放到子线程。
网络分析
Network 面板显示每个请求的大小和耗时。如果请求太大,考虑压缩或分页。
过度绘制
过度绘制(Overdraw)是指同一像素被绘制多次。比如背景上叠背景,背景上又叠卡片背景,同一个像素画了三遍,浪费 GPU。
打开方式:开发者选项 > 调试 GPU 过度绘制。屏幕会变成彩色:
| 颜色 | 含义 |
|---|---|
| 原色(无变色) | 绘制 1 次,理想 |
| 蓝色 | 绘制 2 次,可接受 |
| 绿色 | 绘制 3 次,注意 |
| 粉色 | 绘制 4 次,有问题 |
| 红色 | 绘制 5 次以上,严重 |
优化方法:
- 移除多余背景:Window 已经有背景了,根布局不需要再设。
android:windowBackground设为@null或透明。 - 去掉不必要的背景:卡片背景下面不需要再铺一层。
- Compose 自动优化:Compose 默认会裁剪绘制区域,过度绘制比 View 体系少很多。
NoteCompose 在减少过度绘制方面有天然优势。声明式 UI 框架知道哪些区域需要重绘,不会像 View 那样全量刷新。
Compose 性能优化
避免不必要重组
Compose 每次状态变化都会重组(Recomposition)。重组是性能开销的大头,不必要的重组要避免。
// 错误:每次重组都创建新的 lambda
@Composable
fun BadExample(items: List<String>) {
Column {
items.forEach { item ->
Text(
text = item,
modifier = Modifier.clickable {
// 每次重组都创建新 lambda
handleClick(item)
}
)
}
}
}
// 正确:用 remember 稳定 lambda
@Composable
fun GoodExample(items: List<String>, onItemClick: (String) -> Unit) {
Column {
items.forEach { item ->
Text(
text = item,
modifier = Modifier.clickable { onItemClick(item) }
)
}
}
}
用 key 标记列表项
LazyColumn 的 items 加 key,列表变化时只重组变化的项:
LazyColumn {
items(
items = notes,
key = { it.id } // 用唯一 ID 做 key
) { note ->
NoteItem(note)
}
}
不加 key 的话,列表项顺序变了会全量重组。加了 key 只重组真正变化的项。
用 derivedStateOf 减少重组
当状态派生计算频繁但结果变化少时,用 derivedStateOf:
@Composable
fun ScrollExample(listState: LazyListState) {
// 错误:每次滚动都重组
val showButton = listState.firstVisibleItemIndex > 0
// 正确:只在条件真正变化时重组
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
if (showButton) {
FloatingActionButton(onClick = { /* 滚到顶部 */ }) {
Icon(Icons.Default.ArrowUpward, contentDescription = "顶部")
}
}
}
稳定类型
Compose 用「稳定性」判断是否需要重组。String、Int 等基本类型是稳定的。data class 默认稳定(如果所有属性都稳定)。List 不稳定(可能是可变列表)。
如果传给 Composable 的参数类型不稳定,Compose 会保守重组。解决方法:
// 用不可变集合
@Immutable
data class NoteListState(
val notes: List<Note> // 用 List(不可变)而非 MutableList
)
// 或用 kotlinx.collections.immutable
@Composable
fun NoteList(state: ImmutableList<Note>) { ... }
列表性能优化
RecyclerView 和 LazyColumn 都有列表性能要点:
- DiffUtil:RecyclerView 用
DiffUtil计算差异,只更新变化的项。 - ViewHolder 复用:别在
onBindViewHolder里做耗时操作。 - 图片加载:用 Coil 等库异步加载,自动缓存。
- 固定大小:列表高度固定时设
setHasFixedSize(true),跳过布局计算。
Compose 的 LazyColumn 天然支持这些优化,只要加 key 就行。
图片内存优化
Bitmap 是内存大户。一张 4000x3000 的照片解码后占 4000 * 3000 * 4 = 48MB。
优化策略:
- 按需解码:不需要原始分辨率,用
inSampleSize缩小。
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true // 先只读尺寸
}
BitmapFactory.decodeFile(path, options)
// 计算缩放比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false // 真正解码
BitmapFactory.decodeFile(path, options)
-
用图片库:Coil/Glide 自动处理采样、缓存、异步加载,不用手动管理。
-
及时回收:用完的大图手动
recycle()(一般用图片库就不需要)。 -
WebP 格式:比 PNG/JPEG 更小,Android 4.0+ 原生支持。
启动优化
应用启动慢,用户第一印象就差。启动分冷启动(进程不存在)和热启动(进程在后台)。
冷启动优化:
- 减少 Application.onCreate 耗时:别在
onCreate里做网络请求、数据库初始化。用懒加载或后台线程。 - 用 App Startup:Jetpack 的 App Startup 库统一管理初始化,支持懒加载。
- Splash Screen:用
SplashScreenAPI 显示启动画面,给用户即时反馈。
// build.gradle.kts
implementation("androidx.core:core-splashscreen:1.0.1")
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splash = installSplashScreen()
super.onCreate(savedInstanceState)
// 可以设置条件,条件满足前一直显示闪屏
splash.setKeepOnScreenCondition { viewModel.isLoading.value }
}
}
常见坑
- LeakCanary 报泄漏但忽略:每条泄漏通知都值得看。今天忽略一个,明天积累十个。
- Profiler 只看一次:性能要反复测。每次改完代码跑一遍,对比前后。
- 过度优化:不是所有性能问题都需要解决。先用 Profiler 找到瓶颈,再针对性优化。
- release 包没测性能:debug 包有 LeakCanary 和日志,性能不代表 release。定期测 release 包。
- Compose 不加 key:列表项不加
key,删除/插入时全量重组,滑动卡顿。
Tip性能优化的原则:先测量,再优化。不要凭感觉猜瓶颈在哪,用 Profiler 用数据说话。80% 的性能问题出在 20% 的代码里。
小结
内存泄漏是对象该回收却被引用持有导致。常见场景:静态变量持有 Activity、内部类持有外部类、未取消协程、未注销接收器、单例持有 Context。LeakCanary 自动检测泄漏,debug 包集成即可。StrictMode 在开发期检测主线程耗时操作和资源泄漏。Profiler 看 CPU/内存/网络的实时使用。过度绘制用开发者选项的 GPU 过度绘制检测。Compose 优化重点是减少不必要重组:用 key、derivedStateOf、稳定类型。性能优化先测量再优化,用数据说话。
下一章讲发布应用。