稳定性性能系列之十一——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系统的精彩世界!

找到我 : 个人主页

相关推荐
用户74589002079545 小时前
线程池
android
专注前端30年5 小时前
【PHP开发与安全防护实战】性能调优手册
android·安全·php
王正南6 小时前
安卓逆向之LSposed开发(一)
android·xposed·lsposed
爱吃奶酪的松鼠丶6 小时前
React长列表,性能优化。关于循环遍历的时候,key是用对象数据中的ID还是用索引
javascript·react.js·性能优化
sophie旭7 小时前
内存泄露排查之我的微感受
前端·javascript·性能优化
YIN_尹7 小时前
【MySQL】数据类型(上)
android·mysql·adb
robotx9 小时前
AOSP设备节点权限添加相关
android
顾林海9 小时前
Android文件系统安全与权限控制:给应用数据上把“安全锁”
android·面试·操作系统