性能优化:启动优化、布局优化、内存泄漏检测与LeakCanary

性能优化这个话题,说实话,是区分「能用」和「好用」的分水岭。我见过太多App功能做得很全,但一打开就卡顿、滑动掉帧、用着用着就OOM崩溃。用户不会给你第二次机会——你的App慢,他就卸载。

今天这一章,我会从三个最核心的维度入手:启动速度布局渲染内存泄漏。这三个问题,几乎每个Android项目都会遇到。我个人习惯把性能优化看作「体检」——不是等出问题了再修,而是日常就要监控。

核心观点:性能优化不是一次性的「大扫除」,而是持续性的「日常保洁」。启动优化提升第一印象,布局优化保证流畅体验,内存泄漏检测防止慢性死亡。

Android 性能优化三大支柱 🚀 启动优化 • Application 初始化 • 异步加载与懒加载 • 启动耗时打点 • 主题闪屏优化 • 类预加载 • 启动器任务链 目标:冷启动 < 1.5s 🎨 布局优化 • 层级扁平化 • ConstraintLayout • ViewStub 延迟加载 • 过度绘制检测 • RecyclerView 复用 • 异步布局 目标:渲染 < 16ms/帧 💣 内存泄漏 • 静态变量引用 • 匿名内部类 • Handler 未移除 • 注册未反注册 • 单例持有Context • LeakCanary 接入 目标:零泄漏上架 相互影响 相互影响

一、启动优化:用户的第一印象

启动速度有多重要?有数据说,启动时间每多1秒,用户流失率就增加7%。我接手过一个项目,冷启动要4秒多——用户点开App,先看3秒白屏,然后才看到闪屏。这种体验,说白了就是在赶用户走。

1.1 启动流程与耗时分析

Android应用的启动分为三个阶段:

  • 冷启动:进程从零开始创建,Application.onCreate() 执行,然后创建 MainActivity。这是最慢的,也是优化的主战场。
  • 温启动:进程还在,但Activity被销毁了。需要重新创建Activity。
  • 热启动:进程和Activity都在,直接恢复。基本不需要优化。

我个人习惯用 adb shell am start -W 来打点测量启动时间。它会输出三个关键指标:

指标 含义 优化方向
ThisTime 最后一个Activity启动耗时 Activity布局与初始化
TotalTime 所有Activity启动总耗时 Application + 启动链
WaitTime 包含系统调度的时间 系统层面,一般不动

我的经验:别只看TotalTime。我遇到过TotalTime很短但用户感觉慢的情况——因为闪屏结束后才去加载数据,用户看到的是空页面。所以「用户感知的启动时间」比「技术指标」更重要。

1.2 启动优化的具体手段

启动优化说白了就是两件事:少干活晚点干

第一,Application 初始化要精简。

很多团队喜欢在 Application.onCreate() 里一股脑初始化所有SDK:

// ❌ 错误示范:所有初始化都在主线程
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        initCrashReport()      // 50ms
        initImageLoader()      // 80ms
        initNetworkSDK()       // 60ms
        initPushService()      // 40ms
        initDatabase()         // 120ms
        initAnalytics()        // 30ms
        // 总共 380ms,用户干等
    }
}

正确的做法是:必须同步的才同步,能异步的全异步

// ✅ 推荐做法:按优先级分层初始化
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 第一层:必须同步(主线程)
        initCrashReport()      // 崩溃收集必须最早
        
        // 第二层:可以异步(协程)
        scope.launch(Dispatchers.IO) {
            initDatabase()
            initNetworkSDK()
        }
        
        // 第三层:懒加载(用到才初始化)
        // ImageLoader、PushService 等放到使用时再初始化
    }
}

第二,使用启动器(Startup)库。

Google 官方提供了 App Startup 库,可以帮你管理初始化依赖链。我建议所有新项目都用它——比手动写线程管理靠谱多了。

// 定义初始化任务
class Initializer : Initializer<Unit> {
    override fun create(context: Context) {
        // 执行初始化
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 返回依赖的其他 Initializer
        return listOf(DatabaseInitializer::class.java)
    }
}

第三,主题闪屏优化。

白屏问题怎么解决?很简单——在主题里设置一个背景图,让用户一打开就看到内容,而不是白屏。

<!-- 在 themes.xml 中设置启动主题 -->
<style name="Theme.Splash" parent="Theme.AppCompat.NoActionBar">
    <item name="android:windowBackground">@drawable/splash_background</item>
</style>

<!-- 在 AndroidManifest.xml 中给 MainActivity 设置 -->
<activity
    android:name=".MainActivity"
    android:theme="@style/Theme.Splash">
    ...
</activity>

然后在 Activity 的 onCreate() 里切换回正常主题。这样用户一打开App就看到闪屏背景,而不是白屏。

注意:我曾经犯过一个错误——在闪屏主题里放了一张很大的背景图,结果图片解码本身就要100ms,反而拖慢了启动。闪屏背景图要小,最好用VectorDrawable或者纯色。

二、布局优化:流畅的每一帧

布局优化,说白了就是让UI渲染在16ms内完成。超过这个时间,用户就会感觉到卡顿。你想想看,如果滑动列表时一卡一卡的,用户会怎么想?

2.1 层级扁平化

布局层级越深,测量和绘制的时间就越长。我见过一个页面嵌套了7层LinearLayout——每个LinearLayout都要做两次measure,加起来就是2^7次测量。这能不卡吗?

优化的核心思路:能用ConstraintLayout就用ConstraintLayout。它一次遍历就能完成所有子View的测量,比多层嵌套快得多。

<!-- ❌ 多层嵌套 -->
<LinearLayout>
    <LinearLayout>
        <TextView />
        <TextView />
    </LinearLayout>
    <LinearLayout>
        <ImageView />
        <Button />
    </LinearLayout>
</LinearLayout>

<!-- ✅ 扁平化 -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <TextView app:layout_constraintTop_toTopOf="parent" />
    <TextView app:layout_constraintTop_toBottomOf="@id/tv1" />
    <ImageView app:layout_constraintTop_toTopOf="parent" />
    <Button app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

2.2 延迟加载与ViewStub

有些布局不是一开始就需要显示的,比如「用户协议弹窗」、「设置页面的高级选项」。这些可以用 ViewStub 延迟加载。

<ViewStub
    android:id="@+id/stub_agreement"
    android:layout="@layout/layout_agreement"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

// 在需要时再 inflate
val stub = findViewById<ViewStub>(R.id.stub_agreement)
stub.inflate()  // 只有这时才加载布局

ViewStub 的好处是:它本身不占布局空间,inflate 之后会被替换成实际布局。说白了就是「按需加载」。

2.3 过度绘制检测

过度绘制(Overdraw)是指同一像素被绘制了多次。比如一个红色背景的View上面又盖了一个白色背景的View——下面的红色就被浪费了。

检测方法很简单:打开开发者选项 → 调试GPU过度绘制 → 显示过度绘制区域。颜色越红,问题越严重。

常见的优化手段:

  • 去掉不必要的背景(比如RecyclerView的item背景如果被覆盖了,就去掉)
  • 使用 windowBackground 代替布局根背景
  • 自定义View时用 clipRect() 裁剪不需要绘制的区域

我的习惯:每次提交代码前,我都会打开过度绘制检测跑一遍主要页面。如果看到红色区域,就说明有优化空间。这个习惯帮我发现过很多「看起来没问题但实际有性能隐患」的布局。

三、内存泄漏检测与LeakCanary

内存泄漏是Android开发的「慢性病」。它不会立刻让App崩溃,但会让App越来越卡,最终OOM。我见过一个App运行20分钟后内存占用从80MB涨到500MB——这就是典型的泄漏。

3.1 常见的内存泄漏场景

说白了,内存泄漏就是「本该被回收的对象,因为被某个还活着的对象引用着,导致GC无法回收」。最常见的几种:

场景 原因 解决方案
静态变量持有Activity static Context = activity 改用ApplicationContext
匿名内部类 内部类隐式持有外部类引用 改用静态内部类 + WeakReference
Handler未移除 Handler持有Activity,Message持有Handler onDestroy时 removeCallbacks
注册未反注册 BroadcastReceiver、EventBus等 onDestroy时 unregister
单例持有Context 单例生命周期 = 应用进程 只持有ApplicationContext

3.2 LeakCanary 接入与使用

LeakCanary 是 Square 开源的自动内存泄漏检测工具。它会在检测到泄漏时自动生成堆转储(heap dump)并分析出泄漏路径。我个人觉得它是Android开发者的「必备工具」,没有之一。

接入非常简单:

// 在 build.gradle 中添加依赖
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}

// 不需要任何初始化代码!LeakCanary 2.x 会自动注册
// 它会自动检测 Activity、Fragment、ViewModel、Service 等的泄漏

当检测到泄漏时,LeakCanary 会发送一个通知。点击通知可以看到详细的泄漏分析:

// LeakCanary 输出的泄漏链示例
┬───
│ GC Root: 局部变量
│
├─ com.example.MainActivity instance
│    Leaking: YES (Activity#mDestroyed 为 true)
│    ↓ MainActivity.mHandler
│         ~~~~~~~~
├─ android.os.Handler instance
│    Leaking: YES (Handler 持有 Activity)
│    ↓ Handler.mCallback
│         ~~~~~~~~~
└─ com.example.MyCallback (匿名内部类)
     Leaking: YES (回调未移除)

这个泄漏链告诉你:MainActivity 已经销毁了,但它的 Handler 还在,因为 Handler 里有一个匿名内部类 MyCallback 没有被移除。修复方法就是在 onDestroy()handler.removeCallbacksAndMessages(null)

避坑指南:我曾经在线上版本忘记切回 no-op 依赖,结果LeakCanary在用户手机上弹通知,用户还以为App中毒了。记住:debugImplementation 和 releaseImplementation 一定要分开。线上版本用 no-op,它什么都不做。

3.3 手动检测与MAT分析

LeakCanary 能解决90%的问题,但有些复杂的泄漏还是需要手动分析。比如第三方SDK的泄漏、或者某些系统服务的泄漏。

手动分析的步骤:

  1. adb shell dumpsys meminfo 包名 查看内存概况
  2. 用 Android Studio 的 Profiler 抓取内存快照
  3. 导出 .hprof 文件,用 MAT(Memory Analyzer Tool)分析
  4. 在 MAT 中查看 Suspects(嫌疑泄漏对象)和 GC Roots(根引用路径)

说实话,手动分析比较费时间。我一般只在LeakCanary分析不出来的时候才用MAT。日常开发中,LeakCanary + 代码审查就能覆盖大部分场景。

总结一下:性能优化不是一蹴而就的。启动优化让用户「愿意等」,布局优化让用户「用得爽」,内存泄漏检测让App「活得久」。这三者缺一不可。我个人建议每个项目在开发阶段就接入LeakCanary,每次提交代码前跑一遍启动耗时和过度绘制检测——养成习惯后,性能问题会越来越少。


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