Android:彻底消灭 OOM 的实战经验分享(千分之 1-5---- 万分之 0-2)
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; ++i) {
final View child = root.getChildAt(i);
if (child instanceof ViewGroup) {
child.setBackground(null);
traverse((ViewGroup) child);
} else {
if (child != null) {
child.setBackground(null);
}
if (child instanceof ImageView) {
((ImageView) child).setImageDrawable(null);
} else if (child instanceof EditText) {
((EditText) child).cleanWatchers();
}
}
}}
我们在基类 BaseActivity 的 onDestory()方法中进行了一些资源和引用的清除
三、内存峰值太高
在我们把能 fix 的内存泄漏都盘了一便之后,上线一周并没有发现数据好转,OOM 率还是高居不下,于是乎,我们开始怀疑内存峰值太高的问题,在我们的项目中不仅仅只有 native 的部分模块,还有混合的 H5、RN 模块,当起一个 ReactActivity 的实例时,内存峰值总是涨的特别特别厉害,同时项目中有消息流的展现,其中会包含着大量的图片展示,这也是导致内存峰值太高的原因(Bitmap 对象太大以及太多)
我们又拿出了老伙伴 - Profiler,这可是分析 bitmap 对象的利器,可以直接看到大小、图片的预览,以及可以通过 go to instance 一层一层的找到到底是谁在引用它。比如下面这个例子,直接看引用就知道是被 Fresco 所引用了~ 直接就在 CountingMemoryCache 中。
其实我们主要还是需要去关注 Bitmap 对象的分配和不合法持有导致的内存峰值问题,如果一个 bitmap 对象有 3M,然后持有一个几十上百个在内存中,这谁吃得消,低端机器老早直接 OOM 了。
查 Bitmap 分配查出来的问题
目前我们项目中用的图片加载框架有两个,UIL、Fresco,UIL 我吐槽很久了,这么多年没更新,老早就该换了~?
1. UIL 加载图片在我们项目中的问题:
没有传入合适的 Config,绝大多数地方传的都是 ARGB_8888,其实根本没必要,改成 565 直接少一半内存占用
用 UIL 进行 loadImage 时,没有传入 targetSize,这就直接导致了 UIL 内部是以屏幕的尺寸去 Decode 的 Bitmap 对象,想象一下,一个特别小的头像 View,持有着一个屏幕大小尺寸的 Bitmap 对象,这谁顶得住。
许多地方不需要存内存缓存,比如闪屏广告图,app 启动之后就不会再使用了,可以加载的时候 memoryCache(false)
许多地方不需要磁盘缓存,比如发布动态,从图库中选图,不需要再存一份磁盘缓存了,本身那些图片都是本地图片。直接 diskCache(false)
2.Fresco 在 RN 页面中使用的问题,
通过看代码可以知道,RN 页面销毁的时候,连带着 Fresco 的内存缓存都会被清空,
直接上代码图:
代码看到这里,似乎 Fresco 不用担心了,既然会清空 Fresco 的内存缓存,何愁会引起内存峰值过高,如果读者看到这里,也有这个想法,那就大错特错了。话不多说,直接上图。
Fresco 相关源码的逻辑这篇文章就不分析了,主要讲思路,具体的源码分析后面我会用单独的篇幅去讲~?
为什么我会对 Fresco 的动图缓存这么敏感,那还是 Profiler 的功劳,我在用 Profiler 查看内存中 bitmap 的分配的时候,发现有上百张的 Loading 图没有销毁(我们 Loading 图是动图,大概每帧的 Bitmap 对象在 360K 左右), 且打开的页面越多,Loading 的 bitmap 就会越多。(这是因为我们每一个 RN 页面都会带一个 Loading 动画)
0.3M * 100 = 30M,不少了。。。,说实话有点恐怖
于是乎,干掉他们,这里用了反射,正常情况下不需要反射。直接拿 ImagePipelineFactory 中的对象来 clear 就好
public static void clearAnimationCache() {
if (frescoAnimationCache == null) {
//采用反射的方法,如果 native、rn 同时初始化 Fresco,会造成 Fresco 内部存储动图的 CountingMemoryCache 不是 Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache()了
//暂时用反射的方法,拿到存储动图缓存的 cache,并清空
try {
Class imagePipelineFactoryClz = Class.forName("com.facebook.imagepipeline.core.ImagePipelineFactory");
Field mAnimatedFactoryField = imagePipelineFactoryClz.getDeclaredField("mAnimatedFactory");
mAnimatedFactoryField.setAccessible(true);
AnimatedFactoryV2Impl animatedFactoryV2 = (AnimatedFactoryV2Impl) mAnimatedFactoryField.get(Fresco.getImagePipelineFactory());
Class animatedFactoryV2ImplClz = Class.forName("com.facebook.fresco.animation.factory.AnimatedFactoryV2Impl");
Field mBackingCacheField = animatedFactoryV2ImplClz.getDeclaredField("mBackingCache");
mBackingCacheField.setAccessible(true);
frescoAnimationCache = (CountingMemoryCache) mBackingCacheField.get(animatedFactoryV2);
} catch (Exception e) {
Log.e("FrescoUtil", e.getMessage(), e);
}
}
if (frescoAnimationCache != null) {
frescoAnimationCache.clear();
}
Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache().clear();Fresco.getImagePipelineFactory().getEncodedCountingMemoryCache().clear();}
又一个兜底方案
为了防止峰值过高,我们还起了一个线程,定时的去监控实时的内存使用情况,如果内存紧急了,直接清空 UIL/Fresco 的内存缓存救急
private static Handler lowMemoryMonitorHandler;private static final int MEMORY_MONITOR_INTERVAL = 1000 * 60;/**
开启低内存监测,
如果低内存了,作出相应的反应*/public static void startMonitorLowMemory() {HandlerThread thread = new HandlerThread("thread_monitor_low_memory");thread.start();lowMemoryMonitorHandler = new Handler(thread.getLooper());lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);}
/**
低内存时清空 Fresco、UIL 的内存缓存
如果已用内存达到了总的 80%时,就清空缓存*/private static Runnable releaseMemoryCacheRunner = new Runnable() {@Overridepublic void run() {long alreadyUsedSize = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();long maxSize = Runtime.getRuntime().maxMemory();if (Double.compare(alreadyUsedSize, maxSize * 0.8) == 1) {BitmapUtil.clearMemoryCaches();}lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);}};
五、特大图排查优化
我想大家都不会想到,在我们 app 的登录注册页,会有一个图片轮播控件,它轮播着五六张单张 6M+的 Bitmap。。。当然,特大图不仅限于此,还有其他地方会有相同情况,我们通过 Profiler 找出那些大的 bitmap 对象,然后预览之后确定是哪里在用的。
直接优化掉。最不济 8888 -> 565 就少一半内存占用
怎么讲呢,,OOM 这个东西,还没咋僵持呢,就没了。。
六、总结
深夜一时兴起想分享和记录一些什么,就随便写了这一篇博客,写的不详细,没有排版和良好的语言组织,单纯的就是想分享
总结一下吧,我们为了 fix OOM 所做的事情:
1. 检查内存泄漏,包括常见的 Context 泄漏、单例泄漏、EditText 的 TextWatcher 泄漏等等,找到并 fix 他们,最简单的例子,能传 application 的地方就不要硬传个 activity 过去 2. 兜底方案:
在 Activity onDestory 的时候,遍历 View 树,清空 backGround、Drawable、EditText 的 TextWatcher 等
3. 内存峰值的优化。内存泄漏会导致内存峰值,内存峰值是 OOM 的大锅,举个例子当可用内存不够分配一个 Bitmap 对象时,就会 OOM,Android 上大多数的内存峰值都是图片的加载带来的。现在许多的 app 中都有信息流的展现,可能会有许多的九宫格展示图片,且 Bitmap 对象本身就可以非常大。
优化 UIL 的使用
memoryCache 选用,不是所有的图片加载都需要 UIL 去塞一份内存缓存的,比如闪屏图
ImageLoader.getInstance().displayImage()的时候,传进去的 Option 不要无脑 ARGB_8888,讲道理来说,无脑 RGB_565 都是没啥问题的。。
调用 displayImage 的时候,最好传一个 ImageSize 作为 targetSize,这个 size 可以是你的 ImageView 的尺寸,当 View 尺寸本身不确定的时候,可以传一个大概值,比如我们 app 中有好些个的头像标准尺寸,为了偷懒,直接传 MaxAvatarSize 就 ok
Fresco 的优化
RN 中使用 Fresco 加载图片,在 RN Activity 销毁的时候,会将 Fresco 默认的 memory cache 清空,但是动图的缓存没有清。手动清一下。我们项目中每个 RN 页面都会带一个 Loading 动图,所以吃了大亏。。
**5. 持续的后台监控内存,**起一个 HandlerThread,一直在后台拿内存使用的状态,达到了危险警戒线就清空一把 UIL、Fresco 的 memory cache,先让世界安静一下 6. 需要对内存泄漏、OOM、Crash、ANR 进行监控
一些其他的细节暂时想不起来了,凌晨四点脑子不清醒了
后续关于这里面涉及到的 Fresco 的部分源码分析、Profiler 的最佳使用姿势(经过这一次的折腾,总结出来一句话,Profiler 真香)、以及前段时间在做的 App 的启动速度优化等等等等等都会单独拎文章去分享,后续也会带来更多,涉及的内容包括但不限于:
主流框架的一些设计思想的分享
工作项目中遇到的麻烦和坑
工作中蹚坑的一些经验
好代码
坏代码
坏的设计
程序员从头发浓密到成为下雨天报警员的心路历程
。。。
原文链接:https://juejin.im/post/5d617d6851882575e8054fab
好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
Android 架构师之路很漫长,一起共勉吧!
评论