稳定性性能系列之十一——Android内存优化与OOM问题深度解决

引言

内存问题是Android应用最常见的性能瓶颈之一。一个看似正常运行的应用,可能因为内存泄漏在后台悄悄积累问题,最终在用户使用高峰期突然崩溃。根据Google Play统计数据,OOM(Out Of Memory)是Android应用崩溃的首要原因之一,占比超过30%。

为什么内存问题如此普遍?主要原因包括:

  1. 移动设备资源有限: 即使是高端手机,每个应用可用的堆内存也受到严格限制(通常256MB-512MB)
  2. 生命周期复杂: Activity/Fragment的生命周期管理不当容易导致泄漏
  3. 异步机制: Handler、AsyncTask等异步组件持有外部引用
  4. 图片资源: Bitmap占用大量内存,管理不当直接OOM
  5. 第三方库: 依赖库的内存问题难以察觉

本文将从原理到实战,系统讲解Android内存优化的完整方法论:

  • 第一部分: Android内存管理机制 - 掌握底层原理
  • 第二部分: 内存分析工具全解 - 熟练使用Memory Profiler、MAT、LeakCanary
  • 第三部分: 常见内存泄漏场景 - 识别并解决典型问题
  • 第四部分: OOM问题优化 - Bitmap、缓存、资源释放最佳实践
  • 第五部分: 实战案例 - 图片浏览OOM优化从85%到3%
  • 第六部分: 监控体系 - 建立内存预警机制

Android内存管理机制

要解决内存问题,首先要理解Android如何管理内存。Android的内存管理涉及进程内存模型、GC垃圾回收、LMK低内存杀手三个核心机制。

2.1 进程内存模型

每个Android进程的内存由多个区域组成,理解这些区域的用途是优化的基础。

Java堆 (Java Heap)

Java堆是存储Java对象的内存区域,由ART虚拟机管理。每个应用的Java堆大小受到系统限制:

bash 复制代码
# 查看堆内存限制
adb shell getprop dalvik.vm.heapgrowthlimit  # 普通应用堆限制 (如192m)
adb shell getprop dalvik.vm.heapsize          # large heap限制 (如512m)

关键概念:

  • heapgrowthlimit: 默认堆限制,普通应用不能超过此值
  • heapsize : 通过android:largeHeap="true"可以申请的最大堆
  • Pss (Proportional Set Size): 实际物理内存占用,计算共享内存的比例
  • Uss (Unique Set Size): 进程独占内存,最准确反映实际占用
  • Rss (Resident Set Size): 驻留内存,包含共享库

查看应用内存占用:

bash 复制代码
# 详细内存信息
adb shell dumpsys meminfo com.example.app

# 关键指标解读
# Java Heap: Java对象占用
# Native Heap: Native代码分配 (JNI、NDK、so库)
# Code: 代码段 (dex、so、oat)
# Stack: 线程栈
# Graphics: GPU内存 (纹理、buffer)
# Private Other: 其他私有内存

实际案例输出:

复制代码
App Summary
           Pss(KB)
           ------
      Java Heap:    38436    # Java对象占用38MB
    Native Heap:    12584    # Native分配12MB
           Code:    15220    # 代码段15MB
          Stack:     1036    # 栈1MB
       Graphics:    25688    # GPU内存25MB
  Private Other:     8432
         System:     4256
        Unknown:        0
          TOTAL:   105652    # 总共105MB
Native堆 (Native Heap)

Native堆存储通过JNI/NDK分配的内存,不受Java堆限制但计入进程总内存。常见使用场景:

  • Bitmap像素数据 (Android 8.0+迁移到Native堆)
  • Native库 (如WebView、OpenGL、第三方so)
  • JNI对象分配

Native内存泄漏更难发现,因为Java GC无法回收这些对象。

栈和方法区
  • 栈 (Stack): 每个线程都有独立栈空间,存储局部变量和方法调用。默认1MB,过深递归会导致StackOverflowError
  • 方法区 (Method Area): 存储类信息、常量池、静态变量、JIT编译代码
图形内存 (Graphics Memory)

GPU使用的纹理、缓冲区等内存,在dumpsys meminfo的Graphics项显示。大量图片显示或复杂UI会占用大量图形内存。

2.2 垃圾回收机制 (GC)

Android的ART虚拟机使用自动垃圾回收管理Java堆内存。理解GC机制有助于优化内存分配和避免卡顿。

GC算法

ART使用了多种GC算法的组合:

  1. 标记-清除 (Mark-Sweep): 标记存活对象,清除未标记对象,会产生内存碎片
  2. 复制算法 (Copying): 将存活对象复制到另一块内存,适用于新生代
  3. 标记-整理 (Mark-Compact): 标记后整理内存,消除碎片,适用于老年代
GC类型

在logcat中可以看到不同类型的GC日志:

复制代码
# 并发GC (Concurrent GC)
I/art: Background concurrent mark sweep GC freed 2MB, 15% free, 32MB/38MB, paused 1.203ms total 23.567ms

# 显式GC (Explicit GC)
I/art: Explicit concurrent mark sweep GC freed 156KB, 12% free, 38MB/43MB, paused 987us total 21.345ms

# 分配失败GC (Alloc Failed GC)
I/art: Alloc partial concurrent mark sweep GC freed 1MB, 24% free, 28MB/37MB, paused 2.456ms total 45.678ms

关键指标解读:

  • freed: 回收了多少内存
  • free: 当前堆空闲比例
  • paused: GC暂停时间 (影响UI流畅度)
  • total: GC总耗时
GC Root对象

GC从GC Root对象开始标记存活对象。常见的GC Root包括:

  • 虚拟机栈中引用的对象 (局部变量)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • Native方法引用的对象
  • 活跃线程

内存泄漏的本质: 无用对象被GC Root直接或间接引用,导致GC无法回收。

对象引用类型

Java提供了四种引用类型来控制对象的生命周期:

kotlin 复制代码
// 强引用 (Strong Reference) - 永远不会被GC回收
val strongRef: Object = Object()

// 软引用 (Soft Reference) - 内存不足时回收
val softRef: SoftReference<Bitmap> = SoftReference(bitmap)

// 弱引用 (Weak Reference) - 下次GC时回收
val weakRef: WeakReference<Activity> = WeakReference(activity)

// 虚引用 (Phantom Reference) - 用于对象回收监控
val phantomRef: PhantomReference<Object> = PhantomReference(obj, queue)

最佳实践:

  • 缓存图片使用SoftReferenceWeakReference
  • 避免在静态变量中持有Activity的强引用
  • 异步回调使用WeakReference持有外部对象

2.3 低内存杀手 (LMK)

当系统内存不足时,Android会通过LMK (Low Memory Killer)杀死进程释放内存。理解LMK机制有助于优化应用的生存优先级。

LMK工作原理

LMK是Linux OOM Killer的定制版本,根据进程的oom_adj值决定杀死顺序:

  1. 内核监测系统内存压力 (通过/proc/meminfo)
  2. 当可用内存低于阈值,触发LMK机制
  3. 根据oom_adj值从高到低杀进程 (值越大越先被杀)
  4. 释放的内存分配给前台进程

查看进程的oom_adj值:

bash 复制代码
# 查看所有进程的oom_adj
adb shell ps -A -o NAME,PID,USER,%MEM,OOM_ADJ | grep com.example

# 输出示例
com.example.app    12345   u0_a123   8.5%    0     # 前台进程
com.example.app    12346   u0_a123   2.3%    6     # 可见进程
oom_adj优先级
级别 adj值 进程类型 说明
前台进程 0 FOREGROUND_APP 用户正在交互的Activity
可见进程 1-2 VISIBLE_APP 可见但非前台的Activity
服务进程 3-6 SERVICE 后台Service
缓存进程 7-15 CACHED_APP 已退出的Activity (保留在后台)
空进程 16+ EMPTY 无活动组件的进程

优化建议:

  • 避免在后台占用过多内存
  • 及时释放不用的资源降低内存占用
  • 关键Service使用startForeground()提升优先级
  • 监听onTrimMemory()回调释放缓存
onTrimMemory回调

系统在内存不足时会回调onTrimMemory(),应用应该根据级别释放资源:

kotlin 复制代码
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)

    when (level) {
        // UI隐藏,可以释放UI相关资源
        ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
            Log.d("Memory", "UI hidden, release UI resources")
            releaseUIResources()
        }

        // 进程在后台LRU列表头部,可能被杀死
        ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
            Log.d("Memory", "Background, release some cache")
            imageCache.evictSome()
        }

        // 内存严重不足,进程即将被杀死
        ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
            Log.d("Memory", "Critical memory, release all cache")
            imageCache.evictAll()
            clearAllCache()
        }

        // 前台应用内存不足
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
            Log.d("Memory", "Low memory, shrink cache")
            imageCache.trimToSize(imageCache.size() / 2)
        }
    }
}

AOSP源码位置:

  • LMK实现: system/core/lmkd/lmkd.c
  • oom_adj管理: frameworks/base/services/core/java/com/android/server/am/ProcessList.java
  • 内存监控: frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

内存分析工具全解

工欲善其事,必先利其器。Android提供了丰富的内存分析工具,掌握这些工具是解决内存问题的关键。

3.1 Memory Profiler (Android Studio自带)

Memory Profiler是Android Studio内置的实时内存监控工具,适合快速定位内存增长和泄漏。

基本使用
  1. 打开Memory Profiler:

    • Android Studio → View → Tool Windows → Profiler
    • 选择设备和应用进程
    • 点击Memory行展开详细视图
  2. 实时内存图表:

    • Java: Java堆内存
    • Native: Native堆内存
    • Graphics: 图形内存
    • Stack: 栈内存
    • Code: 代码段
    • Others: 其他内存
  3. 操作功能:

    • Force GC: 手动触发垃圾回收
    • Dump Java Heap: 捕获堆快照
    • Record Allocations: 记录对象分配
检测内存泄漏

典型流程:

  1. 建立基准: 在初始状态Dump堆快照
  2. 执行操作: 打开Activity → 返回 (重复多次)
  3. 触发GC: 点击"Force GC"按钮
  4. 对比快照: Dump第二次快照,对比对象数量

案例: 检测Activity泄漏

kotlin 复制代码
// 操作步骤
1. 打开ProfileActivity → Dump Heap (快照1)
2. 返回 → 再次打开ProfileActivity → 返回 (重复5次)
3. Force GC
4. Dump Heap (快照2)

// 在快照2中搜索ProfileActivity
// 正常: 应该只有0-1个实例
// 泄漏: 如果有5+个实例,说明Activity没被回收

在Heap Dump视图中:

  • 按Class Name排序,搜索Activity类名
  • 查看Shallow Size (对象自身大小) 和 Retained Size (对象及其引用树总大小)
  • 点击实例查看引用链,找到GC Root
Allocation Tracking

记录对象分配可以找到内存增长的源头:

  1. 点击"Record"按钮开始记录
  2. 执行导致内存增长的操作
  3. 点击"Stop"停止记录
  4. 查看分配表格,按Allocations或Shallow Size排序

案例: 查找图片加载内存增长

复制代码
操作: 滑动RecyclerView加载100张图片

分析结果:
Class Name                Allocations    Shallow Size
------------------------------------------------------
android.graphics.Bitmap        120         156 MB     # 图片对象
byte[]                        2340         78 MB      # Bitmap像素数据
Glide$Request                  120         0.5 MB     # Glide请求

可以看出Bitmap及其像素数据占用了主要内存,需要优化图片加载策略。

3.2 MAT (Memory Analyzer Tool)

MAT是Eclipse出品的强大Heap分析工具,适合深度分析复杂内存问题。

生成Heap Dump
bash 复制代码
# 方式1: 通过adb命令
adb shell am dumpheap <pid> /data/local/tmp/heap.hprof
adb pull /data/local/tmp/heap.hprof

# 方式2: 通过Memory Profiler导出
# Memory Profiler → Dump Heap → 右键 → Export to file

# 转换格式 (Android hprof需要转换为标准格式)
hprof-conv heap.hprof heap-converted.hprof
MAT分析流程
  1. 打开hprof文件: File → Open Heap Dump
  2. 查看概览: 自动生成Leak Suspects报告
  3. 分析内存占用: Histogram视图查看所有类的实例数量和内存
  4. 查找泄漏: Dominator Tree查看最大对象及其引用
关键视图

Histogram (直方图):

  • 显示所有类的实例数量和Shallow/Retained Heap
  • 按Retained Heap排序找到占用内存最多的类
  • 右键 → List objects → with incoming references 查看引用链

Dominator Tree (支配树):

  • 显示对象的支配关系 (对象A支配对象B意味着到B的所有路径都经过A)
  • 直观展示哪些对象占用了最多内存
  • 删除某对象能释放多少内存 (Retained Heap)

OQL查询:

sql 复制代码
-- 查找所有Activity实例
SELECT * FROM INSTANCEOF android.app.Activity

-- 查找Bitmap占用超过1MB的对象
SELECT * FROM android.graphics.Bitmap WHERE retainedHeapSize > 1048576

-- 查找静态字段持有的对象
SELECT * FROM java.lang.Object s WHERE s.@GCRoot
案例: 定位Activity泄漏

假设通过Memory Profiler发现有3个MainActivity实例:

  1. Histogram查找 : 搜索MainActivity,显示3个实例

  2. 查看引用链: 右键 → Merge Shortest Paths to GC Roots → exclude weak/soft references

  3. 分析引用链:

    MainActivity @ 0x12a45000
    ← mContext in MyApplication
    ← sInstance in MyApplication (static)
    ← class MyApplication
    ← system class loader

结论: MyApplication的静态单例持有了MainActivity的引用,导致泄漏。

解决方案:

kotlin 复制代码
// ❌ Bad: 静态单例持有Activity
class MyApplication : Application() {
    companion object {
        var sInstance: MyApplication? = null
    }

    fun init(context: Context) {
        sInstance = this  // 持有Activity会导致泄漏
    }
}

// ✅ Good: 使用ApplicationContext
class MyApplication : Application() {
    companion object {
        lateinit var sInstance: Application
    }

    override fun onCreate() {
        super.onCreate()
        sInstance = this  // Application不会泄漏
    }
}

// 或者使用弱引用
companion object {
    var contextRef: WeakReference<Context>? = null
}

3.3 LeakCanary (自动化泄漏检测)

LeakCanary是Square开源的自动内存泄漏检测库,能在开发和测试阶段自动发现泄漏。

集成LeakCanary
kotlin 复制代码
// build.gradle.kts
dependencies {
    // 仅在debug构建中启用
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}

// 无需其他配置,Application启动时自动初始化
工作原理
  1. 监听生命周期: 自动监听Activity、Fragment、ViewModel等组件销毁
  2. 弱引用观察: 组件销毁后创建WeakReference引用
  3. 触发GC: 延迟5秒后触发GC
  4. 检测泄漏: GC后检查WeakReference是否被清除
  5. Dump分析: 发现泄漏时Dump堆快照分析引用链
  6. 通知展示: 在通知栏显示泄漏报告
查看泄漏报告

LeakCanary检测到泄漏后会显示详细报告:

复制代码
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│  Leaking: NO (Android system class)
│  ↓ static ActivityThread.sCurrentActivityThread
│
├─ android.app.ActivityThread instance
│  Leaking: NO (ActivityThread is always reachable)
│  ↓ ActivityThread.mActivities
│
├─ android.util.ArrayMap instance
│  Leaking: NO (Application is still reachable)
│  ↓ ArrayMap[0]
│
├─ android.app.ActivityThread$ActivityClientRecord instance
│  Leaking: UNKNOWN
│  ↓ ActivityThread$ActivityClientRecord.activity
│
╰→ com.example.MainActivity instance
   Leaking: YES (RefWatcher was watching this and MainActivity#onDestroy() was called)
   key = 8a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
   retainedSize = 1.2 MB

报告解读:

  • GC Root: 泄漏的根源 (此例为ActivityThread静态字段)
  • Leaking: YES: 确认发生泄漏
  • retainedSize: 泄漏导致无法回收的内存大小
自定义监听

监听自定义对象:

kotlin 复制代码
class MyService : Service() {
    override fun onDestroy() {
        super.onDestroy()

        // 通知LeakCanary监听此Service
        AppWatcher.objectWatcher.watch(
            watchedObject = this,
            description = "MyService received onDestroy callback"
        )
    }
}
生产环境禁用

LeakCanary仅应在debug构建中使用,发布版本会自动禁用:

kotlin 复制代码
// 确保仅debug依赖
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// releaseImplementation不添加依赖

3.4 命令行工具

除了图形化工具,命令行工具在CI/CD和自动化测试中更实用。

dumpsys meminfo

查看进程内存详情:

bash 复制代码
# 查看指定应用
adb shell dumpsys meminfo com.example.app

# 查看所有应用内存排序
adb shell dumpsys meminfo --oom

# 只显示关键摘要
adb shell dumpsys meminfo com.example.app --summary
procrank

查看所有进程内存排序 (需要root):

bash 复制代码
adb shell procrank

# 输出示例
  PID      Vss      Rss      Pss      Uss  cmdline
 1234   580M     85M      42M      35M  com.example.app
 5678   420M     65M      28M      22M  com.android.systemui
showmap

查看进程内存映射 (需要root):

bash 复制代码
adb shell showmap <pid>

# 显示各个so库、dex文件的内存占用
脚本自动化

将内存监控集成到CI流程:

bash 复制代码
#!/bin/bash
# memory_monitor.sh

PACKAGE="com.example.app"
THRESHOLD_MB=200

while true; do
    PSS=$(adb shell dumpsys meminfo $PACKAGE | grep "TOTAL PSS" | awk '{print $3}')
    echo "$(date): PSS = ${PSS}KB"

    PSS_MB=$((PSS / 1024))
    if [ $PSS_MB -gt $THRESHOLD_MB ]; then
        echo "WARNING: Memory exceeded threshold ${THRESHOLD_MB}MB"
        adb shell am dumpheap $(adb shell pidof $PACKAGE) /data/local/tmp/heap.hprof
        echo "Heap dump saved"
        break
    fi

    sleep 5
done

常见内存泄漏场景与解决

内存泄漏是Android开发中最常见的问题。掌握典型场景和解决方案,可以避免90%的内存问题。

4.1 静态变量持有Activity

这是最常见的泄漏场景,因为静态变量的生命周期是整个应用进程。

错误示例1: 静态变量直接持有Activity
kotlin 复制代码
// ❌ Bad: 静态变量持有Activity引用
class DataManager {
    companion object {
        var sContext: Context? = null
    }

    fun init(context: Context) {
        sContext = context  // 如果传入Activity,会导致泄漏
    }

    fun doSomething() {
        val text = sContext?.getString(R.string.app_name)
    }
}

// MainActivity中初始化
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DataManager().init(this)  // ⚠️ 传入Activity引用
    }
}

泄漏原因 : sContext是静态变量,持有MainActivity引用,MainActivity即使销毁也无法被GC回收。

解决方案: 使用ApplicationContext
kotlin 复制代码
// ✅ Good: 使用ApplicationContext
class DataManager {
    companion object {
        lateinit var sAppContext: Context
    }

    fun init(context: Context) {
        // 转换为ApplicationContext
        sAppContext = context.applicationContext
    }
}

// 或者在Application中初始化
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        DataManager.sAppContext = this
    }
}

核心原则: 静态变量只能持有ApplicationContext,永远不要持有Activity、Service、View等短生命周期对象。

错误示例2: 静态内部类持有外部引用
kotlin 复制代码
// ❌ Bad: 单例持有Activity
class ImageLoader private constructor() {
    companion object {
        val instance: ImageLoader by lazy { ImageLoader() }
    }

    private var context: Context? = null

    fun init(context: Context) {
        this.context = context  // 持有Activity会泄漏
    }

    fun loadImage(url: String, imageView: ImageView) {
        // 使用context加载图片
    }
}

解决方案: 使用弱引用或ApplicationContext

kotlin 复制代码
// ✅ Good: 使用WeakReference
class ImageLoader private constructor() {
    companion object {
        val instance: ImageLoader by lazy { ImageLoader() }
    }

    private var contextRef: WeakReference<Context>? = null

    fun init(context: Context) {
        contextRef = WeakReference(context.applicationContext)
    }

    fun loadImage(url: String, imageView: ImageView) {
        contextRef?.get()?.let { context ->
            // 使用context加载图片
        }
    }
}

4.2 非静态内部类持有外部类

内部类、匿名类默认持有外部类引用,如果生命周期不一致会导致泄漏。

错误示例1: Handler内部类泄漏
kotlin 复制代码
// ❌ Bad: Handler内部类持有Activity
class MainActivity : AppCompatActivity() {

    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // 这个匿名内部类持有MainActivity引用
            updateUI()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 发送延迟消息
        handler.sendEmptyMessageDelayed(0, 60000)  // 1分钟后处理

        // 如果用户在1分钟内退出Activity,Activity无法被回收
    }

    private fun updateUI() {
        // 更新界面
    }
}

泄漏原因:

  1. Handler匿名内部类持有MainActivity引用
  2. Handler的Message被放入MessageQueue,持有Handler引用
  3. MessageQueue在1分钟内一直持有Message
  4. MainActivity → Handler → Message → MessageQueue,导致MainActivity无法回收
解决方案: 静态内部类 + 弱引用
kotlin 复制代码
// ✅ Good: 静态Handler + 弱引用
class MainActivity : AppCompatActivity() {

    private val handler = MyHandler(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        handler.sendEmptyMessageDelayed(0, 60000)
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理Handler消息
        handler.removeCallbacksAndMessages(null)
    }

    private fun updateUI() {
        // 更新界面
    }

    // 静态内部类不持有外部类引用
    private class MyHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
        // 使用弱引用持有Activity
        private val activityRef = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            // 获取Activity,可能为null
            val activity = activityRef.get()
            if (activity == null || activity.isFinishing) {
                return  // Activity已销毁,直接返回
            }

            activity.updateUI()
        }
    }
}

核心原则:

  1. Handler使用静态内部类或独立类
  2. 通过WeakReference持有外部对象
  3. onDestroy中移除所有消息: handler.removeCallbacksAndMessages(null)
错误示例2: Thread/Runnable匿名类泄漏
kotlin 复制代码
// ❌ Bad: Thread匿名类持有Activity
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Thread {
            // 长时间任务
            Thread.sleep(60000)

            // 访问Activity成员
            runOnUiThread {
                updateUI()
            }
        }.start()

        // Thread持有Activity引用,即使Activity销毁Thread仍在运行
    }
}

解决方案: 使用静态类或协程

kotlin 复制代码
// ✅ Good: 使用Kotlin协程
class MainActivity : AppCompatActivity() {

    private val scope = CoroutineScope(Dispatchers.Main + Job())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        scope.launch {
            withContext(Dispatchers.IO) {
                // 后台任务
                delay(60000)
            }

            // 回到主线程更新UI
            updateUI()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 取消所有协程
        scope.cancel()
    }
}

4.3 单例模式持有Context

单例的生命周期是整个应用,如果持有Activity引用会导致严重泄漏。

错误示例: 单例持有Activity
kotlin 复制代码
// ❌ Bad: 单例持有Activity
class LocationManager private constructor(private val context: Context) {

    companion object {
        @Volatile
        private var instance: LocationManager? = null

        fun getInstance(context: Context): LocationManager {
            return instance ?: synchronized(this) {
                instance ?: LocationManager(context).also { instance = it }
            }
        }
    }

    fun startLocationUpdates() {
        // 使用context注册监听器
    }
}

// Activity中使用
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ⚠️ 传入Activity,导致单例持有Activity引用
        val locationManager = LocationManager.getInstance(this)
    }
}
解决方案: 使用ApplicationContext
kotlin 复制代码
// ✅ Good: 单例使用ApplicationContext
class LocationManager private constructor(context: Context) {

    // 转换为ApplicationContext
    private val appContext: Context = context.applicationContext

    companion object {
        @Volatile
        private var instance: LocationManager? = null

        fun getInstance(context: Context): LocationManager {
            return instance ?: synchronized(this) {
                instance ?: LocationManager(context.applicationContext).also { instance = it }
            }
        }
    }

    fun startLocationUpdates() {
        // 使用appContext注册监听器
    }
}

最佳实践: 所有单例涉及Context时,统一在Application中初始化:

kotlin 复制代码
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 统一初始化单例
        LocationManager.getInstance(this)
        DatabaseHelper.init(this)
        ImageLoader.init(this)
    }
}

4.4 未取消的监听器和回调

注册的监听器、回调如果未及时取消,会持有对象引用导致泄漏。

错误示例1: 未注销BroadcastReceiver
kotlin 复制代码
// ❌ Bad: 注册Receiver未注销
class MainActivity : AppCompatActivity() {

    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            updateUI()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 动态注册Receiver
        val filter = IntentFilter("com.example.ACTION")
        registerReceiver(receiver, filter)

        // ⚠️ 忘记在onDestroy中注销
    }

    // 缺少onDestroy注销
}

解决方案: 配对注册和注销

kotlin 复制代码
// ✅ Good: 配对注册和注销
class MainActivity : AppCompatActivity() {

    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            updateUI()
        }
    }

    private var isReceiverRegistered = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val filter = IntentFilter("com.example.ACTION")
        registerReceiver(receiver, filter)
        isReceiverRegistered = true
    }

    override fun onDestroy() {
        super.onDestroy()

        // 注销Receiver
        if (isReceiverRegistered) {
            unregisterReceiver(receiver)
            isReceiverRegistered = false
        }
    }
}
错误示例2: 未移除Listener
kotlin 复制代码
// ❌ Bad: 添加Listener未移除
class MainActivity : AppCompatActivity() {

    private val scrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            updateHeader()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        recyclerView.addOnScrollListener(scrollListener)

        // ⚠️ 忘记移除Listener
    }
}

解决方案: onDestroy中移除

kotlin 复制代码
// ✅ Good: 移除Listener
class MainActivity : AppCompatActivity() {

    private val scrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            updateHeader()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        recyclerView.addOnScrollListener(scrollListener)
    }

    override fun onDestroy() {
        super.onDestroy()

        // 移除Listener
        recyclerView.removeOnScrollListener(scrollListener)
    }
}
常见需要注销的组件
组件 注册方法 注销方法 注销时机
BroadcastReceiver registerReceiver() unregisterReceiver() onDestroy
EventBus EventBus.register() EventBus.unregister() onDestroy
RxJava subscribe() dispose() onDestroy
LiveData observe() removeObserver() 自动管理(lifecycle-aware)
Animation start() cancel() onPause/onDestroy
Handler Message sendMessage() removeCallbacksAndMessages(null) onDestroy

最佳实践: 使用Lifecycle感知组件自动管理生命周期:

kotlin 复制代码
// ✅ Good: 使用LiveData自动管理
class MainActivity : AppCompatActivity() {

    private val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // LiveData会自动在onDestroy时移除观察者
        viewModel.data.observe(this) { data ->
            updateUI(data)
        }
    }

    // 无需手动注销
}

4.5 资源未关闭

File、Cursor、Stream等资源使用后必须关闭,否则会导致文件描述符泄漏。

错误示例: 文件未关闭
kotlin 复制代码
// ❌ Bad: 文件未关闭
fun readFile(path: String): String {
    val inputStream = FileInputStream(path)
    val reader = BufferedReader(InputStreamReader(inputStream))
    return reader.readText()
    // ⚠️ 忘记关闭stream,导致文件描述符泄漏
}

解决方案: 使用use()自动关闭

kotlin 复制代码
// ✅ Good: 使用use自动关闭
fun readFile(path: String): String {
    return File(path).inputStream().use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            reader.readText()
        }
    }
}

// 或者简化为
fun readFile(path: String): String {
    return File(path).readText()
}

核心原则 : 所有Closeable资源都应该用use()包裹,确保自动关闭。


OOM问题分析与优化

OOM(Out Of Memory)是Android应用崩溃的首要原因。理解OOM类型和优化策略是每个Android开发者的必备技能。

5.1 OOM类型分类

Android OOM有多种类型,需要针对性分析。

Java堆OOM

最常见的OOM,Java对象分配超过堆限制:

复制代码
Fatal Exception: java.lang.OutOfMemoryError: Failed to allocate a 4194316 byte allocation with 2097152 free bytes and 2MB until OOM

特征:

  • 错误信息: OutOfMemoryError: Failed to allocate
  • 原因: Bitmap、大数组、对象泄漏
Native堆OOM

Native代码分配内存失败:

复制代码
Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 12345

特征:

  • 错误信息: pthread_create failed: couldn't allocate 或 SIGSEGV
  • 原因: JNI内存泄漏、第三方so库问题、Bitmap像素数据(Android 8.0+)
FD(文件描述符)耗尽

打开文件/Socket过多:

复制代码
java.io.IOException: Could not open file: /data/xxx.db: Too many open files

特征:

  • 错误信息: Too many open files
  • 原因: 文件/Cursor/Stream未关闭
线程数超限

创建线程过多:

复制代码
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again

特征:

  • 错误信息: pthread_create failed
  • 原因: 线程泄漏、线程池配置不当

查看当前线程数:

bash 复制代码
adb shell ps -T <pid> | wc -l

5.2 Bitmap内存优化

Bitmap是OOM的主要元凶,优化Bitmap加载是解决OOM的关键。

问题: 加载大图导致OOM
kotlin 复制代码
// ❌ Bad: 直接加载大图
fun loadLargeImage(path: String): Bitmap {
    return BitmapFactory.decodeFile(path)
    // 4K图片(4096x2160 ARGB_8888)占用: 4096*2160*4 = 35MB
    // 可能直接OOM
}
优化1: 采样压缩 (inSampleSize)
kotlin 复制代码
// ✅ Good: 按需采样加载
fun decodeSampledBitmapFromFile(
    path: String,
    reqWidth: Int,  // 需要的宽度
    reqHeight: Int  // 需要的高度
): Bitmap {
    return BitmapFactory.Options().run {
        // 第一次解码: 只获取图片尺寸
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(path, this)

        val (actualWidth, actualHeight) = outWidth to outHeight

        // 计算采样率
        inSampleSize = calculateInSampleSize(actualWidth, actualHeight, reqWidth, reqHeight)

        // 第二次解码: 实际加载图片
        inJustDecodeBounds = false
        BitmapFactory.decodeFile(path, this)
    }
}

fun calculateInSampleSize(
    actualWidth: Int,
    actualHeight: Int,
    reqWidth: Int,
    reqHeight: Int
): Int {
    var inSampleSize = 1

    if (actualHeight > reqHeight || actualWidth > reqWidth) {
        val halfHeight = actualHeight / 2
        val halfWidth = actualWidth / 2

        // 计算最大的inSampleSize(2的幂次),保证缩放后尺寸仍大于需要的尺寸
        while ((halfHeight / inSampleSize) >= reqHeight &&
               (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

// 使用: 加载到ImageView (1080x1920屏幕)
val bitmap = decodeSampledBitmapFromFile(
    path = "/sdcard/large_image.jpg",
    reqWidth = 1080,
    reqHeight = 1920
)
imageView.setImageBitmap(bitmap)

// 4K图片采样后: 4096/4 x 2160/4 x 4 = 2.2MB (节省93%内存)

核心原理:

  • inJustDecodeBounds = true: 只解析图片头部获取尺寸,不分配内存
  • inSampleSize: 采样率,必须是2的幂次 (2, 4, 8, 16...)
  • inSampleSize=4: 宽高各缩小4倍,内存占用减少16倍
优化2: 使用RGB_565格式
kotlin 复制代码
// ✅ Good: 使用RGB_565减少内存
fun loadBitmapRGB565(path: String): Bitmap {
    return BitmapFactory.Options().run {
        inPreferredConfig = Bitmap.Config.RGB_565  // 2字节/像素
        BitmapFactory.decodeFile(path, this)
    }
}

// ARGB_8888: 4字节/像素 (透明度+RGB各1字节)
// RGB_565: 2字节/像素 (R5位+G6位+B5位,无透明度)
// 内存节省50%,适合不需要透明度的场景
优化3: Bitmap复用 (inBitmap)
kotlin 复制代码
// ✅ Good: 复用Bitmap内存
class BitmapPool {
    private val reusableBitmaps = mutableSetOf<SoftReference<Bitmap>>()

    fun addBitmapToPool(bitmap: Bitmap) {
        reusableBitmaps.add(SoftReference(bitmap))
    }

    fun getBitmapFromPool(options: BitmapFactory.Options): Bitmap? {
        var bitmap: Bitmap? = null

        if (reusableBitmaps.isNotEmpty()) {
            val iterator = reusableBitmaps.iterator()
            while (iterator.hasNext()) {
                val item = iterator.next().get()

                if (item != null && item.isMutable) {
                    // 检查尺寸是否匹配
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item
                        iterator.remove()
                        break
                    }
                } else {
                    iterator.remove()
                }
            }
        }

        return bitmap
    }

    private fun canUseForInBitmap(
        candidate: Bitmap,
        targetOptions: BitmapFactory.Options
    ): Boolean {
        val width = targetOptions.outWidth / targetOptions.inSampleSize
        val height = targetOptions.outHeight / targetOptions.inSampleSize
        val byteCount = width * height * getBytesPerPixel(candidate.config)

        return byteCount <= candidate.allocationByteCount
    }

    private fun getBytesPerPixel(config: Bitmap.Config): Int {
        return when (config) {
            Bitmap.Config.ARGB_8888 -> 4
            Bitmap.Config.RGB_565 -> 2
            Bitmap.Config.ARGB_4444 -> 2
            Bitmap.Config.ALPHA_8 -> 1
            else -> 1
        }
    }
}

// 使用复用池
val pool = BitmapPool()

fun loadBitmapWithReuse(path: String): Bitmap {
    return BitmapFactory.Options().run {
        inMutable = true
        inBitmap = pool.getBitmapFromPool(this)  // 尝试复用
        BitmapFactory.decodeFile(path, this)
    }
}

核心原理 : 设置inBitmap后,BitmapFactory会尝试复用该Bitmap的内存,避免重新分配。

优化4: 及时回收Bitmap
kotlin 复制代码
// ✅ Good: 不用时及时回收
imageView.setImageBitmap(null)  // 清除ImageView引用
bitmap.recycle()  // 回收Bitmap内存

// 注意: recycle()后Bitmap不可再使用

5.3 LruCache缓存优化

使用LruCache管理内存缓存,自动淘汰旧数据。

kotlin 复制代码
// ✅ Good: 使用LruCache管理图片缓存
class ImageCache {

    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()  // KB
    private val cacheSize = maxMemory / 8  // 使用1/8堆内存作为缓存

    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {

        // 计算每个Bitmap的大小(KB)
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount / 1024
        }

        // 移除时回收Bitmap
        override fun entryRemoved(
            evicted: Boolean,
            key: String?,
            oldValue: Bitmap?,
            newValue: Bitmap?
        ) {
            if (evicted && oldValue != null && !oldValue.isRecycled) {
                oldValue.recycle()
            }
        }
    }

    fun getBitmap(key: String): Bitmap? {
        return memoryCache.get(key)
    }

    fun putBitmap(key: String, bitmap: Bitmap) {
        if (getBitmap(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }

    fun evictAll() {
        memoryCache.evictAll()
    }

    fun trimToSize(size: Int) {
        memoryCache.trimToSize(size)
    }
}

// 使用
val imageCache = ImageCache()

fun loadImageWithCache(url: String, imageView: ImageView) {
    val cached = imageCache.getBitmap(url)
    if (cached != null) {
        imageView.setImageBitmap(cached)
        return
    }

    // 从网络/磁盘加载
    loadImageAsync(url) { bitmap ->
        imageCache.putBitmap(url, bitmap)
        imageView.setImageBitmap(bitmap)
    }
}

LruCache原理:

  • LRU (Least Recently Used): 最近最少使用算法
  • 缓存满时自动淘汰最久未使用的项
  • 通过sizeOf()计算每项大小
  • trimToSize()可手动缩减缓存

5.4 集成Glide优化图片加载

推荐使用Glide等成熟图片加载库,已内置大量优化:

kotlin 复制代码
// ✅ Good: 使用Glide加载图片
dependencies {
    implementation("com.github.bumptech.glide:glide:4.15.1")
}

// 基本使用
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)  // 占位图
    .error(R.drawable.error)              // 错误图
    .into(imageView)

// 指定尺寸(自动采样)
Glide.with(context)
    .load(imageUrl)
    .override(800, 600)  // 指定目标尺寸
    .into(imageView)

// 使用缩略图
Glide.with(context)
    .load(imageUrl)
    .thumbnail(0.1f)  // 先加载10%尺寸的缩略图
    .into(imageView)

// 监听内存警告
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
        Glide.get(this).clearMemory()  // 清空内存缓存
    }
}

Glide内置优化:

  • 自动采样和缩放
  • 三级缓存 (内存/磁盘/网络)
  • Bitmap复用池
  • 生命周期感知 (自动取消请求)
  • 格式转换 (WebP/JPEG优化)

5.5 大对象缓存优化

除了图片,其他大对象也需要合理缓存。

分页加载
kotlin 复制代码
// ✅ Good: 分页加载大数据
class DataRepository {

    private val pageSize = 50
    private val cache = LruCache<Int, List<Data>>(20)  // 缓存20页

    suspend fun loadPage(page: Int): List<Data> {
        // 先查缓存
        cache.get(page)?.let { return it }

        // 从数据库/网络加载
        val data = database.queryPage(page, pageSize)
        cache.put(page, data)

        return data
    }
}
及时释放资源
kotlin 复制代码
// ✅ Good: 监听内存警告
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)

    when (level) {
        ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
            // UI隐藏,释放UI资源
            releaseUIResources()
        }

        ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
            // 进入后台,释放部分缓存
            imageCache.trimToSize(imageCache.size() / 2)
        }

        ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
            // 内存严重不足,清空所有缓存
            imageCache.evictAll()
            dataCache.clear()
        }
    }
}

实战案例: 图片浏览OOM优化

通过一个真实案例,展示完整的OOM问题分析和优化过程。

6.1 问题现象

场景: 图片浏览应用,用户快速滑动浏览大量高清图片时频繁OOM崩溃。

统计数据 (优化前):

  • OOM崩溃率: 85次/万次启动
  • 平均内存占用: 420MB (接近512MB堆限制)
  • 内存峰值: 498MB (触发OOM)
  • 用户反馈: 浏览50+张图片必然崩溃

6.2 问题分析

步骤1: 复现问题
kotlin 复制代码
// 原始代码 (存在问题的版本)
class GalleryActivity : AppCompatActivity() {

    private val images = mutableListOf<Bitmap>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 加载图片列表
        val imagePaths = getImagePaths()
        imagePaths.forEach { path ->
            val bitmap = BitmapFactory.decodeFile(path)  // ❌ 直接加载大图
            images.add(bitmap)  // ❌ 全部保存在内存
        }

        setupRecyclerView(images)
    }
}
步骤2: Memory Profiler分析
  1. 打开Memory Profiler监控内存

  2. 滑动浏览50张图片

  3. 观察内存曲线:

    内存占用变化:
    0s: 80MB (启动)
    10s: 180MB (加载10张)
    20s: 320MB (加载30张)
    30s: 450MB (加载48张)
    32s: 498MB → OOM崩溃

  4. Dump Heap分析:

    • Bitmap及byte\[\]占用: 385MB
    • 单张4K图片: 35MB (4096x2160x4)
    • 问题根源: 全量加载+未压缩+未复用
步骤3: 定位代码问题

通过MAT分析Heap Dump:

复制代码
Dominator Tree:
├─ GalleryActivity @ 0x12a45000  Retained: 485MB
   ├─ images: ArrayList @ 0x12a46000  Retained: 385MB
   │  ├─ [0] Bitmap @ 0x12b00000  Retained: 35MB
   │  ├─ [1] Bitmap @ 0x12b50000  Retained: 35MB
   │  ├─ [2] Bitmap @ 0x12c00000  Retained: 35MB
   │  ... (48 Bitmaps)

问题点:

  1. 直接加载原图,未采样压缩
  2. ArrayList持有所有Bitmap引用
  3. RecyclerView未复用Bitmap
  4. 未使用缓存机制

6.3 优化方案

优化1: 采样加载
kotlin 复制代码
// ✅ 优化: 按需采样加载
fun loadSampledBitmap(path: String, targetWidth: Int = 1080): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(path, this)

        // 计算采样率
        inSampleSize = calculateInSampleSize(outWidth, outHeight, targetWidth, targetWidth * 16 / 9)

        inJustDecodeBounds = false
        inPreferredConfig = Bitmap.Config.RGB_565  // 使用RGB_565
        BitmapFactory.decodeFile(path, this)
    }
}

// 内存占用: 35MB → 2.2MB (节省93%)
优化2: 使用Glide + LruCache
kotlin 复制代码
// ✅ 优化: 集成Glide
class GalleryActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val imagePaths = getImagePaths()

        // 使用RecyclerView + Glide
        recyclerView.adapter = GalleryAdapter(imagePaths)
    }
}

class GalleryAdapter(private val imagePaths: List<String>) :
    RecyclerView.Adapter<GalleryAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val path = imagePaths[position]

        // Glide自动管理内存
        Glide.with(holder.itemView.context)
            .load(path)
            .override(1080, 1920)  // 指定目标尺寸
            .placeholder(R.drawable.placeholder)
            .into(holder.imageView)
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val imageView: ImageView = view.findViewById(R.id.imageView)
    }
}
优化3: 预加载与复用
kotlin 复制代码
// ✅ 优化: RecyclerView预加载
recyclerView.apply {
    // 设置RecycledViewPool
    setRecycledViewPool(RecyclerView.RecycledViewPool().apply {
        setMaxRecycledViews(0, 20)  // 缓存20个ViewHolder
    })

    // 设置预加载
    layoutManager = LinearLayoutManager(context).apply {
        isItemPrefetchEnabled = true
        initialPrefetchItemCount = 4
    }
}

// Glide预加载
fun preloadNextImages(startPosition: Int, count: Int) {
    for (i in startPosition until min(startPosition + count, imagePaths.size)) {
        Glide.with(this)
            .load(imagePaths[i])
            .preload(1080, 1920)
    }
}
优化4: 监听内存警告
kotlin 复制代码
// ✅ 优化: 响应内存警告
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)

    when (level) {
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
            // 清空Glide内存缓存
            Glide.get(this).clearMemory()
        }

        ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
            // UI隐藏,清空部分缓存
            Glide.get(this).trimMemory(level)
        }
    }
}

6.4 优化效果

指标 优化前 优化后 改善幅度
OOM崩溃率 85次/万次启动 3次/万次启动 96.5%
平均内存占用 420MB 95MB 77%
内存峰值 498MB 156MB 69%
浏览体验 50张必崩溃 500+张流畅 10倍

优化措施贡献度:

  1. 采样加载 + RGB_565: 60% (35MB → 2.2MB/张)
  2. Glide LruCache自动管理: 25% (限制缓存大小)
  3. RecyclerView复用: 10% (减少对象创建)
  4. 内存警告响应: 5% (及时释放缓存)

6.5 验证与监控

自动化测试
kotlin 复制代码
@Test
fun testImageLoadingMemory() {
    // 启动应用
    val scenario = ActivityScenario.launch(GalleryActivity::class.java)

    // 获取初始内存
    val initialMemory = getMemoryUsage()

    // 滑动加载100张图片
    onView(withId(R.id.recyclerView))
        .perform(RecyclerViewActions.scrollToPosition(100))

    // 触发GC
    Runtime.getRuntime().gc()
    Thread.sleep(1000)

    // 检查内存增长
    val currentMemory = getMemoryUsage()
    val memoryIncrease = currentMemory - initialMemory

    // 断言: 内存增长不超过200MB
    assert(memoryIncrease < 200 * 1024 * 1024) {
        "Memory increase exceeded 200MB: ${memoryIncrease / 1024 / 1024}MB"
    }
}

fun getMemoryUsage(): Long {
    val runtime = Runtime.getRuntime()
    return runtime.totalMemory() - runtime.freeMemory()
}
线上监控
kotlin 复制代码
// 集成Firebase Crashlytics监控OOM
class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        // 监听内存警告
        registerComponentCallbacks(object : ComponentCallbacks2 {
            override fun onTrimMemory(level: Int) {
                val memoryInfo = getMemoryInfo()

                // 记录到Crashlytics
                FirebaseCrashlytics.getInstance().log(
                    "onTrimMemory: level=$level, availMem=${memoryInfo.availMem / 1024 / 1024}MB"
                )

                if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
                    // 上报内存危机事件
                    FirebaseCrashlytics.getInstance().recordException(
                        Exception("Critical memory warning")
                    )
                }
            }

            override fun onConfigurationChanged(newConfig: Configuration) {}
            override fun onLowMemory() {}
        })
    }

    private fun getMemoryInfo(): ActivityManager.MemoryInfo {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return ActivityManager.MemoryInfo().also {
            activityManager.getMemoryInfo(it)
        }
    }
}

内存监控与预警

建立完善的内存监控体系,可以在问题发生前预警和干预。

7.1 Runtime API监控

在应用运行时实时监控内存状态:

kotlin 复制代码
class MemoryMonitor {

    fun getMemoryInfo(): MemoryInfo {
        val runtime = Runtime.getRuntime()
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val memInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memInfo)

        return MemoryInfo(
            maxMemory = runtime.maxMemory(),        // 最大堆内存
            totalMemory = runtime.totalMemory(),    // 已分配堆内存
            freeMemory = runtime.freeMemory(),      // 堆中空闲内存
            usedMemory = runtime.totalMemory() - runtime.freeMemory(),  // 已使用内存
            systemAvailMem = memInfo.availMem,      // 系统可用内存
            systemTotalMem = memInfo.totalMem,      // 系统总内存
            systemLowMemory = memInfo.lowMemory     // 系统是否低内存
        )
    }

    fun getMemoryUsagePercent(): Float {
        val runtime = Runtime.getRuntime()
        val used = (runtime.totalMemory() - runtime.freeMemory()).toFloat()
        val max = runtime.maxMemory().toFloat()
        return (used / max) * 100
    }

    fun isMemoryWarning(): Boolean {
        return getMemoryUsagePercent() > 80  // 超过80%触发警告
    }
}

data class MemoryInfo(
    val maxMemory: Long,
    val totalMemory: Long,
    val freeMemory: Long,
    val usedMemory: Long,
    val systemAvailMem: Long,
    val systemTotalMem: Long,
    val systemLowMemory: Boolean
)

7.2 定期检测与告警

kotlin 复制代码
class MemoryWatchdog(private val context: Context) {

    private val handler = Handler(Looper.getMainLooper())
    private val monitor = MemoryMonitor()
    private val checkInterval = 5000L  // 5秒检测一次

    private val checkRunnable = object : Runnable {
        override fun run() {
            checkMemory()
            handler.postDelayed(this, checkInterval)
        }
    }

    fun start() {
        handler.post(checkRunnable)
    }

    fun stop() {
        handler.removeCallbacks(checkRunnable)
    }

    private fun checkMemory() {
        val info = monitor.getMemoryInfo()
        val usagePercent = monitor.getMemoryUsagePercent()

        Log.d("MemoryWatchdog", "Memory usage: ${usagePercent.toInt()}%")

        // 内存预警
        when {
            usagePercent > 90 -> {
                Log.e("MemoryWatchdog", "CRITICAL: Memory usage ${usagePercent.toInt()}%")
                onMemoryCritical()
            }
            usagePercent > 80 -> {
                Log.w("MemoryWatchdog", "WARNING: Memory usage ${usagePercent.toInt()}%")
                onMemoryWarning()
            }
        }

        // 上报监控数据
        reportMemoryMetrics(info, usagePercent)
    }

    private fun onMemoryWarning() {
        // 清理部分缓存
        context.applicationContext.apply {
            if (this is Application) {
                (this as? ComponentCallbacks2)?.onTrimMemory(
                    ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
                )
            }
        }
    }

    private fun onMemoryCritical() {
        // 紧急清理所有缓存
        Glide.get(context).clearMemory()

        // 通知所有Activity
        context.applicationContext.apply {
            if (this is Application) {
                (this as? ComponentCallbacks2)?.onTrimMemory(
                    ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
                )
            }
        }

        // 触发GC
        Runtime.getRuntime().gc()
    }

    private fun reportMemoryMetrics(info: MemoryInfo, usagePercent: Float) {
        // 上报到监控平台
        FirebaseAnalytics.getInstance(context).logEvent("memory_usage") {
            param("usage_percent", usagePercent.toLong())
            param("used_mb", (info.usedMemory / 1024 / 1024).toInt().toLong())
            param("max_mb", (info.maxMemory / 1024 / 1024).toInt().toLong())
        }
    }
}

// Application中启动
class MyApplication : Application() {
    private lateinit var memoryWatchdog: MemoryWatchdog

    override fun onCreate() {
        super.onCreate()

        memoryWatchdog = MemoryWatchdog(this)
        memoryWatchdog.start()
    }
}

7.3 内存泄漏自动检测

集成LeakCanary在开发阶段自动检测:

kotlin 复制代码
// build.gradle.kts
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}

// 自定义LeakCanary配置
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 自定义LeakCanary配置 (可选)
        LeakCanary.config = LeakCanary.config.copy(
            dumpHeap = true,  // 发现泄漏时Dump堆
            retainedVisibleThreshold = 3  // 保留3个对象未回收时告警
        )
    }
}

7.4 线上OOM监控

通过Firebase Crashlytics监控线上OOM:

kotlin 复制代码
class CrashReportingTree : Timber.Tree() {
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (priority < Log.WARN) return

        val crashlytics = FirebaseCrashlytics.getInstance()
        crashlytics.log("$tag: $message")

        t?.let {
            if (it is OutOfMemoryError) {
                // OOM特殊处理
                val memoryInfo = getMemorySnapshot()
                crashlytics.setCustomKey("oom_heap_used", memoryInfo.usedMemory)
                crashlytics.setCustomKey("oom_heap_max", memoryInfo.maxMemory)
            }
            crashlytics.recordException(it)
        }
    }

    private fun getMemorySnapshot(): MemoryInfo {
        return MemoryMonitor().getMemoryInfo()
    }
}

// Application中初始化
Timber.plant(CrashReportingTree())

总结与最佳实践

8.1 核心要点回顾

内存管理机制:

  • Java堆受heapgrowthlimit限制,普通应用通常192-256MB
  • GC无法回收被GC Root直接或间接引用的对象
  • LMK根据oom_adj值从高到低杀进程释放内存
  • 监听onTrimMemory()及时释放缓存

内存分析工具:

  • Memory Profiler: 实时监控,快速定位内存增长
  • MAT: 深度分析Heap Dump,查找泄漏根源
  • LeakCanary: 自动化泄漏检测,开发必备
  • 命令行工具: dumpsys meminfo用于脚本监控

常见泄漏场景:

  • 静态变量持有Activity → 使用ApplicationContext
  • Handler内部类持有Activity → 静态类+弱引用
  • 单例持有Context → 转换为ApplicationContext
  • 未注销Listener/Receiver → onDestroy中配对注销
  • 资源未关闭 → 使用use()自动关闭

OOM优化:

  • Bitmap采样加载: inSampleSize降低内存
  • 使用RGB_565格式节省50%内存
  • LruCache管理缓存,自动淘汰旧数据
  • 集成Glide简化图片加载和内存管理
  • 监听onTrimMemory()及时释放资源

8.2 最佳实践清单

开发阶段:

  • ✅ 所有Activity销毁后检查LeakCanary报告
  • ✅ 使用Memory Profiler监控内存曲线
  • ✅ Bitmap加载必须采样,禁止直接BitmapFactory.decodeFile()
  • ✅ 长生命周期对象(单例/静态)不持有短生命周期对象(Activity/View)
  • ✅ Handler/Thread/Runnable使用静态类+弱引用
  • ✅ 所有Closeable资源用use()包裹

测试阶段:

  • ✅ 编写自动化测试验证内存增长
  • ✅ 使用Monkey测试长时间运行稳定性
  • ✅ 分析MAT Heap Dump检查大对象
  • ✅ 压测高负载场景 (快速滑动/频繁切换)

生产阶段:

  • ✅ 集成Crashlytics监控OOM崩溃率
  • ✅ 定期分析线上OOM堆栈
  • ✅ 监控关键页面内存指标
  • ✅ A/B测试内存优化效果

代码规范:

kotlin 复制代码
// ✅ DO: 正确的内存管理
class GoodActivity : AppCompatActivity() {

    // 使用ViewModel管理数据
    private val viewModel: MyViewModel by viewModels()

    // 静态Handler + 弱引用
    private val handler = MyHandler(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 使用Glide加载图片
        Glide.with(this)
            .load(url)
            .override(800, 600)
            .into(imageView)

        // LiveData自动管理生命周期
        viewModel.data.observe(this) { data ->
            updateUI(data)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理Handler消息
        handler.removeCallbacksAndMessages(null)
    }

    private class MyHandler(activity: GoodActivity) : Handler(Looper.getMainLooper()) {
        private val activityRef = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            activityRef.get()?.let { activity ->
                if (!activity.isFinishing) {
                    // 安全使用Activity
                }
            }
        }
    }
}

// ❌ DON'T: 错误的内存管理
class BadActivity : AppCompatActivity() {
    companion object {
        var sContext: Context? = null  // ❌ 静态变量持有Context
    }

    private val handler = object : Handler() {  // ❌ 匿名内部类持有Activity
        override fun handleMessage(msg: Message) {
            updateUI()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sContext = this  // ❌ 存储Activity到静态变量

        val bitmap = BitmapFactory.decodeFile(path)  // ❌ 直接加载大图
        imageView.setImageBitmap(bitmap)

        handler.sendEmptyMessageDelayed(0, 60000)  // ❌ 长时间持有Activity
    }

    // ❌ 缺少onDestroy清理
}

8.3 延伸阅读

AOSP源码:

  • ART GC实现: art/runtime/gc/
  • LMK机制: system/core/lmkd/lmkd.c
  • ActivityManager内存管理: frameworks/base/services/core/java/com/android/server/am/

工具文档:

系列文章:

作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!


🎉 感谢关注,让我们一起深入Android系统的精彩世界!

找到我 : 个人主页

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
liangshanbo12151 天前
React 性能优化实战
性能优化·react
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas