10. 从崩溃率6.7%到0.5%!Android内存优化小红书实战案例:泄漏、抖动、溢出 KOOM+LeakCanary+MAT (最完整的解决方案)

7,582 阅读12分钟

之前几篇文章,主要是内存的案例! 这次写一个总的实际案例,一步一步分析内存问题!

先看内存系列的文章,然后再看本文, 多看几遍和结合案例,你会有不一样的突破

1.Android 内存泄露实战之自定义view 揭秘LeakCanary+MAT+Profile全链路深度解剖自定义View泄漏 - 掘金

2. Android 深度剖析LeakCanary:从原理到实践的全方位指南1. LeakCanary基本使用介绍 - 掘金

3.Android 内存优化 Koom分析官方案例实战简介: KOOM是快手团队开源的Android内存监控与分析 - 掘金

4.Android 大图片导致的内存溢出实战 KOOM + Profile +MAT 深入分析摘要: 通过 - 掘金

5. Android 加载抖音视频导致的内存溢出实战 KOOM+Profiler+MAT大内存的场景 加载大 - 掘金

6.Android 精准诊断小对象内存累积导致OOM实 KOOM+Profiler+MAT分析因很多小内存堆积导致 - 掘金

7. Android RecyclerView吃了80MB内存!KOOM定位+Profiler解剖+MAT验尸全记录 - 掘金

8. Android 崩溃率直降90%!快手KOOM+Profiler+MAT组合拳暴打Bitmap泄漏该案例中的图片未 - 掘金

9. Android KOOM深度源码解析:快手开源线上OOM杀手!毫秒级内存快照背后的黑科技揭秘快手开源的And - 掘金

大纲.png

1.内存有哪些问题

1.1 内存泄漏

1.1.1. 内存泄漏的原因

对象不再被使用,但因其被无效引用(Garbage Root持有)导致GC无法回收

内存泄漏的本质是对象生命周期管理失控

1.1.2. 常见的内存泄漏的场景

泄漏类型典型场景后果
静态引用泄漏public static Activity currentActivity;Activity无法回收
未注销监听器广播、事件总线、位置监听未反注册Context长期滞留
Handler泄漏非静态Handler + 延迟消息 + Activity退出未清除消息Handler持有Activity
线程泄漏AsyncTask持有Activity引用/线程池任务未取消任务完成前无法回收
资源未释放文件流、数据库游标、动画未关闭资源对象累积
系统服务泄漏InputMethodManager持有EditText/Activity销毁时未清除焦点意外持有View
Fragment泄漏ViewPager中的Fragment被静态变量引用Fragment无法回收
核心机制:
  1. GC Roots 强引用滞留

    • 静态变量(static fields)
    • 活动线程(active threads)
    • JNI全局引用
    • 系统服务注册(ClipboardManager等)
  2. 对象引用链未断开

    // 典型泄漏链示例

    static Context sContext; // GC Root

    ↳ Activity instance // 泄漏对象

    ↳ View hierarchy

    ↳ Bitmap(10MB) // 大内存对象

  3. 生命周期不匹配

    • 长生命周期对象持有短生命周期对象
    • 例如:静态工具类持有Activity引用

1.1.3.内存泄漏的解决工具LeakCanary及其原理

 LeakCanary:自动化泄漏检测利器

LeakCanary 的核心也是解决内存问题常见的三板斧:监控泄漏、采集镜像、分析镜像。

核心原理:

1. 监听Activity/Fragment销毁

2.注册生命周期,在activity和fragment结束的时候会进行检测

3.引用队列RefrenceQueue,5s有没有被清除。手动调用GC,Runtime

4.找到有问题的引用,开始分析(可达分析),是否是真正的内存泄露

详细的原理可以看我之前的文章

2. Android 深度剖析LeakCanary:从原理到实践的全方位指南1. LeakCanary基本使用介绍 - 掘金

leak.webp

1.1.4.内存泄漏的解决工具Profiler分析

Heap Dump分析技巧:

  1. 筛选Activity实例:

    • 在Objects栏搜索Activity名$(如MainActivity
    • 正常情况应只剩1个实例(当前显示的)
    • 泄漏时出现多个实例
  2. 查看引用链:

    • 右键实例 → "Jump to Source"
    • 在"References"面板排除弱/软引用
    • 查找强引用路径

1.Android 内存泄露实战之自定义view 揭秘LeakCanary+MAT+Profile全链路深度解剖自定义View泄漏 - 掘金

1.1.5.内存泄漏的解决工具MAT分析

  1. 导出Profiler的Hprof文件
  2. 用MAT打开 → 执行Leak Suspects Report
  3. 查看Dominator Tree排序
  4. 重点排查占用内存高的Activity

具体介绍如下:

1.Android 内存泄露实战之自定义view 揭秘LeakCanary+MAT+Profile全链路深度解剖自定义View泄漏 - 掘金

1.2 内存抖动

1.2.1 内存抖动的原因

短时间内大量对象被创建并迅速回收,导致频繁GC(尤其是Young GC

1.2.2 内存抖动的场景

项目中特有场景
  1. RecyclerView滚动优化

    • 问题:onBindViewHolder中创建Bitmap/Paint
    • 现象:快速滚动时明显卡顿
    • 方案:ViewHolder复用+对象池
  2. 动画资源泄漏

    • 问题:未复用ValueAnimator/ObjectAnimator
    • 现象:连续触发动画时内存锯齿状波动
    • 方案:使用AnimatorSet复用动画对象
典型内存抖动场景
场景问题代码示例优化方案
循环体内创建对象for(int i=0; i<1000; i++) { String s = "Item" + i; }改用StringBuilder复用
View绘制创建对象onDraw()new Paint()/new Path()提前初始化并重用
数据解析临时对象Gson解析生成大量临时对象使用流式解析(如JsonReader)
集合操作list.add(new Item())未预分配空间ArrayList(initialCapacity)

1.2.3 内存抖动的分析工具profiler+KOOM

KOOM 是线上环境监控内存抖动的唯一可行方案,Profiler 仅适用于线下深度分析

问题:KOOM能否监控出内存抖动?

可以, - 每 5秒 采集一次 Java 堆内存占用

  • 计算相邻两次采样的波动率(波动幅度/总内存)
KOOM典型报告字段
{
  "issue_type": "memory_churn",
  "process": "com.example.app",
  "device_model": "Pixel 6",
  "churn_rate": "62%", 
  "top_stacks": [
    {
      "stack": "android.graphics.Bitmap.createBitmap → MyAdapter.onBindViewHolder",
      "count": 128,
      "sample_time": "2023-08-12 14:22:33"
    },
    {
      "stack": "java.lang.StringBuilder.toString → ProductParser.parse",
      "count": 86,
      "sample_time": "2023-08-12 14:22:35"
    }
  ]
}
关键指标说明
字段说明诊断意义
churn_rate内存波动率>40% 表明剧烈抖动
count相同堆栈的分配次数数值越大问题越严重
stack对象分配调用链直接定位代码位置

大量小对象不停的创建导致内存抖动和OOM: 具体的案例参考下面

6.Android 精准诊断小对象内存累积导致OOM实 KOOM+Profiler+MAT分析因很多小内存堆积导致 - 掘金

1.3 内存溢出

java.png

1.3.1 内存溢出的原因

内存溢出(OOM)发生在应用内存占用超过系统限制时

问题:OOM是哪部分内存满了?多进程是如何的?

  • Java/Kotlin 堆内存溢出(最常见)

    • 区域Java Heap(Dalvik/ART 虚拟机管理的内存)
    • 表现java.lang.OutOfMemoryError: Java heap space
    • 原因:应用创建过多对象(如 Bitmap 未释放、数据缓存失控、内存泄漏累积),超过 堆最大限制ActivityManager.getMemoryClass(),通常 64MB-512MB,因设备而异)。
    • 关键点:即使物理内存充足,单个 App 的 Java 堆也不能无限增长。
  • Native 堆内存溢出

    • 区域Native Heap(C/C++ 代码或 JNI 分配的内存)
    • 表现java.lang.OutOfMemoryError: native memory exhausted (或直接崩溃)
    • 原因:NDK 代码泄漏、OpenGL 纹理未释放、MediaCodec 等 Native 资源未回收。
    • 特点:与 Java 堆独立,通过 malloc/new 分配,上限更高但仍有约束

OOM可以大致归为以下3类:

1.pthread_create问题-----  线程数太多. 50%

2.文件描述符超限问题------打开太多文件. 10%

上面2个, pthread_create 和 fd 数量不足均为 native 内存限制导致的 Java 层崩溃

3.堆内存超限---------------内存不足. 40%

堆内存超限原因

1.单次配过大

2.累计使用过大

具体机制:
  1. Java堆内存耗尽

    • 内存泄漏累积(如泄漏Activity)
    • 超大对象分配(如未压缩的Bitmap),
    • 累计的小对象堆积: 缓存失控(无上限的LruCache),图片没有释放,动画没有释放

堆内存单次分配过大多次分配累计过大

触发这类问题的原因有数据异常导致单次内存分配过大超限,也有一些是 StringBuilder 拼接累计大小过大导致等等。这类问题的解决思路比较简单,问题就在当前的堆栈。

堆内存累积分配触顶

这类问题的问题堆栈会比较分散,在任何内存分配的场景上都有可能会被触发,那些高频的内存分配节点发生的概率会更高,比如 Bitmap 分配内存。这类 OOM 的根本原因是内存累积占用过多,而当前的堆栈只是压死骆驼的最后一根稻草,并不是问题的根本所在。所以这类问题我们需要分析整体的内存分配情况,从中找到不合理的内存使用(比如内存泄露、大对象、过多小对象、大图等)

  1. Native内存耗尽

    • Bitmap像素数据(占Native堆)
    • MediaCodec/OpenGL资源未释放
    • JNI层malloc分配的内存泄漏
  2. 线程资源枯竭

pthread_create (1040KB stack) failed: Out of memory

这个是典型的创建新线程触发的OOM问题

```
// 线程数超限公式
max_threads = (VM_MAX - VSS - heap_max) / stack_size
```

-   32位设备默认线程数限制:~320个
-   64位设备默认线程数限制:~1024

3. 文件描述符耗尽

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
Java.lang.OutOfMemoryError: Could not allocate JNI Env
  • 未关闭的文件流/网络连接
  • 泄漏的ParcelFileDescriptor
  • Cursor未关闭

对于打开文件数太多的问题,盲目优化其实无从下手,总体的方案是监控为主。(C++实现)

1.3.2 内存溢出的场景

Android特有OOM陷阱(修订版)

场景内存类型错误信息示例关键特征
加载超清图片Nativeandroid.graphics.Bitmap.nativeCreate单张图片占用30MB+ Native内存
RecyclerView小对象累积Javajava.lang.OutOfMemoryError: Java heap space海量小对象(如ItemModel)占据堆内存
视频解码NativeMediaCodec 缓冲区溢出视频帧未及时释放
内存泄漏累积Javajava.lang.OutOfMemoryError: Java heap space泄漏对象(如Activity)随时间累积
线程爆炸线程栈pthread_create failed: Out of memory线程数超过1024限制(/proc/self/task)
数据库游标未关闭FDToo many open filesFD数超过1024限制(/proc/self/fd)

OOM的项目案例

1.帧动画

2.拍多张照片,比如9张图片批量上传, 小红书的发布帖子

3.分享图片给微信,bitmap没有释放

  1. 随心听多个tab页面,如果tab增加到了20个,就会OOM

5.欢迎页的图片

6.自定义view,涉及到的图片

7.Feed流页面

首页推荐列表的每一次 Loadmore 操作,都不会清理之前缓存起来的视频对象,导致用户长时间停留在推荐 Feed 时,存起来的视频对象过多会导致内存方面的压力

1.3.3 内存溢出的解决工具KOOM

  • 大图片
  • 图片没有释放
  • 内存泄漏
  • 大对象
  • 集合对象
  • 线程
  • fd

1.3.3.1 KOOM的使用案例

3.Android 内存优化 Koom分析官方案例实战简介: KOOM是快手团队开源的Android内存监控与分析 - 掘金 具体包含:

1.大图片导致增大

4.Android 大图片导致的内存溢出实战 KOOM + Profile +MAT 深入分析摘要: 通过 - 掘金

2.大对象导致增大OOM

5. Android 加载抖音视频导致的内存溢出实战 KOOM+Profiler+MAT大内存的场景 加载大 - 掘金

3.RecycleView导致增大OOM

[7. Android RecyclerView吃了80MB内存!KOOM定位+Profiler解剖+MAT验尸全记录 - 掘金] (juejin.cn/post/753610…)

4.小对象累计堆积导致增大OOM

6.Android 精准诊断小对象内存累积导致OOM实 KOOM+Profiler+MAT分析因很多小内存堆积导致 - 掘金

5.图片没有及时释放

8. Android 崩溃率直降90%!快手KOOM+Profiler+MAT组合拳暴打Bitmap泄漏该案例中的图片未 - 掘金

1.3.3.2 KOOM的原理

其核心流程为三部分: 和LeakCanary是一样的

1.监控OOM,发生问题时触发内存镜像的采集,以便进一步分析问题(监控)
2.采集内存镜像,学名堆转储,将内存数据拷贝到文件中,以下简称dump hprof(采集)
3.解析镜像文件,对泄漏、超大对象等我们关注的对象进行可达性分析,解析出其到GC root的引用链以解决问题(分析)

总流程: 监控------------->采集(裁剪)------------>解析-------------------->上传

详细的原理:参考之前的文章

9. Android KOOM深度源码解析:快手开源线上OOM杀手!毫秒级内存快照背后的黑科技揭秘快手开源的And - 掘金

koom.png

1.3.4 内存溢出的解决工具MAT

在下面的6.4 详细的介绍了! 之前的MAT案例如下:

4.Android 大图片导致的内存溢出实战 KOOM + Profile +MAT 深入分析摘要: 通过 - 掘金

2.为什么要优化内存? 什么原因导致你优化内存问题

2.1. 线上崩溃报警(最高优先级)
// 典型崩溃日志
java.lang.OutOfMemoryError: 
  Failed to allocate a 12582928 byte allocation 
  with 4194304 free bytes
  • 触发场景

    • 监控平台突增OOM报警
    • 特定机型/OS版本崩溃率飙升
2.2. 卡顿投诉激增
  • 用户反馈"页面滑动卡顿"
  • 性能平台显示GC耗时 > 100ms/帧
  • 案例:电商大促时列表页滚动卡顿率上升15倍
2.3. 竞品性能碾压
指标你的应用竞品应用差距
内存占用185MB126MB↓32%
OOM率0.25%0.08%↑212%
帧率稳定性78%93%↓16pts
2.4. 后台被杀死

因为腾讯音乐在后台运行播放,内存越高越容易被系统杀死。就像进程有个优先级,adj-oom的值越低,越不容易被杀死!!!

2.5.bugly上TimeOut问题

我们在stackoverflow上看到了相关的讨论,大致意思是有些情况下比如息屏,或者一些省电模式下,频繁地调System.gc()可能会因为内核状态切换超时的异常。这个问题貌似没有比较好的解决方法,只能是优化内存,尽量减少手动调用System.gc()

根本也是因为内存问题

3.内存的基本概念

VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

3.1 栈内存

  • 存储内容

    • 基本数据类型(int, boolean等)
    • 对象引用(指向堆内存地址)
    • 方法调用栈帧(局部变量表、操作数栈等)
  • 关键参数

    • 默认大小:1MB/线程(Android ART)
    • 修改方式:-Xss参数(但Android不推荐修改)
  • 溢出场景

    // 递归调用导致栈溢出
    void stackOverflow() {
        stackOverflow();  // StackOverflowError
    }
    

3.2 堆内存

对象

3.3.内存计算指标,应用分配多大内存, 最大可用内存,可用内存

关键API与计算:
// 获取内存指标
Runtime runtime = Runtime.getRuntime();

// 当前已分配内存
long totalMemory = runtime.totalMemory(); 

// 最大可用内存(堆上限)
long maxMemory = runtime.maxMemory();

// 空闲内存(未使用部分)
long freeMemory = runtime.freeMemory();

// 实际使用内存
long usedMemory = totalMemory - freeMemory;

// 可用内存(还可分配的空间)
long availableMemory = maxMemory - usedMemory;
Android特有指标:
// 获取系统内存信息
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
((ActivityManager)getSystemService(ACTIVITY_SERVICE)).getMemoryInfo(memInfo);

// 系统剩余内存
boolean isLowMemory = memInfo.lowMemory;
long systemAvailMem = memInfo.availMem; 

3.4 一个Object对象占用多少内存?一个线程占用多少内存?

图解:

object内省.png

  • 计算结果

    • 32位系统:8字节 (对象头4B + 类型指针4B)
    • 64位系统:16字节 (对象头8B + 类型指针8B)
    • 开启指针压缩:12字节 (对象头8B + 类型指针4B) 验证代码
// 使用Instrumentation计算
public class MemoryCounter {
    public static long sizeOf(Object obj) {
        return android.os.Debug.getNativeHeapAllocatedSize();
    }
    
    public static void main(String[] args) {
        long before = sizeOf(new Object[0]);
        Object obj = new Object();
        long after = sizeOf(obj);
        System.out.println("Object size: " + (after - before));
    }
}

3.5 计算Java对象内存大小?

图片:

对象内存.png

3.6 问题: 一张图片100x100在内存中的大小?

核心计算公式

内存大小 = 宽度 × 高度 × 每个像素占用的字节数

关键点:图片内存占用由像素总量和色彩格式决定,与文件大小无关。务必关注设备密度适配问题,这是实际开发中最常见的内存放大陷阱!

密度适配影响

 如果图片放在 `res/drawable-xxhdpi` 目录(480dpi)
 运行在 `mdpi` 设备(160dpi)时:  
    内存尺寸 = 100 × (480/160) = 300×300 像素!
    此时内存占用:`300×300×4 = 360,000 字节 ≈ 352 KB`  
    (解决方案:使用 `res/drawable-nodpi` 或手动缩放)

3.7 . Garbage Collection (GC):

-   自动回收不再使用的对象占用的堆内存。
-   Young GC (Minor GC):  回收Young Gen,快但频繁。
-   Full GC (Major GC):  回收整个堆(Young+Old),慢,STW时间长。

问题:为什么Android应用要使用多进程?

一个进程有一份虚拟机,一个虚拟机默认是32M.

1). 更大的内存分配,我们知道,Android设备限制了每一个进程所分配的内存大小,如果这个大小是32M,那么如果我们的应用有两个进程,那么所能使用的最大内存就是32*2=64M。所以如果用户经常收到OutOfMemory异常,那么就应该考虑使用多进程。

2). 防止进程被杀死。考虑这样一种情况,一个后台进程在播放腾讯音乐,此时,内存吃紧,系统需要释放更多的内存,此时系统就会优先把UI进程给杀掉,而不是播放音乐的进程。还有另外一个应用场景,我们可以使用守护进程来保护我们的主进程不被杀死。具体的做法是主进程和守护进程相互监护,若对方被杀死,则重启。

场景:

我们就以音乐播放器为例,播放音乐的实现功能部份我们往往放在Service中去做,并把这个Service运行在一个单独的进程中(通过设置android:process属性)

这样做的好处是,音乐播放器的UI部份在一个进程,可以退出释放内存。而Service部份所在的进程没有UI资源,占用的内存也较少

下表对比不同设备的理论值与实际值:

设备类型maxMemory(MB)实际安全阈值(MB)占用率
低端机(heapgrowthlimit=96)967679%
中端机(heapgrowthlimit=192)19215380%
高端机(heapsize=512)51240980%

终极提示:在Android开发中,实际可用内存 ≈ maxMemory × 0.8,超过此值将触发频繁GC,需预留20%缓冲空间保证流畅性。

4. 如何解决上面说的三种内存问题

 核心解决思路
  • 内存泄漏:  找到并切断无效引用链。
  • 内存抖动:  减少不必要的对象创建,重用对象。
  • 内存溢出:  根治泄漏 + 合理管理资源 + 优化内存占用

优化内存的手段

LeakCanary+Profiler+Koom+ MAT

工具链综合对比表

工具内存泄漏内存抖动内存溢出使用场景
LeakCanary✅ 自动定位开发/测试阶段
Profiler✅ 深度验证✅ 分配追踪⚠️ 需手动触发开发阶段
KOOM✅ 线上监控✅ 波动检测✅ 现场捕获生产环境
MAT✅ 引用链分析✅ 对象统计✅ 根源定位深度分析

4.1 内存泄漏 : LeakCanary + Profiler

1.Android 内存泄露实战之自定义view 揭秘LeakCanary+MAT+Profile全链路深度解剖自定义View泄漏 - 掘金

4.2 内存抖动 : KOOM+ Profiler

6.Android 精准诊断小对象内存累积导致OOM实 KOOM+Profiler+MAT分析因很多小内存堆积导致 - 掘金

4.3 内存溢出 : KOOM+ MAT

5. Android 加载抖音视频导致的内存溢出实战 KOOM+Profiler+MAT大内存的场景 加载大 - 掘金

5. 内存OOM问题的专项监控方案

其实:KOOM就做是做到了对应内存OOM得监控

koom2.png

5.1 图片内存优化和监控专项

  • 监控:  Hook Bitmap构造函数/decode方法,记录大小、堆栈、生命周期。

  • 优化:

    • 建立图片加载规范(尺寸限制、格式选择)。
    • 实现全局图片加载监控平台,统计大图、重复加载、泄漏图。
    • 推广使用inBitmap和高效图片库。

图片异常监控的4大维度

图片监控.png

5.1.1 尺寸监控实现

大图检测:github.com/Leaking/Hun… 通过AOP,自定义插件实现

或者参考这个

Android ASM 字节码插桩:监控大图加载加载图片是一个很常规的操作,同时也是一个“成本”较高的行为,因为加载一张 - 掘金

// 全局Hook Bitmap加载
class BitmapMonitor {
    static void hookBitmapFactory() {
        // 使用Xposed或Epic框架Hook
        XposedHelpers.findAndHookMethod(
            "android.graphics.BitmapFactory",
            "decodeResource",
            Resources.class, int.class, Options.class,
            new XC_MethodHook() {
                protected void afterHookedMethod(MethodHookParam param) {
                    Bitmap bitmap = (Bitmap) param.getResult();
                    if (bitmap.getWidth() > 4096 || bitmap.getHeight() > 4096) {
                        reportOversizedBitmap(bitmap, getCallStack());
                    }
                }
            }
        );
    }
    
    private static void reportOversizedBitmap(Bitmap bitmap, String stack) {
        // 上报到监控平台
        ImageMonitor.report("OVERSIZE", 
            "width=" + bitmap.getWidth(),
            "height=" + bitmap.getHeight(),
            "stack=" + stack
        );
    }
}

5.1.2 图片生命周期监控实现

class LifecycleBitmapTracker {
    private static final Map<Activity, Set<Bitmap>> activityBitmaps = new HashMap<>();
    
    static void onActivityStart(Activity activity) {
        activityBitmaps.put(activity, new WeakHashSet<>());
    }
    
    static void trackBitmap(Activity activity, Bitmap bitmap) {
        Set<Bitmap> bitmaps = activityBitmaps.get(activity);
        if (bitmaps != null) {
            bitmaps.add(bitmap);
        }
    }
    
    static void onActivityStop(Activity activity) {
        Set<Bitmap> bitmaps = activityBitmaps.get(activity);
        if (bitmaps != null) {
            for (Bitmap bmp : bitmaps) {
                if (!bmp.isRecycled()) {
                    reportLeakedBitmap(activity, bmp);
                }
            }
            activityBitmaps.remove(activity);
        }
    }
}
5.1.3 图片重复加载监控

Github有人开发的完整的项目地址:

github.com/JsonChao/Ch…

class BitmapCacheMonitor {
    private static final LruCache<String, Bitmap> cache = new LruCache<>(50);
    private static final Map<String, Integer> loadCount = new HashMap<>();
    
    static Bitmap loadBitmap(Context ctx, String url) {
        Bitmap cached = cache.get(url);
        if (cached != null) {
            // 记录重复加载
            int count = loadCount.getOrDefault(url, 0);
            loadCount.put(url, count + 1);
            
            if (count > 3) { // 重复加载超过3次
                reportRedundantLoad(url, count);
            }
            return cached;
        }
        
        // 实际加载逻辑
        Bitmap bitmap = doLoad(url);
        cache.put(url, bitmap);
        return bitmap;
    }
}

5.1.4 图片内存大小监控实现

class BitmapMemoryTracker {
    private static final Map<Bitmap, BitmapInfo> bitmapMap = new WeakHashMap<>();
    
    static void track(Bitmap bitmap, String source) {
        long size = bitmap.getAllocationByteCount();
        bitmapMap.put(bitmap, new BitmapInfo(size, source));
        
        // 超过阈值预警
        if (size > 10 * 1024 * 1024) { // 10MB
            reportLargeBitmap(bitmap, source);
        }
    }
    
    // 周期性检查
    static void checkTotalUsage() {
        long total = 0;
        for (BitmapInfo info : bitmapMap.values()) {
            total += info.size;
        }
        
        long maxMemory = Runtime.getRuntime().maxMemory();
        if (total > maxMemory * 0.25) { // 超过25%堆内存
            reportMemoryOverflow(total, maxMemory);
        }
    }
}

5.1.5 图片的格式监控实现: 不同的图片占用的内存大小不一样

异常类型检测方式预警阈值自动处理动作
超大尺寸实时Hook+尺寸检测宽度 > 3840px 或 高度 > 2160px1. 自动降级为RGB_565格式 2. 压缩尺寸至50% 3. 记录原始尺寸
生命周期泄漏Activity生命周期跟踪onStop()后5分钟未释放1. 强制回收Bitmap 2. 记录堆栈信息 3. 通知开发平台
重复加载加载次数统计> 3次/分钟1. 增强缓存提醒 2. 自动加入强引用缓存 3. 阻止重复解码
内存超限周期性扫描> 总可用内存25%1. 清除LRU缓存最旧30% 2. 降级所有图片质量 3. 触发紧急GC

5.1.6 图片优化方案:

就是Glide的原理, 根据图片计算的公式

1.图片的压缩

2.图片采样率缩放

3.图片的三级缓存

4.大图片区域加载

5.图片的格式

6.生命周期,释放图片

5.2 线程内存监控专项

  • 问题:  野线程持有Context/Activity;线程数过多(栈内存消耗);线程泄漏(未正确结束)。

  • 监控:

    • Hook Thread/ThreadPoolExecutor构造函数,记录创建堆栈、生命周期。
    • 定期采集线程列表(/proc/self/taskThread.getAllStackTraces()),分析存活线程状态和持有对象。
  • 优化:  使用统一线程池管理;避免频繁创建销毁线程;使用ThreadLocal注意清理。

一般最大线程数限制为 500 个。

线程异常的五维监控体系

5.2.1. 线程数量监控 - 防止线程爆炸
class ThreadCountMonitor {
    private static final int THREAD_THRESHOLD = 200; // 阈值
    
    public static void start() {
        Executors.newSingleThreadScheduledExecutor()
            .scheduleAtFixedRate(() -> {
                int threadCount = getThreadCount();
                if (threadCount > THREAD_THRESHOLD) {
                    // 记录创建堆栈
                    Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
                    reportThreadOverflow(threadCount, stacks);
                    
                    // 自动处理
                    autoHandleThreadOverflow();
                }
            }, 0, 5, TimeUnit.SECONDS); // 每5秒检测
    }
    
    private static int getThreadCount() {
        File taskDir = new File("/proc/self/task");
        return taskDir.listFiles().length; // 获取真实线程数
    }
    
    private static void autoHandleThreadOverflow() {
        // 1. 停止非核心线程池
        ThreadPoolManager.stopNonCriticalPools();
        
        // 2. 清理僵尸线程
        cleanZombieThreads();
        
        // 3. 限制新线程创建
        ThreadPolicy.enableCreationLimit();
    }
}
5.2.2. 线程生命周期监控 - 检测僵尸线程
class ThreadLivenessMonitor {
    private static final ConcurrentMap<Long, ThreadHeartbeat> threadMap = new ConcurrentHashMap<>();
    private static final long TIMEOUT = 30_000; // 30秒超时
    
    static void onThreadStart(Thread t) {
        threadMap.put(t.getId(), new ThreadHeartbeat());
    }
    
    static void updateHeartbeat(long threadId) {
        ThreadHeartbeat hb = threadMap.get(threadId);
        if (hb != null) hb.lastBeat = SystemClock.uptimeMillis();
    }
    
    static void checkLiveness() {
        long now = SystemClock.uptimeMillis();
        for (Map.Entry<Long, ThreadHeartbeat> entry : threadMap.entrySet()) {
            if (now - entry.getValue().lastBeat > TIMEOUT) {
                reportZombieThread(entry.getKey(), entry.getValue().creationStack);
            }
        }
    }
    
    static class ThreadHeartbeat {
        long lastBeat;
        StackTraceElement[] creationStack;
        
        ThreadHeartbeat() {
            this.lastBeat = SystemClock.uptimeMillis();
            this.creationStack = Thread.currentThread().getStackTrace();
        }
    }
}
5.2.3. 线程资源消耗监控 - 定位内存黑洞
class ThreadResourceMonitor {
    // 估算线程内存占用
    static long estimateThreadMemory(Thread t) {
        long memory = 0;
        
        // 1. 栈内存(默认1MB)
        memory += t.getStackTrace().length * 1024; // 估算
        
        // 2. Thread对象本身
        memory += ClassLayout.parseClass(Thread.class).instanceSize();
        
        // 3. 局部变量(通过字节码分析)
        memory += analyzeLocalVariables(t);
        
        return memory;
    }
    
    // 周期性扫描资源消耗TOP10线程
    static void monitorTopConsumers() {
        Map<Thread, Long> memoryMap = new HashMap<>();
        Set<Thread> threads = Thread.getAllStackTraces().keySet();
        
        for (Thread t : threads) {
            memoryMap.put(t, estimateThreadMemory(t));
        }
        
        // 取TOP10内存消耗线程
        List<Map.Entry<Thread, Long>> topList = memoryMap.entrySet().stream()
            .sorted(Map.Entry.comparingByValue().reversed())
            .limit(10)
            .collect(Collectors.toList());
        
        // 上报
        reportTopConsumers(topList);
    }
}
5.2.4. 堆栈异常监控 - 检测阻塞和死锁
class DeadlockDetector {
    static void detect() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        long[] threadIds = threadBean.findDeadlockedThreads();
        
        if (threadIds != null) {
            ThreadInfo[] infos = threadBean.getThreadInfo(threadIds);
            for (ThreadInfo info : infos) {
                reportDeadlock(info);
            }
            // 自动恢复:中断死锁线程
            Arrays.stream(threadIds).forEach(id -> {
                Thread t = findThreadById(id);
                if (t != null) t.interrupt();
            });
        }
    }
    
    static void detectBlocked() {
        Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
        for (Map.Entry<Thread, StackTraceElement[]> entry : stacks.entrySet()) {
            if (entry.getKey().getState() == Thread.State.BLOCKED) {
                // 阻塞超过5秒视为异常
                if (entry.getValue().length > 10) { // 堆栈深度判断
                    reportBlockedThread(entry.getKey(), entry.getValue());
                }
            }
        }
    }
}
5.2.5.创建模式监控 - 规范线程使用
class ThreadCreationMonitor {
    private static final Map<String, Integer> creationPatterns = new ConcurrentHashMap<>();
    
    static void onThreadCreate(Thread t) {
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        // 提取关键创建堆栈特征
        String pattern = extractCreationPattern(stack); 
        
        // 统计创建频率
        int count = creationPatterns.getOrDefault(pattern, 0) + 1;
        creationPatterns.put(pattern, count);
        
        // 高频创建预警
        if (count > 10) { // 10分钟内创建10次以上
            reportHighFrequencyCreation(pattern, count);
            
            // 自动转线程池
            if (shouldConvertToThreadPool(pattern)) {
                convertToThreadPool(pattern);
            }
        }
    }
    
    private static String extractCreationPattern(StackTraceElement[] stack) {
        // 取前3个业务层堆栈
        for (int i = 0; i < stack.length; i++) {
            String className = stack[i].getClassName();
            if (className.startsWith("com.yourpackage")) {
                return className + "#" + stack[i].getMethodName();
            }
        }
        return "unknown";
    }
}
异常类型检测方式预警阈值自动处理动作
线程爆炸/proc统计+堆栈分析>200线程(中端设备)1. 清理僵尸线程 2. 阻止新线程创建 3. 转线程池
僵尸线程心跳超时检测>30秒无响应1. 强制中断线程 2. 记录资源快照 3. 重启服务
资源消耗内存估算+TOP排序>2MB/线程1. dump线程堆栈 2. 限制资源分配 3. 降级功能
死锁阻塞堆栈状态分析BLOCKED>5秒1. 中断阻塞线程 2. 解锁资源 3. 回滚操作
高频创建创建模式统计>10次/10分钟1. 自动转线程池 2. 缓存线程实例 3. 限制创建频率

优化方案: 全局替换线程创建方式,替换成线程池

Android ASM 字节码插桩:进行线程整治最近看了 京东零售技术 发表的一篇文章:AOP 技术在 APP 开发中的 - 掘金

5.3 监测内存泄漏

  • 线上:  集成LeakCanary定制版或自研Hprof上传分析平台(在OOM或特定时机触发Dump)。
  • 线下:  LeakCanary + Android Profiler深度分析。
  • 监控关键点:  Activity/Fragment泄漏、Service泄漏、单例持有Context、匿名内部类泄漏。

LeakCanary: 上面已经有了,就是Leakcanry的源码

5.4 FD监控 (文件描述符)

当进程中的FD数量达到最大限制时,再去新建线程,在创建JNIEnv时会抛出OOM错误。但是FD数量超出限制除了会导致创建线程抛出OOM以外,还会导致很多其它的异常,为了能够统一处理这类FD数量溢出的问题,Probe中对进程中的FD数量做了监控。在后台启动一个线程,每隔1s读取一次当前进程创建的FD数量,当检测到FD数量达到阈值时(FD最大限制的95%),读取当前进程的所有FD信息归并后上报。

在/proc/pid/limits描述着Linux系统对对应进程的限制,其中Max open files就代表可创建FD的最大数目。

进程中创建的FD记录在/proc/pid/fd中,通过遍历/proc/pid/fd,可以得到FD的信息。

  • 问题:  FD泄漏导致IOException: Too many open files,常由未关闭文件流、Socket、数据库连接等引起。
  • 监控:  定期读取/proc/self/fd目录,统计FD数量及类型。设置阈值告警。
  • 优化:  确保所有Closeable对象(FileInputStreamCursorSocket)在finally块中关闭或使用try-with-resources(Java 7+)

KOOM的源码,监控FD, 之前的文章已经详细分析了

FD数量监控原理

class FdCountMonitor {
    private static final int FD_WARN_THRESHOLD = 900; // 90%阈值
    
    public static void start() {
        Executors.newSingleThreadScheduledExecutor()
            .scheduleAtFixedRate(() -> {
                int fdCount = getFdCount();
                if (fdCount > FD_WARN_THRESHOLD) {
                    // 记录FD详情
                    Map<String, Integer> fdStats = getFdStats();
                    reportFdOverflow(fdCount, fdStats, getOpenFilesStack());
                    
                    // 自动处理
                    autoHandleFdOverflow();
                }
            }, 0, 5, TimeUnit.MINUTES); // 每5分钟检测
    }
    
    private static int getFdCount() {
        File fdDir = new File("/proc/self/fd");
        return fdDir.listFiles().length;
    }
    
    private static Map<String, Integer> getFdStats() {
        File fdDir = new File("/proc/self/fd");
        Map<String, Integer> stats = new HashMap<>();
        for (File fd : fdDir.listFiles()) {
            try {
                String link = Os.readlink(fd.getAbsolutePath());
                String type = "other";
                if (link.contains("socket")) type = "socket";
                else if (link.contains("anon_inode")) type = "pipe";
                else if (link.contains("/")) type = "file";
                stats.put(type, stats.getOrDefault(type, 0) + 1);
            } catch (Exception e) {
                // 忽略读取错误
            }
        }
        return stats;
    }
}

6.内存工具的使用

6.1 LeakCanary (实时内存泄漏检测之王)

  • 定位:  自动化检测Activity/Fragment泄漏的明星工具。
  • 原理:  监听对象生命周期 -> 对象应被回收时主动GC -> 检测是否仍被引用 -> Dump Hprof -> 分析引用链。
  • 优点:  接入简单,报告清晰(直接指出泄漏引用链)。
  • 局限:  主要针对Android组件;线上需定制;分析Hprof较耗时。

6.2 Profiler(实时内存总览分析)

  • 定位:  强大的实时性能剖析工具,分析Java/Native内存、CPU、Network。

  • 内存分析功能:

    • 实时查看Java/Native堆分配、对象数量。
    • Memory Recorder:  捕获内存分配,查看对象创建堆栈(定位抖动利器)。
    • Heap Dump:  捕获堆快照,查看对象实例、引用关系(分析泄漏/大对象)。
    • Native Memory Profiling (Perfetto):  分析Native内存分配。
  • 优点:  官方工具,集成度高,功能全面,可视化好。

  • 局限:  主要用于线下调试,实时性对线上问题帮助有限

使用指南:
  1. 启动监控

    • Android Studio → View → Tool Windows → Profiler
    • 选择进程 → Memory 选项卡
  2. 捕获堆转储

  3. 分配追踪(定位抖动)

    • 操作步骤:

      1. 点击 Record allocations
      2. 操作App复现问题
      3. 点击 Stop recording
    • 分析技巧:

      • 按 Call Tree 查看调用栈
      • 筛选 Top Allocations
  4. Native内存分析(Android 8.0+)

6.3 KOOM (线上内存监控利器: 也是功能最强大的)

包含:json和内存快照Hprof

  • 定位:  快手开源的高性能线上APM监控解决方案,重点解决OOM问题。

  • 核心能力:

    • Java Leak:  类似LeakCanary线上版,低开销监控Activity泄漏。
    • Native Leak:  Hook malloc/free等,监控Native内存泄漏。
    • Thread Leak:  监控线程泄漏。
    • FD Leak:  监控文件描述符泄漏。
    • Fast Hprof Dump:  优化Hprof Dump速度和体积,适合线上。
  • 优点:  为线上场景深度优化,监控维度多,性能开销相对可控。

  • 局限:  接入有一定复杂度,分析报告需要一定经验。

6.4 MAT (内存分析终极武器: 全局观,详细, (特别是可以比较2次内存))

6.4.1 MAT的基本使用

MAT.png

关键操作指南
  • 定位:  专业的堆转储文件(Hprof)深度分析工具。
    • 优点:  分析能力极其强大,是解决复杂内存问题的终极武器。
  • Histogram:基于类的角度分析, 用于看个数。累计&&泄漏
  • dominator_tree:基于实例的角度分析, 看大对象
  • MAT 核心功能:

    • Histogram:  按类统计实例数、浅堆/深堆大小(找大对象、数量异常对象)。
    • Dominator Tree:  展示支配关系,快速找到“支配”最多内存的对象(定位泄漏根源神器)。
    • Path To GC Roots:  查看对象到GC Root的引用链(分析为何无法回收)。
    • Leak Suspects Report:  自动分析报告潜在泄漏点。
    • Compare Snapshots:  比较两个Hprof差异(分析内存增长原因)。
  1. Histogram视图

    • 按Shallow Heap排序 → 找大对象
    • 按Retained Heap排序 → 找内存黑洞

. - Class Name:具体检索某一个类

  • Objects:某一个具体的Class有多少实例

     // 可疑对象特征:
     android.graphics.Bitmap   Retained: 50MB
     byte[]                    Retained: 120MB
    
  1. Dominator Tree视图

    • 展示对象支配关系
    • 识别"内存吞噬者"
    • 作用:查找支配内存最大的对象
    • 案例:发现Bitmap占用500MB
    [Retained: 84MB] com.example.ProductDetailActivity[Retained: 50MB] android.graphics.Bitmap[Retained: 30MB] com.example.data.ProductModel
    
  2. 引用链分析

    • 右键对象 → Path to GC Roots
    • 排除弱/软引用(仅看强引用)
    GlobalCache.INSTANCE (static)
      ↳ ArrayList<ProductDetailActivity>
         ↳ [0] ProductDetailActivity instance
    
  3. 对比堆快照

    • 操作前Dump Hprof A

    • 操作后Dump Hprof B

    • MAT中执行:Tools → Compare Heap Dumps

    • 分析对象增量:

      + com.example.CacheManager   +120 instances
      + android.graphics.Bitmap     +45 instances (Total +86MB)
      
  • 操作:

    1. File → Open Heap Dump (第一次)
    2. Navigation → Compare to Another Heap Dump

分析:

        Objects Delta: +1,284
        Shallow Heap: +5.2MB
        Retained Heap: +24.7MB

5. 高级技巧:OQL查询

// 查找尺寸超标的Bitmap
SELECT * FROM android.graphics.Bitmap 
WHERE width * height * 4 > 1024 * 1024 * 10 // 大于10MB的Bitmap

// 查找重复字符串
SELECT s, COUNT(s) AS cnt 
FROM java.lang.String s 
GROUP BY s.toString() 
HAVING cnt > 50
高阶技巧:
  • 分析线程栈内存

    1. 在Histogram中输入java.lang.Thread
    2. 计算总内存:线程数 * (stackSize + 对象大小)
  • 定位资源泄漏

    // 查找未关闭的Closeable
    SELECT * FROM java.io.Closeable 
    WHERE closeable.@retainedHeapSize > 0
    
    

6.4.2 MAT的精华部分

分析步骤

1.排序,得到内存比较大的对象,bitmap,byte,Array (排序) 即内存里占用内存最多的对象列表

2.查看里面的单个 右击->list objects->with incoming references

3.单个的引用关系 :Path to GC Roots-> exclude xxx references

其他: 得到图片的大小,计算内存大小,得到图片的ID

大小: 8294416 B,是多少M

通过ImageView的ID(如图)及build目录下的R.txt反查可知该ImageView的ID名称

6.4.3 MAT分析内存泄漏 (Histogram)

用Histogram分析

Histogram分析内存泄漏,一看就可以看出,看Activity的实例个数

快速定位 Activity 泄漏
  1. 打开 Histogram
  2. 搜索 Activity
  3. 检查实例数是否异常(正常情况:当前活动 Activity 1-2 个)
  4. 对多余实例执行 Merge Shortest Paths to GC Roots

6.4.4 MAT分析综合分析内存(Dominator & Histogram)

6.4.4.1 . Dominator Tree 排序

步骤 1:定位大对象入口

  1. 打开 Dominator Tree 视图(支配树)
  2. 按 Retained Heap 降序排序
  3. 关注顶部对象(通常占用超过 1MB 内存=1024*1024=1232896)

步骤 2:识别大对象类型

对象类型典型特征常见场景
byte[]纯数据存储文件缓存、网络响应
Object[]数组结构大型集合底层存储
HashMap键值对集合缓存系统
String超大文本JSON/XML 数据
自定义对象复杂结构游戏场景、3D 模型
  1. 右键大对象 → Merge Shortest Paths to GC Roots → exclude weak references

  2. 分析引用链:

    • 静态变量持有
    • 线程栈引用
    • 全局缓存系统
6.4.4.2 . Bitmap 专项分析 (Dominator)
-- 查找所有 Bitmap 对象
SELECT * FROM java.lang.Object 
WHERE toString() MATCHES ".*Bitmap.*"
  • 右键 Bitmap → Show Retained Set 查看持有者
6.4.4.3 MAT对于数组和集合 专项分析
-- 查找大于 1MB 的 byte 数组
SELECT * FROM byte[] s WHERE s.@retainedHeapSize > 1048576

-- 查找元素超过 1000 的集合
SELECT * FROM java.util.ArrayList WHERE size > 1000
SELECT * FROM java.util.HashMap WHERE size > 1000
6.4.4.4 . MAT对于小对象堆积 专项分析(Histogram------高难度------)

关键分析步骤

步骤 1:识别高密度对象区域
  1. 打开 Histogram 视图

  2. 添加筛选器:Retained Heap < 1,000 (小于1KB的对象)

  3. 按 Objects 降序排序

  4. 关注:

    • 对象数量 > 1000 的类
    • 多个相关类的组合对象数
步骤 2:包级分组分析
-- OQL 包级对象统计
SELECT pkg.name, COUNT(*), SUM(SIZE_OF(o))
FROM OBJECTS (
    SELECT o
    FROM INSTANCEOF java.lang.Object o
    WHERE o.@retainedHeapSize < 1000
) 
GROUP BY DOMINATE_SORT(pkg.name)
ORDER BY COUNT(*) DESC
步骤 3:支配树累积点定位
  1. 打开 Dominator Tree

  2. 按 Retained Heap 降序

  3. 寻找:

    • 持有大量小对象的容器
    • 支配多个小对象类的父对象
    • Accumulated Point 标记的对象

案例:京东电商应用内存增长 (没有清空购物车)

现象:内存缓慢增长,无主导大对象
分析过程

  1. Histogram 筛选小对象:

    com.ecommerce.CartItem    : 12,400 objects (8.7MB)
    com.ecommerce.ProductTag  : 8,700 objects (3.1MB)
    com.ecommerce.UserHistory : 7,800 objects (7.8MB)
    com.ecommerce.PriceRule   : 5,600 objects (2.2MB)
    
  2. 包级分析:

    SELECT * FROM com.ecommerce.cart..*   -- 35,000 objects (23MB)
    SELECT * FROM com.ecommerce.user..*   -- 28,000 objects (18MB)
    
  3. 支配树发现:

    ShoppingCartActivity (支配树占比 45%)
      ↳ CartFragment
         ↳ RecyclerView.Adapter
            ↳ 持有 12,400 CartItem
    

工具链综合对比

特性LeakCanaryProfilerKOOMMAT
使用场景开发/测试阶段开发阶段线上生产环境深度分析
检测泄漏✅ 自动⚠️ 手动✅ 自动✅ 需人工分析
内存抖动✅ 分配追踪✅ 波动检测⚠️ 间接分析
OOM分析⚠️ 需手动触发✅ 自动捕获✅ 详细分析
Native内存✅ (Perfetto)✅ 全面监控
性能开销中等(GC时)高(录制时)低(<3% CPU)无(离线)
学习曲线简单中等中等陡峭

7.深度内存优化

关于应用内存分析,需要重点关注四个阶段

  • App 停留在闪屏页面内存固定值
  • App 的 闪屏 到 主页面 的内存波动值
  • App 运行十分钟后回归到 主页面 内存波动值
  • App 内存使用量分布值

7.1 MAT内存静态分析

7.1 内存静态分析:解剖单次堆转储

关键操作指南

1. Histogram 深度分析
// OQL 查找大对象示例
SELECT * 
FROM java.util.HashMap 
WHERE @retainedHeapSize > 10485760 // 10MB以上

// 按类统计
SELECT class, COUNT(*) AS instances, 
       SUM(OBJECTS.@retainedHeapSize) AS retained 
FROM OBJECTS java.lang.Object 
GROUP BY class 
ORDER BY retained DESC

分析技巧

  • 按 Retained Heap 排序找内存黑洞
  • 右键类名 → List objects → with outgoing references 查看对象详情
  • 使用 Regex 过滤包名:.*com.example.app.*
2. Dominator Tree 实战

操作流程

  1. 打开 Dominator Tree

  2. 按 Retained Heap 降序排序

  3. 展开可疑对象:

    • 检查 java.lang.Class 关联的类加载器
    • 查看 android.app.Activity 实例
  4. 右键 → Path to GC Roots → exclude weak references

3. Leak Suspects 报告解读

典型报告结构

1. Problem Suspect 1
   - 45 instances of "com.example.MyActivity"
   - Retained: 84MB
   - Accumulated: 120MB
   
2. Problem Suspect 2
   - 128 instances of "byte[]" 
   - Retained: 56MB

验证步骤

  1. 点击 Details 查看引用链
  2. 检查 Shortest Paths to Accumulation Point
  3. 确认是否被 GC Root 持有

7.2 内存动态分析:追踪对象生命周期

实战案例:分析RecyclerView内存增长

步骤1:捕获基准快照
  1. 打开页面 → 滚动到顶部
  2. 触发GC → 捕获 Heap Dump A
步骤2:执行操作
  1. 滚动到底部再回到顶部(20次)
  2. 触发GC → 捕获 Heap Dump B
步骤3:MAT对比分析
// 查询新增对象
SELECT * 
FROM OBJECTS com.example.adapter.ItemModel 
WHERE ${snapshot} = 'B'
  AND OBJECTS NOT IN (
    SELECT * FROM OBJECTS com.example.adapter.ItemModel 
    WHERE ${snapshot} = 'A'
  )
  
// 比较对象数量变化
SELECT class, 
       (COUNT(*) - COUNT(OBJECTS OF ${baseline})) AS delta 
FROM OBJECTS java.lang.Object 
GROUP BY class 
ORDER BY delta DESC

关键发现

  • ItemModel 对象新增 120 个
  • Bitmap 对象增加 15 个(未复用)
  • ViewHolder 未被回收(泄漏)

7.3 MAT内存比较分析2个Hprof文件:精准定位增长点

dump出2个页面的内存快照文件,然后利用MAT的对比功能,找出每个页面相对于上个页面内存里主要增加了哪些东西,做针对性优化

同时MAT支持compare对比功能,将两个.hprof文件导入,都Add to Compare Basket之后即可进行对比,这对于对比某个页面相较与前一页面的内存增量来说是非常有意义的。

具体步骤如下:

1).通过profiler,dump出hprofiy文件。然后进行转换

2).用MAT打开,看下饼状图,一个内存的整体分布

3).看直方图  Dominator Tree(即内存里占用内存最多的对象列表)Dominator 英文:支配者

Shallow Heap:对象本身占用内存的大小,不包含其引用的对象内存。

Retained Heap:包含自己和引用对象的内存

4).我们通过List objects->with incoming references查看

(这里with incoming references表示查看谁引用了这个对象,with outgoing references表示这个对象引用了谁)

主要看这个byte数组【】

右击其中一个数组->Path to GC Roots-> exclude xxx references

比较分析双模式

模式适用场景操作路径
堆转储对比分析两个时间点内存变化Navigation → Compare to Another Heap Dump
对象集对比对比特定对象集合差异Tools → Compare Object Sets

报告解读要点

  • + Objects:新增对象数量(警惕持续增长)
  • + Shallow Heap:对象自身内存增量
  • + Retained Heap:对象关联内存增量(关键指标)

对象集对比实战

场景:定位Fragment泄漏
  1. 在 Histogram 中过滤 androidx.fragment.app.Fragment
  2. 右键 → Compare to Another Heap Dump
  3. 选择泄漏后的堆转储

分析结果

Fragment实例变化:+3
Retained Heap增加:+15.7MB
引用链差异:Fragment → HostActivity已被销毁

7.4 MAT分析线上Hprof文件的难点

可以把内存泄漏解决 图片: 并不知道是哪个页面,所以不知道图片是否存在,是否合理!

就必现像KOOM一样,要拿到Json同时和Hrpof才能分析

8. 综合分析内存的案例和具体操作步骤

8.1 需求详细描述

1).在小红书的瀑布流中,RecyclerView加载图片,累计很多对象List集合

2).点击RecyclerView的item播放视频, 视频太大,加载内存比较大

3).退出页面,handler导致内存泄漏!

4).退出的时候,底图没有主动释放,导致图片泄漏

存在4个问题:大内存的视频,图片没有释放, Handler内存泄漏,recycler滚动,不停加载List,内存不断增加,写一个案例

8.2 源码

public class WaterfallActivity extends AppCompatActivity {
    // 问题1: 非静态Handler导致泄漏
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 更新UI...
        }
    };

    // 问题2: 全局缓存持有Context
    private static final List<ItemData> sGlobalCache = new ArrayList<>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_waterfall);
        
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        WaterfallAdapter adapter = new WaterfallAdapter();
        recyclerView.setAdapter(adapter);
        
        // 问题3: 加载大量数据
        loadData(adapter);
        
        // 发送延迟消息(可能泄漏)
        mHandler.sendEmptyMessageDelayed(0, 60000);
    }
    
    private void loadData(WaterfallAdapter adapter) {
        List<ItemData> data = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            // 问题4: 每次创建新对象
            data.add(new ItemData("Item " + i, getRandomImageUrl()));
        }
        adapter.setData(data);
        
        // 添加到全局缓存(错误!)
        sGlobalCache.addAll(data);
    }
    
    // 问题5: 没有资源释放
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 缺少:mHandler.removeCallbacksAndMessages(null);
    }
}

class WaterfallAdapter extends RecyclerView.Adapter<ViewHolder> {
    private List<ItemData> mData = new ArrayList<>();
    
    // 问题6: 图片资源未释放
    public void setData(List<ItemData> data) {
        mData = data;
        notifyDataSetChanged();
    }
    
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        ItemData item = mData.get(position);
        holder.title.setText(item.title);
        
        // 问题7: 直接加载大图
        Glide.with(holder.itemView)
             .load(item.imageUrl)
             .into(holder.image);
        
        holder.itemView.setOnClickListener(v -> {
            // 问题8: 直接播放高清视频
            playVideo(item.videoUrl); // 可能加载4K视频
        });
    }
    
    private void playVideo(String url) {
        VideoView videoView = new VideoView(context);
        videoView.setVideoPath(url); // 可能触发OOM
        
        // 缺少:视频播放结束释放资源
    }
}

// 问题9: 数据模型设计过大
class ItemData {
    String title;
    String imageUrl;
    String videoUrl;
    Bitmap cachedBitmap; // 危险!
    // ...其他冗余字段
}

案例.png

8.3 内存分析综合案例:小红书瀑布流加载图片和视频(重点)

LeakCarany+Koom+MAT+ Profiler

adb shell dumpsys meminfo

8.3.1 问题根源:

  1. 图片泄漏:小红书退出页面时未释放RecyclerView中的图片资源
  2. 视频大内存:直接加载高清视频导致Native内存溢出
  3. Handler泄漏:非静态Handler持有Activity引用
  4. 集合膨胀:瀑布流滚动时不断累积数据对象

8.3.2 分析思路

1). LeakCanary把泄漏扫描并解决

2).Koom把大图片,图片没有释放,大对象,线程,FD 监控并解决

3).MAT:把累计的小对象导致的集合,越来越大问题解决

解决方案.png

8.3.3 分析步骤

步骤1:LeakCanary解决泄漏问题

问题定位

  1. 退出Activity后LeakCanary报告:

    WaterfallActivity 泄漏
    GC Root: static WaterfallActivity.sGlobalCache
    -> ArrayList.elementData
    -> ItemData instance
    -> ItemData.this$0 (WaterfallActivity)
    
  2. Handler泄漏报告:

    Handler 持有 Activity
    

修复方案

// 修复Handler泄漏
private static class SafeHandler extends Handler {
    private final WeakReference<WaterfallActivity> mActivityRef;
    
    SafeHandler(WaterfallActivity activity) {
        mActivityRef = new WeakReference<>(activity);
    }
    
    @Override
    public void handleMessage(Message msg) {
        WaterfallActivity activity = mActivityRef.get();
        if (activity != null) {
            // 处理消息
        }
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    
    // 修复全局缓存泄漏
    sGlobalCache.clear();
}

步骤2:KOOM监控大图片和视频

KOOM监控到的问题

// KOOM报告1:大图片
{
  "type": "large_bitmap",
  "width": 4096,
  "height": 3072,
  "size": "47.7MB",
  "config": "ARGB_8888",
  "stack": "Glide.loadInto()"
}

// KOOM报告2:视频内存溢出
{
  "type": "native_leak",
  "size": "84MB",
  "stack": "MediaPlayer.setDataSource()"
}

修复方案

// 图片加载优化
Glide.with(holder.itemView)
     .load(item.imageUrl)
     .override(1080, 1920) // 限制尺寸
     .format(DecodeFormat.RGB_565) // 减少内存
     .into(holder.image);

// 视频播放优化
private void playVideo(String url) {
    // 1. 添加视频尺寸检测
    if (is4KVideo(url)) {
        showToast("设备不支持4K播放");
        return;
    }
    
    // 2. 使用TextureView替代SurfaceView
    TextureView textureView = new TextureView(context);
    
    // 3. 添加内存监控
    KOOM.addMonitor("video_player", textureView);
    
    // 4. 生命周期绑定
    textureView.setSurfaceTextureListener(new SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureDestroyed(SurfaceTexture surface) {
            releaseMediaPlayer(); // 确保释放
        }
    });
}

// 5. 视频播放结束释放资源
private void releaseMediaPlayer() {
    if (mediaPlayer != null) {
        mediaPlayer.release();
        mediaPlayer = null;
    }
}

步骤3:MAT分析集合累计对象导致的内存问题

分析流程

image.png 代码

MAT操作步骤

  1. 在Histogram视图中搜索 com.example.ItemData
  2. 右键 → List objects → with outgoing references
  3. 查看对象数量(预期:1000个)
  4. 计算Retained Heap大小(预期:50MB+)
  5. 右键 → Path to GC Roots → exclude weak references
  6. 发现被 WaterfallActivity.sGlobalCache 持有

OQL分析

// 查找大集合
SELECT * 
FROM java.util.ArrayList 
WHERE object.@retainedHeapSize > 10485760 // 10MB

// 查找重复数据
SELECT DISTINCT OBJECTS d.title 
FROM com.example.ItemData d

修复方案

// 数据模型瘦身
class ItemData {
    String title;
    String imageUrl;
    String videoUrl;
    // 移除cachedBitmap等冗余字段
}

// 分页加载
private void loadData(WaterfallAdapter adapter) {
    Pager pager = new Pager(20); // 每页20条
    adapter.setData(pager.nextPage());
    
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            if (isLastItemVisible()) {
                // 加载下一页
                adapter.appendData(pager.nextPage());
            }
        }
    });
}

// 使用弱引用缓存
private static final List<WeakReference<ItemData>> sWeakCache = new ArrayList<>();

步骤4:Profiler验证优化效果(不用这个,尽量用MAT)

关键指标对比

指标优化前优化后改善幅度
页面内存占用185MB68MB↓63%
退出后内存残留45MB2.4MB↓95%
图片内存峰值87MB32MB↓63%
视频加载成功率78%99%↑21%
OOM发生率23%0.5%↓98%

完整的流程.png

9.内存优化的一些基本措施

9.1 优化内存不同的手段
  1. 避免内存泄漏:

    • 使用Application Context代替Activity Context(除非必须)。
    • 使用WeakReference/SoftReference持有潜在的大对象或Context。
    • 及时解注册监听器(onDestroy/onPause)。
    • 谨慎使用静态变量,尤其避免持有View/Activity。
    • 使用静态内部类 + WeakReference 处理Handler/Runnable
    • 及时关闭资源(CursorInputStream/OutputStreamSQLiteDatabase)。
  2. 减少内存抖动:

    • 对象池化:  重用对象(RecyclerView.ViewHolder, 自定义对象池)。
    • 避免循环内创建对象:  如字符串拼接用StringBuilder;集合初始化预估大小。
    • View.onDraw/onMeasure优化:  提前创建并重用PaintPathMatrix等。
    • 选择合适的数据结构:  SparseArray代替HashMapArrayMap代替HashMap
  3. 预防内存溢出:

    • 图片优化:

      • 使用合适的inSampleSize加载图片(BitmapFactory.Options)。
      • 使用合适的Bitmap.Config(RGB_565代替ARGB_8888)。
      • 使用inBitmap重用Bitmap内存(Android 3.0+)。
      • 及时recycle()(API < 11)或依赖GC(API >= 11)。
      • 使用高效图片库(Glide, Picasso, Fresco)。
    • 资源管理:

      • 按需加载资源,及时释放大资源(如页面不可见时释放图片缓存)。
      • 使用内存缓存(LruCache并设置合理上限
      • 考虑磁盘缓存减轻内存压力。
    • 数据结构优化:  避免持有超大集合,及时清理无用数据。

    • Native内存管理:  确保JNI代码正确释放分配的内存(free/delete),及时释放MediaCodec、AudioTrack等资源。

9.2 优化内存的通用手段
  1. 图片:

    • 使用inSampleSize按需加载。
    • 优先RGB_565格式(非透明图)。
    • 利用inBitmap复用内存。
    • 使用高效图片库(Glide/Picasso/Fresco)。
    • 列表滑动时暂停加载(RecyclerView.OnScrollListener)。
  2. 数据与集合:

    • 预估ArrayList/HashMap初始大小。
    • 优先使用SparseArray/ArrayMap代替HashMap(Key为int/Object)。
    • 及时清理集合中无用数据。
  3. 对象与资源:

    • 重用对象:  ViewHolder模式、对象池(Pools)。
    • 避免高频创建:  onDraw/onMeasure内避免创建对象(重用PaintPath等)。
    • 释放资源:  CursorStreamReceiverSensorListenerLocationListener务必在onDestroy/onPause解注册或关闭。try-with-resources是好习惯。
  4. 组件与引用:

    • 优先使用Application Context
    • 静态变量谨慎持有View/Activity。考虑WeakReference/SoftReference
    • Handler使用静态内部类 + WeakReference,并在onDestroyremoveCallbacksAndMessages(null)
    • 避免非静态内部类(隐式持有外部类实例)。
  5. 缓存:

    • 使用LruCache(内存缓存)和DiskLruCache(磁盘缓存)。
    • 设置合理的缓存大小!  (基于设备内存动态计算)。
    • 实现缓存清理策略(基于时间、数量、大小、优先级)。
  6. 代码习惯:

    • 字符串拼接用StringBuilder
    • 循环内避免创建临时对象。
    • 注意匿名内部类、Lambda表达式捕获的外部变量。
    • 使用@NonNull/@Nullable注解辅助空指针检查,避免不必要的对象创建。

10. 内存总结;

第一步:计算,和优化指标对比, 为了看优化的效果如何  

第二步  3大内存杀手, 需要用工具分析才行

2.1)内存泄漏-------->看详细博客

2.2).  OOM------------>Bitmap: 分配及回收追踪   这样总体可用减少OOM,内存溢出的问题

OOM主要是大内存分析     静态内存分析和动态内存分析,主要是图片优化

2.3)内存抖动-------->看详细博客(线上一般监控不出来! )

第三步). 优化之后可以总结:

1>.减少内存

2>.复用内存

3>.回收内存

第四步: >最重要的: 预防和线上监控

5大监测:

1).监测泄漏

2).监测大图

3).监测GC事件

4).监控线程

5). IO监控

之前说的都是虚的,最重要的是线上监控。因为线下根本很多场景覆盖不到。

最主要解决:线上内存排名靠前的几个大问题就够了

项目源码的地址:github.com/pengcaihua1…