Android 线上 OOM 的排查过程

作者:王晨彦

开篇

一天,后台统计到线上有大量 OOM 崩溃,小王收到老板的紧急指令,立即排查!

小王心想,这还不简单,待我看看崩溃堆栈,分分钟解决。

于是小王不慌不忙的打开崩溃后台,一看傻眼了,同样的 OOM,却有几十种不同的堆栈,大到创建 View,小到 new 一个 String

小王差点骂了出来:这 OOM 不讲武德啊!

骂完之后,还是得解决问题啊,否则怎么面对老板啊

心路历程

正郁闷着,小王突然想起曾经看过性能优化的文章,里面介绍了 Android Studio 中集成的 Profiler 可以分析 APP 内存

既然堆栈看不出什么问题,那就只能照着文章的方法,碰碰运气了

于是小王点开了 IDE 底部那个毫不起眼的「Profiler」面板,映入眼帘的是

小王一眼就看到了 MEMORY 栏,这不就是内存使用嘛

嗯,数据倒是挺全,可是怎么知道哪里导致 OOM 了啊,小王又开始怀疑人生了…

“放着不动肯定看不出什么啊,内存是动态申请的嘛”

小王心想,既然这么多 OOM,那么肯定是 APP 内的常用页面导致的,于是小王开始一边来回切换常用页面,一边观察内存走势

经过多次尝试,小王发现应用的内存占用确实在不断升高,即使手动 GC 之后,仍然居高不下

小王想起面试宝典中「无法被 GC 回收的对象,会导致内存泄露」,于是手动点了下 GC,避免数据不准确

Java 堆从 15.7MB 涨到 19.3MB,好像问题不大,而 Native 就离谱了,好家伙,竟然从 56.1MB 涨到了 97.5MB,分分钟就涨了 40MB+

小王喜出望外,终于发现内存问题了!看来平时摸鱼的时候多看看文章真是没坏处啊

可是,就算知道内存不正常,但还是不能定位是哪段代码导致了…

小王平复了一下心情,继续观察规律,终于发现,每次从A页面跳转出去,内存都会增加几M,而且 GC 无法回收,那肯定是这个页面有问题了!

于是小王骂骂咧咧的开始阅读这个页面的代码,希望能够发现内存泄露的元凶。心里嘀咕着,让我看看是哪个 ** 写出了内存泄露的代码

小王逐字逐句看完了代码:可是并没有什么问题啊,就是一个普通的列表页,还是用 RecyclerView 实现的,没啥毛病啊

这下又把小王难住了,小王心想,不能在黎明前倒下啊,于是又想起文章中关于 Profiler 的介绍,可以使用 Dump 功能方便的查看当前的内存快照,兴许能发现什么端倪呢

好家伙,原来是 Bitmap 占了这么大内存,于是小王又想起面试宝典

Android 2.3.3(API level 10) 和更早的版本,Bitmap 对象和对象里对应的像素数据是分开存储的,Bitmap 存在虚拟机的堆里,而像素数据存储在 Native 内存里。

从 Android 3.0(API level 11) 到 Android 7.1(API level 25),Bitmap 对象及其像素数据都存储在虚拟机的堆里。

从 Android 8.0(API level 26) 开始,Bitmap 对象存储在虚拟机的堆里,而对应的像素数据存储在 Native 堆里。

小王测试的手机是 Android 10,Bitmap 数据存储在 Native 堆,所以基本上可以确定就是 Bitmap 导致内存泄露了。虽然又前进了一大步,但还是找不到原因。

小王发现,点击对象,可以查看所有实例的引用链,这下可把小王高兴坏了,而且小王还发现了一个非常可疑的引用链

这不是 CoilMemory Cache 嘛,可是这里明明是有缓存的嘛,怎么还会泄露,难不成是这个开源库有 bug?

小王怀着忐忑的心情打开了 RealMemoryCache 这个类

这不就是一个基于 LRU 实现的内存缓存嘛,乍一看好像没什么毛病

没时间仔细研究了,小王心想,先看看开源社区有没有人反馈过这个问题,小王过滤了一下包含 “memory leak” 关键字的 issue

果然有一个 PR 的标题非常接近 Fix memory leak if request is started on detached view.

看起来问题已经被修复且已经发布了新版本,于是小王立马升级版本再次测试,果然没有泄露了

于是立马提交代码,兴冲冲的去找老板炫耀了!!!

追根溯源

回过头来,小王心想,作为一个“有上进心”的程序员,我得看看是什么原因导致的泄露啊

于是再次打开 PR,在诸多改动中,终于找到一个真正的代码改动,其他都是测试用例

小王不禁感慨,歪果仁就是专业呀,改了两行代码就要写一堆测试用例

小王终于弄清了导致泄露的原因,原来是在快速切换页面时,有时页面已经销毁了,才开始加载图片,此时 Coil 会把这个 View 对象保存起来,等待 View detach 的时候释放,然而此时 View 已经是 detach 的状态了,因此永远不会被释放了,而 Bitmap 又被 View 持有,而我们都知道 Bitmap 是内存占用大户,因此就出现了上面 Bitmap 占用大量内存的情况。

而解决方案就是再判断一下 View 是否已经 Detach,是的话就直接释放了,避免造成泄露。

猜你喜欢

转载自blog.csdn.net/ajsliu1233/article/details/124271981