Android - 彻底消灭OOM的实战经验分享(千分之1

不讲废话了,也不讲那些网上都可以查到的一些常规优化方法来填字数了,我会针对如何去fix OOM这个目标,将思考的历程以及解决问题的办法分享出来,希望其中会有某一条经验正好击中你们,能起到一些帮助~~

开干!!下面的内容,我会用一级标题的字体~ 显眼一些哈哈,毕竟前面都是啰嗦的废话

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一、排查内存泄漏

首先fix OOM第一件事肯定是来排查内存泄漏。想要排查内存泄漏,那就第一步要对内存泄漏进行监控、上报。

我们采用了LeakCanary,实现了一个自定义的Service继承自DisplayLeakService,重写afterDefaultHandling方法,将内存泄漏上报到Sentry。

样例代码如下:

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进行监控之后,项目里面的大部分内存泄漏无处可逃~ ,内存泄漏比较简单,我就不花大量篇幅去赘述了~,我自己看文章的过程中,最讨厌篇幅太长。。。

除了LeakCanary,我们还使用了Android Studio自带的Profiler工具对内存有进行分析,包括内存泄漏的问题和内存峰值过高的问题。

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的部分模块,还有混合的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() {
    @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);
    }
    };
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。

后面我就自己整理了一套资料,还别说,真香!

资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。

系列教程图片

2020Android复习资料汇总.png

flutter

NDK

设计思想开源框架

微信小程序

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

(img-7AOy4UYo-1713072564826)]

[外链图片转存中…(img-HHPB2A7Z-1713072564826)]

[外链图片转存中…(img-fqfddcGA-1713072564826)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值