这是我在掘金的第一篇博客分享,最近在掘金上看了许多大佬的文章,学到了很是多的东西,实在是忍不住想要把咱们平时工做中用到的一些优化方案分享出来,其实也是一个你们一块儿讨论学习的过程,但愿你们能够多多交流 ~ java
第一篇博客,总得介绍下本身~,有校友或者其余间接挨得着边的联系的能够私聊交流,前1/4 -> 1/3人生实在没啥交集的也能够眼熟一下。祖籍赣,天府磨子桥文理学院七年计算机,18年夏天毕业,目前在北京海淀768工做,「脉脉」平台客户端开发一枚。喜欢打游戏唱歌撸猫次好次的,其余的没了程序员
先简单讲讲跟oom纠结的历史吧。web
在18年年末,咱们app进行了一次很是大的版本更迭,由于时间紧急、业务繁忙、人数也没达到能够凑人数可让某些人准点下班的那种数量(各个公司的常规缘由),业务线在对一些模块进行重构和大量新需求的开发过程当中,许许多多的细节没有注意到,直接致使了后面一个月的崩溃率、OOM率猛增, 且居高不下。大概快到了千分之2的这个数量级,这是很是很是恐怖的。所以咱们花了一段时间,集中的fix了一把OOM的相关问题,一顿操做,直接让主版本的崩溃率来到了「万分之一」,OOM率来到了十万分之一这个数量级。缓存
不讲废话了,也不讲那些网上均可以查到的一些常规优化方法来填字数了,我会针对如何去fix OOM这个目标,将思考的历程以及解决问题的办法分享出来,但愿其中会有某一条经验正好击中大家,能起到一些帮助~~bash
开干!!下面的内容,我会用一级标题的字体~ 显眼一些哈哈,毕竟前面都是啰嗦的废话app
首先fix OOM第一件事确定是来排查内存泄漏。想要排查内存泄漏,那就第一步要对内存泄漏进行监控、上报。框架
样例代码以下:dom
public static class LeakReportService extends DisplayLeakService {
@SuppressWarnings("ThrowableNotThrown")
@Override
protected void afterDefaultHandling(@NonNull HeapDump heapDump, @NonNull AnalysisResult result, @NonNull String leakInfo) {
if (!result.leakFound || result.excludedLeak) {
return;
}
try {
Exception exception = new Exception("Memory Leak from LeakCanary");
exception.setStackTrace(result.leakTraceAsFakeException().getStackTrace());
Sentry.capture(exception);
} catch (Exception e) {
e.printStackTrace();
}
}
}复制代码
当内存泄漏上报到sentry上面以后,咱们直接观察是哪里泄漏的就行了。经过sentry进行监控以后,项目里面的大部份内存泄漏无处可逃~ ,内存泄漏比较简单,我就不花大量篇幅去赘述了~,我本身看文章的过程当中,最讨厌篇幅太长。。。ide
profiler工具的使用方法我就不赘述了吧,讲一下小技巧吧。工具
在排查bitmap对象,咱们能够用Profiler直接看java 堆中的bitmap对象图片的预览~ 这样能够直接定位到是哪里泄漏了以及哪里bitmap加载过大
方法:找到对应的Bitmap对象,而后~ ,点击它,而后就能够preview,以下图:
复制代码
咱们能够知道的是,当一个Activity的生命周期要走完了,那就说明咱们绝大几率不会再使用这个Activity对象了,所以彻底能够对他的可能致使整个Activity泄露的引用进行清空,将其中的一些资源释放干净,好比有EditText的TextWatcher,这是很是容易泄露且在咱们项目中大量出现的一个case,而后,因而乎咱们加上了更加丧心病狂的兜底策略,
话很少说,直接上代码
private void traverse(ViewGroup root) {
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的部分模块,还有混合的H五、RN模块,当起一个ReactActivity的实例时,内存峰值老是涨的特别特别厉害,同时项目中有消息流的展示,其中会包含着大量的图片展现,这也是致使内存峰值过高的缘由(Bitmap对象太大以及太多)
咱们又拿出了老伙伴 - Profiler,这但是分析bitmap对象的利器,能够直接看到大小、图片的预览,以及能够经过 go to instance一层一层的找到究竟是谁在引用它。好比下面这个例子,直接看引用就知道是被Fresco所引用了~ 直接就在CountingMemoryCache中。
其实咱们主要仍是须要去关注Bitmap对象的分配和不合法持有致使的内存峰值问题,若是一个bitmap对象有3M,而后持有一个几十上百个在内存中,这谁吃得消,低端机器老早直接OOM了。
目前咱们项目中用的图片加载框架有两个,UIL、Fresco,UIL我吐槽好久了,这么多年没更新,老早就该换了~
1. UIL加载图片在咱们项目中的问题:
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() {
@Override
public 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所作的事情:
一些其余的细节暂时想不起来了,凌晨四点脑子不清醒了
后续关于这里面涉及到的Fresco的部分源码分析、Profiler的最佳使用姿式(通过这一次的折腾,总结出来一句话,Profiler真香)、以及前段时间在作的App的启动速度优化等等等等等都会单独拎文章去分享,后续也会带来更多,涉及的内容包括但不限于:
个人简书 邹啊涛涛涛的简书
个人CSDN 邹啊涛涛涛的CSDN
个人掘金 邹啊涛涛涛的掘金