性能优化:内存泄漏检测、ANR分析、布局优化、协程优化

性能优化这个话题,说实话,很多开发者觉得它「虚」。

代码能跑,功能正常,为什么要优化?

嗯,我以前也这么想。直到有一次,我负责的一个社交App在用户手机后台运行半小时后,直接OOM崩溃了。用户反馈炸了锅,我连夜排查,发现罪魁祸首就是一个没被释放的Activity引用——说白了,就是内存泄漏。

从那以后,我把性能优化列入了每个版本的必修课。今天我们就聊聊四个最核心的方向:内存泄漏、ANR、布局和协程。

核心观点:性能优化不是锦上添花,而是雪中送炭。一个流畅、不崩溃、不卡顿的App,才是用户愿意留下来的基础。

一、内存泄漏检测:LeakCanary 实战

内存泄漏,说白了就是「该回收的对象没被回收」。Java/Kotlin 有 GC,但 GC 只回收「不可达」的对象。如果你的代码里某个对象被一个长生命周期的对象引用着,GC 就永远动不了它。

我个人的习惯是:在开发阶段就集成 LeakCanary,而不是等到线上出问题再查。

1.1 集成方式

// build.gradle.kts (app 模块)
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}

就这么一行。LeakCanary 2.x 不需要手动初始化,它会自动注册。你只需要在 debug 包中运行 App,正常操作,如果有泄漏,通知栏会弹出提示。

1.2 常见泄漏场景

  • 静态变量持有 Activity 引用:比如一个静态的 View 或 Context。
  • 内部类/匿名类:非静态内部类隐式持有外部类引用。Handler、Runnable 最容易中招。
  • 未取消的注册:比如 BroadcastReceiver、EventBus 注册后没解绑。
  • 单例持有 Context:单例里如果传了 Activity 的 Context,Activity 退出后单例还活着,就泄漏了。

我的经验:我曾经在一个 Fragment 里写了一个匿名 Handler,postDelayed 了一个 10 秒的任务。结果用户快速切换页面,Fragment 被销毁了,但 Handler 还在,导致 Fragment 泄漏。后来我改用 Lifecycle 配合协程,彻底解决了这个问题。

1.3 如何分析 LeakCanary 的堆栈

LeakCanary 会给出一个引用链,比如:

┬───
│ GC Root: Local variable in native code
│
├─ android.app.ActivityThread instance
│    ↓ ActivityThread.mActivities
├─ android.util.ArrayMap
│    ↓ ArrayMap.valueAt
├─ com.example.MainActivity instance
│    ↓ MainActivity.mLeakedView
├─ android.widget.TextView
│    ↓ TextView.mContext
╰─ com.example.MainActivity (leaked)

看到这个链,你就知道:MainActivity 被 ActivityThread 的 mActivities 引用着,而 mActivities 里有一个 TextView 的 mContext 指向了 MainActivity。说白了,就是 Activity 没被销毁,或者销毁后还被其他地方引用着。

注意:LeakCanary 只在 debug 包中生效。线上包不要集成,否则会影响性能。如果你需要线上监控,可以考虑接入 Matrix 或自己封装一个轻量版。

二、ANR 分析:从日志到定位

ANR(Application Not Responding)是用户最直观的「卡死」体验。系统会弹出一个对话框,问用户「继续等待还是关闭」。说实话,用户大概率选关闭。

ANR 的触发条件很简单:

  • 主线程阻塞超过 5 秒(输入事件)
  • BroadcastReceiver 未在 10 秒内返回
  • Service 未在 20 秒内执行完

2.1 如何获取 ANR 日志

ANR 发生时,系统会在 /data/anr/traces.txt 中记录堆栈信息。你可以通过 adb 拉取:

adb pull /data/anr/traces.txt

或者,在 Android 8.0 以上,系统会在 /data/anr/ 下生成多个文件,文件名包含时间戳。

2.2 分析 traces.txt 的关键点

  • 找到 main 线程(通常叫 "main" prio=5 tid=1
  • 看它的堆栈,停在哪个方法上
  • 如果是 Thread.sleep()wait(),说明是主动阻塞
  • 如果是 BinderProxy.transact(),说明是跨进程调用卡住了
  • 如果是 Handler.dispatchMessage() 里执行了耗时操作,那就是代码问题

避坑指南:我曾经遇到一个 ANR,堆栈显示停在 View.onDraw() 里。我以为是绘制太复杂,结果发现是 onDraw() 里调用了 BitmapFactory.decodeResource()——这玩意儿在主线程里做 IO,不卡才怪。

2.3 预防 ANR 的手段

  • 主线程不要做任何 IO 操作(网络、文件、数据库)
  • 使用 StrictMode 在开发阶段检测主线程违规
  • 复杂计算放到协程或线程池
  • 使用 AsyncTaskHandlerThread 处理后台任务

三、布局优化:从渲染树到性能

布局优化,说白了就是让 View 树更「扁」、更「轻」。Android 的渲染流程是:Measure → Layout → Draw。每一层嵌套都会增加计算量。

3.1 减少层级

我见过最夸张的布局,一个简单的列表项嵌套了 7 层 LinearLayout。你想想看,每个 item 都要 measure 7 次,滑起来能不卡吗?

优化手段:

  • 使用 ConstraintLayout 替代多层嵌套
  • 使用 Merge 标签合并根布局
  • 使用 ViewStub 延迟加载不常用的布局

3.2 使用 includeViewStub

<!-- 主布局 -->
<LinearLayout ...>
    <include layout="@layout/common_header" />
    <ViewStub
        android:id="@+id/stub_extra"
        android:layout="@layout/extra_panel"
        android:inflatedId="@+id/extra_panel" />
</LinearLayout>

在代码中按需加载:

val stub = findViewById<ViewStub>(R.id.stub_extra)
stub.inflate()  // 只有调用时才加载

3.3 使用 RecyclerView 替代 ListView

这个其实不用多说。RecyclerView 的 ViewHolder 复用机制比 ListView 高效得多。而且它支持动画、多布局、拖拽,功能也更强大。

我的建议:如果你还在用 ListView,赶紧迁移到 RecyclerView。迁移成本不高,但性能提升是立竿见影的。

四、协程优化:从线程到协程的思维转变

Kotlin 协程不是线程,它只是一个「可挂起的计算」。说白了,协程让异步代码写起来像同步代码,而且不会阻塞线程。

4.1 协程的常见陷阱

  • 全局作用域:使用 GlobalScope.launch 会导致协程无法被取消,容易泄漏。
  • 未处理异常:协程中的异常如果不处理,会直接崩溃。
  • 线程切换过多:频繁 withContext(Dispatchers.IO) 反而增加开销。

4.2 正确的协程使用方式

// 在 ViewModel 中使用 viewModelScope
viewModelScope.launch {
    val data = withContext(Dispatchers.IO) {
        repository.fetchData()
    }
    // 回到主线程更新 UI
    updateUI(data)
}

为什么用 viewModelScope?因为它会在 ViewModel 被清除时自动取消所有协程,避免泄漏。

4.3 协程的并发控制

如果你需要并发执行多个任务,可以用 async/await

viewModelScope.launch {
    val result1 = async { fetchData1() }
    val result2 = async { fetchData2() }
    val combined = result1.await() + result2.await()
}

但要注意:async 默认使用 Dispatchers.Default,如果任务中有 IO 操作,记得指定 Dispatchers.IO

注意:不要滥用 async。如果两个任务之间有依赖关系,用 launch 顺序执行反而更清晰。我见过有人把三个串行任务写成三个 async,然后 await 三次——代码又长又难读,性能也没提升。

知识体系总览

下面这张图,是我自己总结的「性能优化四象限」。你可以把它当作一个检查清单:

性能优化四象限 内存泄漏 LeakCanary 检测 静态变量 / 内部类 / 单例 引用链分析 debug 包集成 ANR 分析 traces.txt 日志 主线程阻塞检测 StrictMode 预防 IO 操作移出主线程 布局优化 减少层级 / ConstraintLayout ViewStub 延迟加载 RecyclerView 复用 Merge 标签合并 协程优化 viewModelScope 管理 async/await 并发 异常处理 避免 GlobalScope 性能

这四个方向,每一个都值得深入。但说实话,你不需要一次性全部做完。我的建议是:先从内存泄漏和 ANR 入手,这两个问题对用户体验影响最大。布局优化和协程优化,可以在日常开发中逐步养成习惯。

最后说一句:性能优化不是一次性的工作,而是一种思维习惯。每次写代码前,问自己一句:「这段代码会不会在主线程卡住?会不会导致泄漏?」久而久之,你写出来的代码自然就「抗造」了。


公众号:蓝海资料掘金营,微信deep3321