深入探索 Android 内存优化(炼狱级别-上)

Java的 内存分配区域 分为如下 五部分

  • 1)、方法区:主要存放静态常量
  • 2)、虚拟机栈:Java变量引用
  • 3)、本地方法栈:native变量引用
  • 4)、堆:对象
  • 5)、程序计数器:计算当前线程的当前方法执行到多少行

2、Java 内存回收算法

1、标记-清除算法

流程可简述为 两步

  • 1)、标记所有需要回收的对象
  • 2)、统一回收所有被标记的对象
优点

实现比较简单。

缺点
  • 1)、标记、清除效率不高
  • 2)、产生大量内存碎片

2、复制算法

流程可简述为 三步

  • 1)、将内存划分为大小相等的两块
  • 2)、一块内存用完之后复制存活对象到另一块
  • 3)、清理另一块内存
优点

实现简单,运行高效,每次仅需遍历标记一半的内存区域

缺点

浪费一半的空间,代价大。

3、标记-整理算法

流程可简述为 三步

  • 1)、标记过程与 标记-清除算法 一样
  • 2)、存活对象往一端进行移动
  • 3)、清理其余内存
优点
  • 1)、避免 标记-清除 导致的内存碎片
  • 2)、避免复制算法的空间浪费

4、分代收集算法

现在 主流的虚拟机 一般用的比较多的还是分代收集算法,它具有如下 特点

  • 1)、结合多种算法优势
  • 2)、新生代对象存活率低,使用 复制算法
  • 3)、老年代对象存活率高,使用 标记-整理算法

3、Android 内存管理机制

Android 中的内存是 弹性分配 的,分配值 与 最大值 受具体设备影响

对于 OOM场景 其实可以细分为如下两种:

  • 1)、内存真正不足
  • 2)、可用(被分配的)内存不足

我们需要着重注意一下这两种的区分。

4、小结

以Android中虚拟机的角度来说,我们要清楚 Dalvik 与 ART 区别Dalvik 仅固定一种回收算法,而 ART 回收算法可在 运行期按需选择,并且,ART 具备 内存整理 能力,减少内存空洞

最后,LMK(Low Memory killer) 机制保证了进程资源的合理利用,它的实现原理主要是 根据进程分类和回收收益来综合决定的一套算法集

四、内存抖动

内存频繁分配和回收 导致内存 不稳定,就会出现内存抖动,它通常表现为 频繁GC、内存曲线呈锯齿状

并且,它的危害也很严重,通常会导致 页面卡顿,甚至造成 OOM

1、那么,为什么内存抖动会导致 OOM?

主要原因有如下两点:

  • 1)、频繁创建对象,导致内存不足及碎片(不连续)
  • 2)、不连续的内存片无法被分配,导致OOM

2、内存抖动解决实战

这里我们假设有这样一个场景:点击按钮使用 handler 发送一个空消息,handler 的 handleMessage 接收到消息后创建内存抖动,即在 for 循环创建 100个容量为10万 的 strings 数组并在 30ms 后继续发送空消息。

一般使用 Memory Profiler (表现为 频繁GC、内存曲线呈锯齿状)结合代码排查即可找到内存抖动出现的地方。

通常的技巧就是着重查看 循环或频繁被调用 的地方。

3、内存抖动常见案例

下面列举一些导致内存抖动的常见案例,如下所示:

1、字符串使用加号拼接

  • 1)、使用StringBuilder替代
  • 2)、初始化时设置容量,减少StringBuilder的扩容

2、资源复用

  • 1)、使用 全局缓存池,以 重用频繁申请和释放的对象
  • 2)、注意 结束 使用后,需要 手动释放对象池中的对象

3、减少不合理的对象创建

  • 1)、ondraw、getView 中创建的对象尽量进行复用
  • 2)、避免在循环中不断创建局部变量

4、使用合理的数据结构

使用 SparseArray类族、ArrayMap 来替代 HashMap

五、内存优化体系化搭建

在开始我们今天正式的主题之前,我们先来回归一下内存泄漏的概念与解决技巧。

所谓的内存泄漏就是 内存中存在已经没有用的对象。它的 表现 一般为 内存抖动、可用内存逐渐减少。 它的 危害 即会导致 内存不足、GC频繁、OOM

而对于 内存泄漏的分析 一般可简述为如下 两步

  • 1)、使用 Memory Profiler 初步观察
  • 2)、通过 Memory Analyzer 结合代码确认

1、MAT回顾

MAT查找内存泄漏

对于MAT来说,其常规的查找内存泄漏的方式可以细分为如下三步:

  • 1)、首先,找到当前 Activity,在 Histogram 中选择其 List Objects 中的 with incoming reference(哪些引用引向了我)
  • 2)、然后,选择当前的一个 Path to GC Roots/Merge to GC Roots 的 exclude All 弱软虚引用
  • 3)、最后,找到的泄漏对象在左下角下会有一个小圆圈

此外,在 Android性能优化之内存优化 还有几种进阶的使用方式,这里就不一一赘述了,下面,我们来看看关于 MAT 使用时的一些关键细节。

MAT的关键使用细节

要全面掌握MAT的用法,必须要先了解 隐藏在 MAT 使用中的四大细节,如下所示:

  • 1)、善于使用 Regex 查找对应泄漏类
  • 2)、使用 group by package 查找对应包下的具体类
  • 3)、明白 with outgoing references 和 with incoming references 的区别
  • with outgoing references:它引用了哪些对象
  • with incoming references:哪些对象引用了它
  • 4)、了解 Shallow Heap 和 Retained Heap 的区别
  • Shallow Heap:表示对象自身占用的内存
  • Retained Heap:对象自身占用的内存 + 对象引用的对象所占用的内存

MAT 关键组件回顾

除此之外,MAT 共有 5个关键组件 帮助我们去分析内存方面的问题,分别如下所示:

  • 1)、Dominator_tree
  • 2)、Histogram
  • 3)、thread_overview
  • 4)、Top Consumers
  • 5)、Leak Suspects

下面我们这里再简单地回顾一下它们。

1、Dominator(支配者):

如果从GC Root到达对象A的路径上必须经过对象B,那么B就是A的支配者。

2、Histogram和dominator_tree的区别:
  • 1)、Histogram 显示 Shallow Heap、Retained Heap、Objects,而 dominator_tree 显示的是 Shallow Heap、Retained Heap、Percentage
  • 2)、Histogram 基于 的角度,dominator_tree是基于 实例 的角度。Histogram 不会具体显示每一个泄漏的对象,而dominator_tree会
3、thread_overview

查看 线程数量线程的 Shallow Heap、Retained Heap、Context Class Loader 与 is Daemon

4、Top Consumers

通过 图形 的形式列出 占用内存比较多的对象

在下方的 Biggest Objects 还可以查看其 相对比较详细的信息,例如 Shallow Heap、Retained Heap

5、Leak Suspects

列出有内存泄漏的地方,点击 Details 可以查看其产生内存泄漏的引用链

2、搭建体系化的图片优化 / 监控机制

在介绍图片监控体系的搭建之前,首先我们来回顾下 Android Bitmap 内存分配的变化

Android Bitmap 内存分配的变化

在Android 3.0之前
  • 1)、Bitmap 对象存放在 Java Heap,而像素数据是存放在 Native 内存中的
  • 2)、如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,但是回调时机是不可控的
Android 3.0 ~ Android 7.0

Bitmap对象像素数据 统一放到 Java Heap 中,即使不调用 recycle,Bitmap 像素数据也会随着对象一起被回收。

但是,Bitmap 全部放在 Java Heap 中的缺点很明显,大致有如下两点:

  • 1)、Bitmap是内存消耗的大户,而 Max Java Heap 一般限制为 256、512MB,Bitmap 过大过多容易导致 OOM
  • 2)、容易引起大量 GC,没有充分利用系统的可用内存
Android 8.0及以后
  • 1)、使用了能够辅助回收 Native 内存的 NativeAllocationRegistry,以实现将像素数据放到 Native 内存中,并且可以和 Bitmap 对象一起快速释放,最后,在 GC 的时候还可以考虑到这些 Bitmap 内存以防止被滥用
  • 2)、Android 8.0 为了 解决图片内存占用过多和图像绘制效率过慢 的问题新增了 硬件位图 Hardware Bitmap

那么,我们如何将图片内存存放在 Native 中呢?

将图片内存存放在Native中的步骤有 四步,如下所示:

  • 1)、调用 libandroid_runtime.so 中的 Bitmap 构造函数,申请一张空的 Native Bitmap。对于不同 Android 版本而言,这里的获取过程都有一些差异需要适配
  • 2)、申请一张普通的 Java Bitmap
  • 3)、将 Java Bitmap 的内容绘制到 Native Bitmap 中
  • 4)、释放 Java Bitmap 内存

我们都知道的是,当 系统内存不足 的时候,LMK 会根据 OOM_adj 开始杀进程,从 后台、桌面、服务、前台,直到手机重启。并且,如果频繁申请释放 Java Bitmap 也很容易导致内存抖动。对于这种种问题,我们该 如何评估内存对应用性能的影响 呢?

对此,我们可以主要从以下 两个方面 进行评估,如下所示:

  • 1)、崩溃中异常退出和 OOM 的比例
  • 2)、低内存设备更容易出现内存不足和卡顿,需要查看应用中用户的手机内存在 2GB 以下所占的比例

对于具体的优化策略与手段,我们可以从以下 七个方面 来搭建一套 成体系化的图片优化 / 监控机制

1、统一图片库

在项目中,我们需要 收拢图片的调用,避免使用 Bitmap.createBitmap、BitmapFactory 相关的接口创建 Bitmap,而应该使用自己的图片框架

2、设备分级优化策略

内存优化首先需要根据 设备环境 来综合考虑,让高端设备使用更多的内存,做到 针对设备性能的好坏使用不同的内存分配和回收策略

因此,我们可以使用类似 device-year-class 的策略对设备进行分级,对于低端机用户可以关闭复杂的动画或”重功能“,使用565格式的图片或更小的缓存内存 等等。

业务开发人员需要 考虑功能是否对低端机开启,在系统资源不够时主动去做降级处理

3、建立统一的缓存管理组件

建立统一的缓存管理组件(参考 ACache),并合理使用 OnTrimMemory / LowMemory 回调,根据系统不同的状态去释放相应的缓存与内存

在实现过程中,需要 解决使用 static LRUCache 来缓存大尺寸 Bitmap 的问题

并且,在通过实际的测试后,发现 onTrimMemory 的 ComponetnCallbacks2.TRIM_MEMORY_COMPLETE 并不等价于 onLowMemory,因此建议仍然要去监听 onLowMemory 回调

4、低端机避免使用多进程

一个 空进程 也会占用 10MB 内存,低端机应该尽可能减少使用多进程。

针对低端机用户可以推出 4MB 的轻量级版本,如今日头条极速版、Facebook Lite。

5、线下大图片检测

在开发过程中,如果检测到不合规的图片使用(如图片宽度超过View的宽度甚至屏幕宽度),应该立刻提示图片所在的Activity和堆栈,让开发人员更快发现并解决问题。在灰度和线上环境,可以将异常信息上报到后台,还可以计算超宽率(图片超过屏幕大小所占图片总数的比例)

下面,我们介绍下如何实现对大图片的检测。

常规实现

继承 ImageView,重写实现计算图片大小。但是侵入性强,并且不通用。

因此,这里我们介绍一种更好的方案:ARTHook。

ARTHook优雅检测大图

ARTHook,即 挂钩,用额外的代码勾住原有的方法,以修改执行逻辑,主要可以用于以下四个方面:

  • 1)、AOP编程
  • 2)、运行时插桩
  • 3)、性能分析
  • 4)、安全审计

具体我们是使用 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 的实现原理感兴趣的,可以查看这篇文章

ARTHook方案实现小结
  • 1)、无侵入性
  • 2)、通用性强
  • 3)、兼容性问题大,开源方案不能带到线上环境

6、线下重复图片检测

首先我们来了解一下这里的 重复图片 所指的概念: 即 Bitmap 像素数据完全一致,但是有多个不同的对象存在

重复图片检测的原理其实就是 使用内存 Hprof 分析工具,自动将重复 Bitmap 的图片和引用堆栈输出

已完全配置好的项目请参见这里

使用说明

使用非常简单,只需要修改 Main 类的 main 方法的第一行代码,如下所示:

// 设置我们自己 App 中对应的 hprof 文件路径
String dumpFilePath = “//Users//quchao//Documents//heapdump//memory-40.hprof”;

然后,我们执行 main 方法即可在 //Users//quchao//Documents//heapdump 这个路径下看到生成的 images 文件夹,里面保存了项目中检测出来的重复的图片。images 目录如下所示:

注意:需要使用 8.0 以下的机器,因为 8.0 及以后 Bitmap 中的 buffer 已保存在 native 内存之中。

实现步骤

具体的实现可以细分为如下三个步骤:

  • 1)、首先,获取 android.graphics.Bitmap 实例对象的 mBuffer 作为 ArrayInstance ,通过 getValues 获取的数据为 Object 类型。由于后面计算 md5 需要为 byte[] 类型,所以通过反射的方式调用 ArrayInstance#asRawByteArray 直接返回 byte[] 数据
  • 2)、然后,根据 mBuffer 的数据生成 png 图片文件,这里直接参考了 github.com/JetBrains/a… 的实现方式。
  • 3)、最后,获取堆栈信息,直接 使用LeakCanary 获取 stack 的方法,使用 leakcanary-analyzer-1.6.2.jar 和 leakcanary-watcher-1.6.2.jar 这两个库文件。并用 反射 的方式调用了 HeapAnalyzer#findLeakTrace 方法。

其中,获取堆栈 的信息也可以直接使用 haha 库来进行获取。这里简单说一下 使用 haha 库获取堆栈的流程,其具体可以细分为八个步骤,如下所示:

  • 1)、首先,预备一个已经存在重复 bitmap 的 hprof 文件
  • 2)、利用 haha 库上的 MemoryMappedFileBuffer 读取 hrpof 文件 [关键代码 new MemoryMappedFileBuffer(heapDumpFile) ]
  • 3)、解析生成 snapshot,获取 heap,这里我只获取了 app heap [关键代码 snapshot.getHeaps(); heap.getName().equals(“app”) ]
  • 4)、从 snapshot 中根据指定 class 查找出所有的 Bitmap Classes [关键代码snapshot.findClasses(Bitmap.class.getName()) ]
  • 5)、从 heap 中获得所有的 Bitmap 实例 instance [关键代码 clazz.getHeapInstances(heap.getId()) ]
  • 6)、根据 instance 中获取所有的属性信息 Field[],并从 Field[] 查找出我们需要的 “mWidth” “mHeight” “mBuffer” 信息
  • 7)、通过 “mBuffer” 属性即可获取到他们的 hashcode 来判断是否是重复图片
  • 8)、最后,通过 instance 中 mNextInstanceToGcRoot 获取整个引用链信息并打印

7、建立全局的线上 Bitmap 监控

为了建立全局的 Bitmap 监控,我们必须 对 Bitmap 的分配和回收 进行追踪。我们先来看看 Bitmap 有哪些特点:

  • 1)、创建场景比较单一:在 Java 层调用 Bitmap.create 或 BitmapFactory 等方法创建,可以封装一层对 Bitmap 创建的接口,注意要 包含调用第三方库产生的 Bitmap,这里我们具体可以使用 ASM 编译插桩 + Gradle Transform 的方式来高效地实现。
  • 2)、创建频率比较低
  • 3)、和 Java 对象的生命周期一样服从 GC,可以使用 WeakReference 来追踪 Bitmap 的销毁

根据以上特点,我们可以建立一套 Bitmap 的高性价比监控组件

  • 1)、首先,在接口层将所有创建出来的 Bitmap 放入一个 WeakHashMap 中,并记录创建 Bitmap 的数据、堆栈等信息。
  • 2)、然后,每隔一定时间查看 WeakHashMap 中有哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。
  • 3)、最后,如果发生了 Bitmap 滥用或泄露,则将相关的数据与堆栈等信息打印出来或上报至 APM 后台。

这个方案的 性能消耗很低,可以在 正式环境 中进行。但是,需要注意的一点是,正式与测试环境需要采用不同程度的监控。

3、建立线上应用内存监控体系

要建立线上应用的内存监控体系,我们需要 先获取 App 的 DalvikHeap 与 NativeHeap,它们的获取方式可归结为如下四个步骤:

  • 1、首先,通过 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 获取内存信息数据
  • 2、然后,通过 hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)可以获得 Memory Profiler 中的多项数据,进而获得 细分内存的使用情况
  • 3、接着,通过 Runtime 获取 DalvikHeap
  • 4、最后,通过 Debug.getNativeHeapAllocatedSize 获取 NativeHeap

对于监控场景,我们需要将其划分为两大类,如下所示:

  • 1)、常规内存监控
  • 2)、低内存监控

1、常规内存监控

根据 斐波那契数列 每隔一段时间(max:30min)获取内存的使用情况。常规内存的监控方法有多种实现方式,下面,我们按照 项目早期 => 壮大期 => 成熟期 的常规内存监控方式进行 演进式 讲解。

项目早期:针对场景进行线上 Dump 内存的方式

具体使用 Debug.dumpHprofData() 实现。

其实现的流程为如下四个步骤:

  • 1)、超过最大内存的 80%
  • 2)、内存 Dump
  • 3)、回传文件至服务器
  • 4)、MAT 手动分析

但是,这种方式有如下几个缺点:

  • 1)、Dump文件太大,和对象数正相关,可以进行裁剪
  • 2)、上传失败率高,分析困难
壮大期:LeakCanary带到线上的方式

在使用 LeakCanary 的时候我们需要 预设泄漏怀疑点,一旦发现泄漏进行回传。但这种实现方式缺点比较明显,如下所示:

  • 1)、不适合所有情况,需要预设怀疑点
  • 2)、分析比较耗时,容易导致 OOM
成熟期:定制 LeakCanary 方式
那么,如何定制线上的LeakCanary?

定制 LeakCanary 其实就是对 haha组件 来进行 定制。haha库是 square 出品的一款 自动分析Android堆栈的java库。这是haha库的 链接地址

对于haha库,它的 基本用法 一般遵循为如下四个步骤:

1、导出堆栈文件

File heapDumpFile = …
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

2、根据堆栈文件创建出内存映射文件缓冲区

DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);

3、根据文件缓存区创建出对应的快照

Snapshot snapshot = Snapshot.createSnapshot(buffer);

4、从快照中获取指定的类

ClassObj someClass = snapshot.findClass(“com.example.SomeClass”);

我们在实现线上版的LeakCanary的时候主要要解决的问题有三个,如下所示:

  • 1)、解决 预设怀疑点 时不准确的问题 => 自动找怀疑点
  • 2)、解决掉将 hprof 文件映射到内存中的时候可能导致内存暴涨甚至发生 OOM 的问题 => 对象裁剪,不全部加载到内存。即对生成的 Hprof 内存快照文件做一些优化:裁剪大部分图片对应的 byte 数据 以减少文件开销,最后,使用 7zip 压缩,一般可 节省 90% 大小
  • 3)、分析泄漏链路慢而导致分析时间过长 => 分析 Retain size 大的对象
成熟期:实现内存泄漏监控闭环

在实现了线上版的 LeakCanary 之后,就需要 将线上版的 LeakCanary 与服务器和前端页面结合 起来。具体的 内存泄漏监控闭环流程 如下所示:

  • 1)、当在线上版 LeakCanary 上发现内存泄漏时,手机将上传内存快照至服务器
  • 2)、此时服务器分析 Hprof,如果不是系统原因导致误报则通过 git 得到该最近修改人
  • 3)、最后将内存泄漏 bug 单提交给负责人。该负责人通过前端实现的 bug 单系统即可看到自己新增的bug

此外,在实现 图片内存监控 的过程中,应注意 两个关键点,如下所示:

  • 1)、在线上可以按照 不同的系统、屏幕分辨率 等纬度去 分析图片内存的占用情况
  • 2)、在 OOM 崩溃时,可以将 图片总内存、Top N 图片占用内存 写入 崩溃日志

2、低内存监控

对于低内存的监控,通常有两种方式,分别如下所示:

  • 1、利用 onTrimMemory / onLowMemory 监听系统回调的物理内存警告
  • 2、在后台起一个服务定时监控系统的内存占用,只要超过虚拟内存大小最大限制的 90% 则直接触发内存警告

3、内存监控指标

为了准确衡量内存性能,我们需要引入一系列的内存监控指标,如下所示:

1)、发生频率
2)、发生时各项内存使用状况
3)、发生时App的当前场景
4)、内存异常率

内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集UV
PSS 获取:调用 Debug.MemoryInfo 的 API 即可

如果出现 新的内存使用不当或内存泄漏 的场景,这个指标会有所 上涨

5)、触顶率

内存 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 和 卡顿

4、小结

在具体实现的时候,客户端 尽量只负责 上报数据,而 指标值的计算 可以由 后台 来计算。这样便可以通过 版本对比监控是否有 新增内存问题。因此,建立线上内存监控的完整方案 至少需要包含以下四点

  • 1)、待机内存、重点模块内存、OOM率
  • 2)、整体及重点模块 GC 次数、GC 时间
  • 3)、增强的 LeakCanry 自动化内存泄漏分析
  • 4)、低内存监控模块的设置

4、建立全局的线程监控组件

每个线程初始化都需要 mmap 一定的栈大小,在默认情况下初始化一个线程需要 mmap 1MB 左右的内存空间

32bit 的应用中有 4g 的 vmsize实际能使用的有 3g+,这样一个进程 最大能创建的线程数 可以达到 3000个,但是,linux 对每个进程可创建的线程数也有一定的限制(/proc/pid/limits),并且,不同厂商也能修改这个限制,超过该限制就会 OOM。

因此,对线程数量的限制,在一定程度上可以 有效地避免 OOM 的发生。那么,实现一套 全局的线程监控组件 便是 刻不容缓 的了。

全局线程监控组件的实现原理

在线下或灰度的环境下通过一个定时器每隔 10分钟 dump 出应用所有的线程相关信息,当线程数超过当前阈值时,则将当前的线程信息上报并预警

5、GC 监控组件搭建

通过 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”);

总结

笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题

觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Debug.getRuntimeStat(“art.gc.blocking-gc-count”);
Debug.getRuntimeStat(“art.gc.blocking-gc-time”);

总结

笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!
BATJ大厂Android高频面试题

[外链图片转存中…(img-FOQLzsYh-1714662086981)]

[外链图片转存中…(img-HLNOkRDV-1714662086981)]

[外链图片转存中…(img-7Yek8MO4-1714662086981)]

[外链图片转存中…(img-MXHzFhA2-1714662086982)]

觉得有收获的记得点赞,关注+收藏哦!你们的点赞就是我的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值