记录一次 OOM 问题

1,649 阅读5分钟

记录一次 OOM 问题

本次的 OOM 问题分析是通过 Profiler dump Java 堆内存来分析,废话少说直接开始。

Bitmap 问题

bitmap.png

我们看到这里的 BitmapNative 内存都占用了 93MB,有 284 个实例,我这里要说明一下在 Android 8 及其以上的版本,Bitmap 的内存占用都移动到了 Native,减少了 JVM 内存的压力,反之在 Android 8 以下的版本中由于 Bitmap 的内存是直接分配在 JVM 堆上,很大的程度上增加了 JVM OOM 的风险。好了继续分析主要是哪部分的 Bitmap 占用内存过大。

超大的资源文件图片

big_bitmap.png

这里发现一个超大的资源文件背景图,占用内存 4MB,它是用在登陆页面的背景,别的地方倒是没有用到。我曾经还遇到过比这个图更大的,直接在系统加载资源图片的时候就直接崩溃了😂。

  • 解决方案1
    UI 给的背景图分辨率比较大,可以考虑降低它的分辨率。

  • 解决方案2
    Android 系统中的资源图片是全局通用的,当第一次加载成功后后续别的地方再加载就直接用这个缓存,当然你这个资源文件非常大而且只有这一个地方使用是可以考虑使用完了后就不再缓存,手动把它直接回收掉,节约内存。

SVGA 动画产生大量 Bitmap

我这里简单介绍一下 SVGA 动画,它的原理其实就是和 Android 中的帧动画一样的,就是由一帧一帧的 Bitmap 构成,所以大家都说 Android 帧动画要谨慎使用,主要也是内存问题,同样的 SVGA 也要谨慎使用,在我们公司的 1v1 陌生人社交的项目有设计用户的动态头像框,当在聊天列表的页面就会显示每个用户头像和动态头像框,这个动态头像框就是用的 SVGA 动画,而且同时播放的动画可不止一个动态头像框,而是列表中所有可见用户的动态头像框,它不仅是有造成 OOM 的风险,而且会频繁创建和回收 Bitmap,让内存占用成锯齿状,可谓是罪行累累。

svga_bitmap.png

SVGAVideoEntity 对象中持有了这些 Bitmap 的引用,而且实例的数量达到了 95 个。

  • 解决方案1
    最好的方案当然是弃用 SVGA 动画,换其他通过 CPU 动态计算的动画,减小内存的占用。

  • 解决方案2 最大程度减小 SVGA 动画的分辨率,反正都是头像框显示的区域比较小。

  • 解决方案3 在 Android 8 以下的设备上或者低端设备上禁止使用 SVGA 动画。

Fresco 加载的网络图片

我自己没怎么使用过 Fresco 加载网络图片,用 GlidePicasso 要多一点,通过 dump 内存的记录我发现它会把 Bitmap 的引用都保存在一个静态变量中。

fresco_bitmap.png

这里持有了 172 个 Bitmap 的实例。

上面 ClasssLiveObjects 就是以下的静态对象:

fresco_alive.png

我没有了解过 Fresco 管理内存的方式,一个页面中肯定用不了 172 个 Bitmap,我认为这里是可以优化的,可以回收部分 Bitmap,而且在 Fresco 加载的网络图片中有不少的内存占用都是超过了 1MB。
网络图片和动画就占用了 267 个 Bitmap 实例,总共才 284 个实例。

  • 解决方案1
    改用 Glide 图片加载,同时配置保守的内存缓存机制。

  • 解决方案2
    根据不同的手机性能选择加载不同分辨率的网络图片。

内存泄漏

mem_leak.png

这个泄漏的 IMMessageListActivity 就是 IM 的聊天列表 Activity,它还是非常重的,其中引用了不少的高内存占用对象,但是它发生了泄漏,它的泄漏是由于做了一个异步操作,然后这个异步的 Lambda 中还对这个 Activity 有引用,而这个异步的 Lambda 对象通过强引用保存在一个静态的 HashMap 中,当 Activity 销毁后,对应的对象也没有办法得到回收,因为 Activity 被静态的 HashMap 强引用,这样就导致了泄漏。

  • 解决方案
    静态的 HashMap 不要以强引用的方式保存 Callback 对象,而使用弱引用。

字符串内存占用

str.png

我们大致看 char[]String 来计算字符串内存占用,大概在 30MB。

SVGA 动画描述文件

svga_str.png

SVGA 真的是,每次扫黄都有你😂。动画的描述文件占用的内存大小也不少,我没有具体统计,但是至少也是几 MB 的内存占用。

IM 对话

conversation_str.png

最终 String 对象的引用最后找到了 ConversationEntity 中的 rawString 的变量中,ConversationEntity 最终保存在静态对象中,而且永远不会回收😂。ConversationEntity 有 9125 个实例,我按一个实例平均占用 1.5KB 内存,光 ConversationEntity 就占用 13MB 的内存(我感觉实际是大于这个值的),其实我觉得大部分的 String 的内存都是 IM 和 SVGA 占用的。

为什么有这么多 ConversationEntity 对象呢?这个是在外部的 IM 列表页面中就把对应的 ConversationEntity 就加载出来了,讲道理我们应该是在二级的聊天页面中才加载,而且退出聊天页面后这个数据就该释放,但是由于历史原因没人敢动这部分代码,产品也是无脑的堆需求,压根儿就不会有人管这个,也就是常说的 Shit Mountain,我相信不只是我们的公司有这样的问题。

最后

根据我们的教训,希望大家也不要踩这样的坑,如果已经在坑中了,那就想想如何爬上来吧。