本篇是 Android 内存优化的进阶篇,难度可以说达到了炼狱级别,建议对内存优化不是非常熟悉的仔细看看前篇文章: Android性能优化之内存优化,其中详细分析了以下几大模块:
如果你对以上基础内容都比较了解了,那么我们便开始 Android 内存优化的探索之旅吧。
本篇文章非常长,建议收藏后慢慢享用~
Android给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?那是因为放在 内存 中,展示会更 “快”,快的原因有两点,如下所示:
这里说一下解码的概念。Android系统要在屏幕上展示图片的时候只认 “像素缓冲”,而这也是大多数操作系统的特征。而我们 常见的jpg,png等图片格式,都是把 “像素缓冲” 使用不同的手段压缩后的结果,所以这些格式的图片,要在设备上 展示,就 必须经过一次解码,它的 执行速度会受图片压缩比、尺寸等因素影响。(官方建议:把从内存中淘汰的图片,降低压缩比后存储到本地,以备后用,这样可以最大限度地降低以后复用时的解码开销。)
下面,我们来了解一下内存优化的一些重要概念。
手机不使用 PC 的 DDR内存,采用的是 LPDDR RAM,即 ”低功耗双倍数据速率内存“。其计算规则如下所示:
LPDDR系列的带宽 = 时钟频率 ✖️内存总线位数 / 8
LPDDR4 = 1600MHZ ✖️64 / 8 ✖️双倍速率 = 25.6GB/s。
复制代码
当系统 内存充足 的时候,我们可以 多用 一些获得 更好的性能。当系统 内存不足 的时候,我们希望可以做到 ”用时分配,及时释放“。
对于Android内存优化来说又可以细分为如下两个维度,如下所示:
主要是 降低运行时内存。它的 目的 有如下三个:
降低应用占ROM的体积,进行APK瘦身。它的 目的 主要是为了 降低应用占用空间,避免因ROM空间不足导致程序无法安装。
那么,内存问题主要是有哪几类呢?内存问题通常来说,可以细分为如下 三类:
下面,我们来了解下它们。
内存波动图形呈 锯齿张、GC导致卡顿。
这个问题在 Dalvik虚拟机 上会 更加明显,而 ART虚拟机 在 内存管理跟回收策略 上都做了 大量优化,内存分配和GC效率相比提升了5~10倍,所以 出现内存抖动的概率会小很多。
Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。简言之,就是 对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。一般来说,可用内存减少、频繁GC,容易导致内存泄漏。
即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heapgrowthlimit。
此外,除了因内存泄漏累积到一定程度导致OOM的情况以外,也有一次性申请很多内存,比如说 一次创建大的数组或者是载入大的文件如图片的时候会导致OOM。而且,实际情况下 很多OOM就是因图片处理不当 而产生的。
在 Android性能优化之内存优化 中我们已经介绍过了相关的优化工具,这里再简单回顾一下。
强大的 Java Heap 分析工具,查找 内存泄漏及内存占用, 生成 整体报告、分析内存问题 等等。建议 线下深入使用。
自动化 内存泄漏检测神器。建议仅用于线下集成。
它的 缺点 比较明显,具体有如下两点:
ART 和 Dalvik 虚拟机使用 分页和内存映射 来管理内存。下面我们先从Java的内存分配开始说起。
Java的 内存分配区域 分为如下 五部分:
流程可简述为 两步:
实现比较简单。
流程可简述为 三步:
实现简单,运行高效,每次仅需遍历标记一半的内存区域。
会浪费一半的空间,代价大。
流程可简述为 三步:
现在 主流的虚拟机 一般用的比较多的还是分代收集算法,它具有如下 特点:
Android 中的内存是 弹性分配 的,分配值 与 最大值 受具体设备影响。
对于 OOM场景 其实可以细分为如下两种:
我们需要着重注意一下这两种的区分。
以Android中虚拟机的角度来说,我们要清楚 Dalvik 与 ART 区别,Dalvik 仅固定一种回收算法,而 ART 回收算法可在 运行期按需选择,并且,ART 具备 内存整理 能力,减少内存空洞。
最后,LMK(Low Memory killer) 机制保证了进程资源的合理利用,它的实现原理主要是 根据进程分类和回收收益来综合决定的一套算法集。
当 内存频繁分配和回收 导致内存 不稳定,就会出现内存抖动,它通常表现为 频繁GC、内存曲线呈锯齿状。
并且,它的危害也很严重,通常会导致 页面卡顿,甚至造成 OOM。
主要原因有如下两点:
这里我们假设有这样一个场景:点击按钮使用 handler 发送一个空消息,handler 的 handleMessage 接收到消息后创建内存抖动,即在 for 循环创建 100个容量为10万 的 strings 数组并在 30ms 后继续发送空消息。
一般使用 Memory Profiler (表现为 频繁GC、内存曲线呈锯齿状)结合代码排查即可找到内存抖动出现的地方。
通常的技巧就是着重查看 循环或频繁被调用 的地方。
下面列举一些导致内存抖动的常见案例,如下所示:
使用 SparseArray类族、ArrayMap 来替代 HashMap。
在开始我们今天正式的主题之前,我们先来回归一下内存泄漏的概念与解决技巧。
所谓的内存泄漏就是 内存中存在已经没有用的对象。它的 表现 一般为 内存抖动、可用内存逐渐减少。 它的 危害 即会导致 内存不足、GC频繁、OOM。
而对于 内存泄漏的分析 一般可简述为如下 两步:
对于MAT来说,其常规的查找内存泄漏的方式可以细分为如下三步:
此外,在 Android性能优化之内存优化 还有几种进阶的使用方式,这里就不一一赘述了,下面,我们来看看关于 MAT 使用时的一些关键细节。
要全面掌握MAT的用法,必须要先了解 隐藏在 MAT 使用中的四大细节,如下所示:
除此之外,MAT 共有 5个关键组件 帮助我们去分析内存方面的问题,分别如下所示:
下面我们这里再简单地回顾一下它们。
如果从GC Root到达对象A的路径上必须经过对象B,那么B就是A的支配者。
查看 线程数量 和 线程的 Shallow Heap、Retained Heap、Context Class Loader 与 is Daemon。
通过 图形 的形式列出 占用内存比较多的对象。
在下方的 Biggest Objects 还可以查看其 相对比较详细的信息,例如 Shallow Heap、Retained Heap。
列出有内存泄漏的地方,点击 Details 可以查看其产生内存泄漏的引用链。
在介绍图片监控体系的搭建之前,首先我们来回顾下 Android Bitmap 内存分配的变化。
将 Bitmap对象 和 像素数据 统一放到 Java Heap 中,即使不调用 recycle,Bitmap 像素数据也会随着对象一起被回收。
但是,Bitmap 全部放在 Java Heap 中的缺点很明显,大致有如下两点:
将图片内存存放在Native中的步骤有 四步,如下所示:
我们都知道的是,当 系统内存不足 的时候,LMK 会根据 OOM_adj 开始杀进程,从 后台、桌面、服务、前台,直到手机重启。并且,如果频繁申请释放 Java Bitmap 也很容易导致内存抖动。对于这种种问题,我们该 如何评估内存对应用性能的影响 呢?
对此,我们可以主要从以下 两个方面 进行评估,如下所示:
对于具体的优化策略与手段,我们可以从以下 七个方面 来搭建一套 成体系化的图片优化 / 监控机制。
在项目中,我们需要 收拢图片的调用,避免使用 Bitmap.createBitmap、BitmapFactory 相关的接口创建 Bitmap,而应该使用自己的图片框架。
内存优化首先需要根据 设备环境 来综合考虑,让高端设备使用更多的内存,做到 针对设备性能的好坏使用不同的内存分配和回收策略。
因此,我们可以使用类似 device-year-class 的策略对设备进行分级,对于低端机用户可以关闭复杂的动画或”重功能“,使用565格式的图片或更小的缓存内存 等等。
业务开发人员需要 考虑功能是否对低端机开启,在系统资源不够时主动去做降级处理。
建立统一的缓存管理组件(参考 ACache),并合理使用 OnTrimMemory / LowMemory 回调,根据系统不同的状态去释放相应的缓存与内存。
在实现过程中,需要 解决使用 static LRUCache 来缓存大尺寸 Bitmap 的问题。
并且,在通过实际的测试后,发现 onTrimMemory 的 ComponetnCallbacks2.TRIM_MEMORY_COMPLETE 并不等价于 onLowMemory,因此建议仍然要去监听 onLowMemory 回调。
一个 空进程 也会占用 10MB 内存,低端机应该尽可能减少使用多进程。
针对低端机用户可以推出 4MB 的轻量级版本,如今日头条极速版、Facebook Lite。
在开发过程中,如果检测到不合规的图片使用(如图片宽度超过View的宽度甚至屏幕宽度),应该立刻提示图片所在的Activity和堆栈,让开发人员更快发现并解决问题。在灰度和线上环境,可以将异常信息上报到后台,还可以计算超宽率(图片超过屏幕大小所占图片总数的比例)。
下面,我们介绍下如何实现对大图片的检测。
继承 ImageView,重写实现计算图片大小。但是侵入性强,并且不通用。
因此,这里我们介绍一种更好的方案:ARTHook。
ARTHook,即 挂钩,用额外的代码勾住原有的方法,以修改执行逻辑,主要可以用于以下四个方面:
具体我们是使用 Epic 来进行 Hook,Epic 是 一个虚拟机层面,以 Java 方法为粒度的运行时 Hook 框架。简单来说,它就是 ART 上的 Dexposed,并且它目前 支持 Android 4.0~10.0。
Epic github 地址
Epic通常的使用步骤为如下三个步骤:
1、在项目 moudle 的 build.gradle 中添加
compile 'me.weishu:epic:0.6.0'
复制代码
2、继承 XC_MethodHook,实现 Hook 方法前后的逻辑。如 监控Java线程的创建和销毁:
class ThreadMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", started..");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", exit..");
}
}
复制代码
3、注入 Hook 好的方法:
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
复制代码
知道了 Epic 的基本使用方法之后,我们便可以利用它来实现大图片的监控报警了。
以 Awesome-WanAndroid 项目为例,首先,在 WanAndroidApp 的 onCreate 方法中添加如下代码:
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 1
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});
复制代码
在注释1处,我们 通过调用 DexposedBridge 的 findAndHookMethod 方法找到所有通过 ImageView 的 setImageBitmap 方法设置的切入点,其中最后一个参数 ImageHook 对象是继承了 XC_MethodHook 类,其目的是为了 重写 afterHookedMethod 方法拿到相应的参数进行监控逻辑的判断。
接下来,我们来实现我们的 ImageHook 类,代码如下所示:
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 1
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}
private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 2、图标宽高都大于view的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
// 3、当宽高度等于0时,说明ImageView还没有进行绘制,使用ViewTreeObserver进行大图检测的处理。
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = "Bitmap size too large: " +
"\n real size: (" + bitmapWidth + ',' + bitmapHeight + ')' +
"\n desired size: (" + viewWidth + ',' + viewHeight + ')' +
"\n call stack trace: \n" + Log.getStackTraceString(t) + '\n';
LogHelper.i(warnInfo);
}
}
复制代码
首先,在注释1处,我们重写了 ImageHook 的 afterHookedMethod 方法,拿到了当前的 ImageView 和要设置的 Bitmap 对象。然后,在注释2处,如果当前 ImageView 的宽高大于0,我们便进行大图检测的处理:ImageView 的宽高都大于 View 的2倍以上,则警告。接着,在注释3处,如果当前 ImageView 的宽高等于0,则说明 ImageView 还没有进行绘制,则使用 ImageView 的 ViewTreeObserver 获取其宽高进行大图检测的处理。至此,我们的大图检测检测组件就已经实现了。如果有小伙伴对 epic 的实现原理感兴趣的,可以查看这篇文章。
首先我们来了解一下这里的 重复图片 所指的概念: 即 Bitmap 像素数据完全一致,但是有多个不同的对象存在。
重复图片检测的原理其实就是 使用内存 Hprof 分析工具,自动将重复 Bitmap 的图片和引用堆栈输出。
已完全配置好的项目请参见这里
使用非常简单,只需要修改 Main 类的 main 方法的第一行代码,如下所示:
// 设置我们自己 App 中对应的 hprof 文件路径
String dumpFilePath = "//Users//quchao//Documents//heapdump//memory-40.hprof";
复制代码
然后,我们执行 main 方法即可在 //Users//quchao//Documents//heapdump 这个路径下看到生成的 images 文件夹,里面保存了项目中检测出来的重复的图片。images 目录如下所示:
[图片上传失败…(image-52adad-1627612907160)]
注意:需要使用 8.0 以下的机器,因为 8.0 及以后 Bitmap 中的 buffer 已保存在 native 内存之中。
具体的实现可以细分为如下三个步骤:
其中,获取堆栈 的信息也可以直接使用 haha 库来进行获取。这里简单说一下 使用 haha 库获取堆栈的流程,其具体可以细分为八个步骤,如下所示:
为了建立全局的 Bitmap 监控,我们必须 对 Bitmap 的分配和回收 进行追踪。我们先来看看 Bitmap 有哪些特点:
根据以上特点,我们可以建立一套 Bitmap 的高性价比监控组件:
这个方案的 性能消耗很低,可以在 正式环境 中进行。但是,需要注意的一点是,正式与测试环境需要采用不同程度的监控。
要建立线上应用的内存监控体系,我们需要 先获取 App 的 DalvikHeap 与 NativeHeap,它们的获取方式可归结为如下四个步骤:
对于监控场景,我们需要将其划分为两大类,如下所示:
根据 斐波那契数列 每隔一段时间(max:30min)获取内存的使用情况。常规内存的监控方法有多种实现方式,下面,我们按照 项目早期 => 壮大期 => 成熟期 的常规内存监控方式进行 演进式 讲解。
具体使用 Debug.dumpHprofData() 实现。
其实现的流程为如下四个步骤:
但是,这种方式有如下几个缺点:
在使用 LeakCanary 的时候我们需要 预设泄漏怀疑点,一旦发现泄漏进行回传。但这种实现方式缺点比较明显,如下所示:
定制 LeakCanary 其实就是对 haha组件 来进行 定制。haha库是 square 出品的一款 自动分析Android堆栈的java库。这是haha库的 链接地址。
对于haha库,它的 基本用法 一般遵循为如下四个步骤:
File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
复制代码
DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
复制代码
Snapshot snapshot = Snapshot.createSnapshot(buffer);
复制代码
ClassObj someClass = snapshot.findClass("com.example.SomeClass");
复制代码
我们在实现线上版的LeakCanary的时候主要要解决的问题有三个,如下所示:
在实现了线上版的 LeakCanary 之后,就需要 将线上版的 LeakCanary 与服务器和前端页面结合 起来。具体的 内存泄漏监控闭环流程 如下所示:
此外,在实现 图片内存监控 的过程中,应注意 两个关键点,如下所示:
对于低内存的监控,通常有两种方式,分别如下所示:
为了准确衡量内存性能,我们需要引入一系列的内存监控指标,如下所示:
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集UV
PSS 获取:调用 Debug.MemoryInfo 的 API 即可
复制代码
如果出现 新的内存使用不当或内存泄漏 的场景,这个指标会有所 上涨。
内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集UV
复制代码
计算触顶率的代码如下所示:
long javaMax = Runtime.maxMemory();
long javaTotal = Runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
float proportion = (float) javaUsed / javaMax;
复制代码
如果超过 85% 最大堆 的限制,GC 会变得更加 频发,容易造成 OOM 和 卡顿。
在具体实现的时候,客户端 尽量只负责 上报数据,而 指标值的计算 可以由 后台 来计算。这样便可以通过 版本对比 来监控是否有 新增内存问题。因此,建立线上内存监控的完整方案 至少需要包含以下四点:
每个线程初始化都需要 mmap 一定的栈大小,在默认情况下初始化一个线程需要 mmap 1MB 左右的内存空间。
在 32bit 的应用中有 4g 的 vmsize,实际能使用的有 3g+,这样一个进程 最大能创建的线程数 可以达到 3000个,但是,linux 对每个进程可创建的线程数也有一定的限制(/proc/pid/limits),并且,不同厂商也能修改这个限制,超过该限制就会 OOM。
因此,对线程数量的限制,在一定程度上可以 有效地避免 OOM 的发生。那么,实现一套 全局的线程监控组件 便是 刻不容缓 的了。
在线下或灰度的环境下通过一个定时器每隔 10分钟 dump 出应用所有的线程相关信息,当线程数超过当前阈值时,则将当前的线程信息上报并预警。
通过 Debug.startAllocCounting 来监控 GC 情况,注意有一定 性能影响。
在 Android 6.0 之前 可以拿到 内存分配次数和大小以及 GC 次数,其对应的代码如下所示:
long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
复制代码
并且,在 Android 6.0 及之后 可以拿到 更精准 的 GC 信息:
Debug.getRuntimeStat("art.gc.gc-count");
Debug.getRuntimeStat("art.gc.gc-time");
Debug.getRuntimeStat("art.gc.blocking-gc-count");
Debug.getRuntimeStat("art.gc.blocking-gc-time");
复制代码
对于 GC 信息的排查,我们一般关注 阻塞式GC的次数和耗时,因为它会 暂停线程,可能导致应用发生 卡顿。建议 仅对重度场景使用。
美团的 Android 内存泄漏自动化链路分析组件 Probe 在 OOM 时会生成 Hprof 内存快照,然后,它会通过 单独进程 对这个 文件 做进一步 分析。
它的缺点比较多,具体为如下几点:
在实现自动化链路分析组件 Probe 的过程中主要要解决两个问题,如下所示:
分析进程占用的内存 跟 内存快照文件的大小 不成正相关,而跟 内存快照文件的 Instance 数量 呈 正相关。所以在开发过程中我们应该 尽可能排除不需要的Instance实例。
Prope 的 总体架构图 如下所示:
[图片上传失败…(image-fa276a-1627612907159)]
而它的整个分析流程具体可以细分为八个步骤,如下所示:
解析后的 Snapshot 中的 Heap 有四种类型,具体为:
解析完 后使用了 计数压缩策略,对 相同的 Instance 使用 计数,以 减少占用内存。超过计数阈值的需要计入计数桶(计数桶记录了 丢弃个数 和 每个 Instance 的大小)。
如果对象是 基础数据类型,会将 自身的 RetainSize 累加到父节点 上,将 怀疑对象 替换为它的 父节点。
使用计数补偿策略计算 RetainSize,主要是 判断对象是否在计数桶中,如果在的话则将 丢弃的个数和大小补偿到对象上,累积计算RetainSize,最后对 RetainSize 排序以查找可疑对象。
项目地址请点击此处
在配置的时候要注意两个问题:
1、liballoc-lib.so在构建后工程的 build => intermediates => cmake 目录下。将对应的 cpu abi 目录拷贝到新建的 libs 目录下。
2、在 DumpPrinter Java 库的 build.gradle 中的 jar 闭包中需要加入以下代码以识别源码路径:
sourceSets.main.java.srcDirs = [‘src’]
具体的使用步骤如下所示:
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
复制代码
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
12-26 10:56:45.103 30450-30450/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData write file to /storage/emulated/0/crashDump/1577329005
复制代码
java -jar tools/DumpPrinter-1.0.jar dump文件路径 > dump_log.txt
复制代码
Found 4949 records:
tid=1 byte[] (94208 bytes)
dalvik.system.VMRuntime.newNonMovableArray (Native method)
android.graphics.Bitmap.nativeCreate (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:975)
android.graphics.Bitmap.createBitmap (Bitmap.java:946)
android.graphics.Bitmap.createBitmap (Bitmap.java:913)
android.graphics.drawable.RippleDrawable.updateMaskShaderIfNeeded (RippleDrawable.java:776)
android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:860)
android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:700)
android.view.View.getDrawableRenderNode (View.java:17736)
android.view.View.drawBackground (View.java:17660)
android.view.View.draw (View.java:17467)
android.view.View.updateDisplayListIfDirty (View.java:16469)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3885)
android.view.View.updateDisplayListIfDirty (View.java:16429)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
复制代码
在 Android 8.0 及之后,可以使用 Address Sanitizer、Malloc 调试和 Malloc 钩子 进行 native 内存分析,参见 native_memory
对于线下 Native 内存泄漏监控的建立,主要针对 是否能重编 so 的情况 来记录分配的内存信息。
设置内存兜底策略的目的,是为了 在用户无感知的情况下,在接近触发系统异常前,选择合适的场景杀死进程并将其重启,从而使得应用内存占用回到正常情况。
通常执行内存兜底策略时至少需要满足六个条件,如下所示:
只有在满足了以上条件之后,我们才会去杀死当前主进程并通过 push 进程重新拉起及初始化。
除了在 Android性能优化之内存优化 => 优化内存空间 中讲解过的一些常规的内存优化策略以外,在下面列举了一些更深入的内存优化策略。
对于 Android 2.x 系统,使用反射将 BitmapFactory.Options 里面隐藏的 inNativeAlloc 打开。
对于 Android 4.x 系统,使用或借鉴 Fresco 将 bitmap 资源在 native 中分配的方式。
使用 Glide、Fresco 等图片加载库,通过定制,在加载 bitmap 时,若发生 OOM,则使用 try catch 将其捕获,然后清除图片 cache,尝试降低 bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)。
需要注意的是,OOM 是可以捕获的,只要 OOM 是由 try 语句中的对象声明所导致的,那么在 catch 语句中,是可以释放掉这些对象,解决 OOM 的问题的。
计算当前应用内存占最大内存的比例的代码如下:
max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio = available / max;
复制代码
显示地除去应用的 memory,以加速内存收集过程的代码如下所示:
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
复制代码
当用户切换到其它应用并且你的应用 UI 不再可见时,应该释放应用 UI 所占用的所有内存资源。这能够显著增加系统缓存进程的能力,能够提升用户体验。
在所有 UI 组件都隐藏的时候会接收到 Activity 的 onTrimMemory() 回调并带有参数 TRIM_MEMORY_UI_HIDDEN。
在 Activity 的 onDestory 中递归释放其引用到的 Bitmap、DrawingCache 等资源,以降低发生内存泄漏时对应用内存的压力。
LeakCanary 的 AndroidExcludeRefs 列出了一些由于系统原因导致引用无法释放的例子,可使用类似 Hack 的方式去修复。具体的实现代码可以参考 Booster => 系统问题修复。
深入探索 Android 内存优化(炼狱级别-下)
1、国内Top团队大牛带你玩转Android性能分析与优化 第四章 内存优化
2、极客时间之Android开发高手课 内存优化
3、微信 Android 终端内存优化实践
4、GMTC-Android内存泄漏自动化链路分析组件Probe.key
5、Manage your app’s memory
6、Overview of memory management
7、Android内存优化杂谈
8、Android性能优化之内存篇
9、管理应用的内存
10、《Android移动性能实战》第二章 内存
11、每天一个linux命令(44):top命令
12、Android内存分析命令