首页 / Android 入门教程 / 性能与内存优化

Android 入门教程

性能与内存优化

本教程共 100 篇 · 第 99 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程性能优化内存泄漏LeakCanaryProfilerStrictMode

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 可能已关闭
        }
    }
}

修正:用 lifecycleScopeviewModelScope,页面销毁时自动取消:

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 内存堆,分析引用链,发通知告诉你泄漏在哪。

Note

LeakCanary 检测到泄漏后会在通知栏弹出提示。点进去能看到完整的引用链,从泄漏对象一路追溯到是谁持有它。

引用链示例:

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()
            )
        }
    }
}
Warning

StrictMode 只在 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 说明内存紧张。

操作流程:

  1. 运行应用,打开 Profiler。
  2. 反复操作页面(打开、关闭、滑动)。
  3. 点 “Capture heap dump” 抓取内存快照。
  4. 按类名排序,看哪个类的实例数异常多。
  5. 找到可疑对象,看它的引用链。
Tip

关闭一个页面后等几秒,手动触发 GC(Profiler 里有按钮),再抓快照。如果页面相关对象还在,说明泄漏了。

CPU 分析

CPU 面板看主线程在忙什么:

  1. 录制一段操作(比如滑动列表)。
  2. 看火焰图(Flame Chart),哪个方法占的时间最多。
  3. 主线程上的耗时方法就是卡顿元凶。

常见问题:JSON 解析放在主线程、图片解码在主线程、数据库查询在主线程。这些都该放到子线程。

网络分析

Network 面板显示每个请求的大小和耗时。如果请求太大,考虑压缩或分页。

过度绘制

过度绘制(Overdraw)是指同一像素被绘制多次。比如背景上叠背景,背景上又叠卡片背景,同一个像素画了三遍,浪费 GPU。

打开方式:开发者选项 > 调试 GPU 过度绘制。屏幕会变成彩色:

颜色含义
原色(无变色)绘制 1 次,理想
蓝色绘制 2 次,可接受
绿色绘制 3 次,注意
粉色绘制 4 次,有问题
红色绘制 5 次以上,严重

优化方法:

  1. 移除多余背景:Window 已经有背景了,根布局不需要再设。android:windowBackground 设为 @null 或透明。
  2. 去掉不必要的背景:卡片背景下面不需要再铺一层。
  3. Compose 自动优化:Compose 默认会裁剪绘制区域,过度绘制比 View 体系少很多。
Note

Compose 在减少过度绘制方面有天然优势。声明式 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 标记列表项

LazyColumnitemskey,列表变化时只重组变化的项:

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 用「稳定性」判断是否需要重组。StringInt 等基本类型是稳定的。data class 默认稳定(如果所有属性都稳定)。List 不稳定(可能是可变列表)。

如果传给 Composable 的参数类型不稳定,Compose 会保守重组。解决方法:

// 用不可变集合
@Immutable
data class NoteListState(
    val notes: List<Note>  // 用 List(不可变)而非 MutableList
)

// 或用 kotlinx.collections.immutable
@Composable
fun NoteList(state: ImmutableList<Note>) { ... }

列表性能优化

RecyclerViewLazyColumn 都有列表性能要点:

  1. DiffUtil:RecyclerView 用 DiffUtil 计算差异,只更新变化的项。
  2. ViewHolder 复用:别在 onBindViewHolder 里做耗时操作。
  3. 图片加载:用 Coil 等库异步加载,自动缓存。
  4. 固定大小:列表高度固定时设 setHasFixedSize(true),跳过布局计算。

Compose 的 LazyColumn 天然支持这些优化,只要加 key 就行。

图片内存优化

Bitmap 是内存大户。一张 4000x3000 的照片解码后占 4000 * 3000 * 4 = 48MB。

优化策略:

  1. 按需解码:不需要原始分辨率,用 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)
  1. 用图片库:Coil/Glide 自动处理采样、缓存、异步加载,不用手动管理。

  2. 及时回收:用完的大图手动 recycle()(一般用图片库就不需要)。

  3. WebP 格式:比 PNG/JPEG 更小,Android 4.0+ 原生支持。

启动优化

应用启动慢,用户第一印象就差。启动分冷启动(进程不存在)和热启动(进程在后台)。

冷启动优化:

  1. 减少 Application.onCreate 耗时:别在 onCreate 里做网络请求、数据库初始化。用懒加载或后台线程。
  2. 用 App Startup:Jetpack 的 App Startup 库统一管理初始化,支持懒加载。
  3. Splash Screen:用 SplashScreen API 显示启动画面,给用户即时反馈。
// 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 }
    }
}

常见坑

  1. LeakCanary 报泄漏但忽略:每条泄漏通知都值得看。今天忽略一个,明天积累十个。
  2. Profiler 只看一次:性能要反复测。每次改完代码跑一遍,对比前后。
  3. 过度优化:不是所有性能问题都需要解决。先用 Profiler 找到瓶颈,再针对性优化。
  4. release 包没测性能:debug 包有 LeakCanary 和日志,性能不代表 release。定期测 release 包。
  5. Compose 不加 key:列表项不加 key,删除/插入时全量重组,滑动卡顿。
Tip

性能优化的原则:先测量,再优化。不要凭感觉猜瓶颈在哪,用 Profiler 用数据说话。80% 的性能问题出在 20% 的代码里。

小结

内存泄漏是对象该回收却被引用持有导致。常见场景:静态变量持有 Activity、内部类持有外部类、未取消协程、未注销接收器、单例持有 Context。LeakCanary 自动检测泄漏,debug 包集成即可。StrictMode 在开发期检测主线程耗时操作和资源泄漏。Profiler 看 CPU/内存/网络的实时使用。过度绘制用开发者选项的 GPU 过度绘制检测。Compose 优化重点是减少不必要重组:用 keyderivedStateOf、稳定类型。性能优化先测量再优化,用数据说话。

下一章讲发布应用。