🌞一个好的架构可以减少甚至避免我们犯错,而一个好的监控可以帮助我们及时发现问题🌞内存优化的重点在于监控,和平时代码规范。这篇文章带你了解监控工具 Memory Profiler、MAT、LeakCanary 的使用以及分析。

本文主要记载

  • 1 什么是 Memory Profiler?
    • 1.1 使用介绍
      • 1.1.1 启用高级分析
      • 1.1.2 计算内存(每个内存类别当前使用多少内存)
      • 1.1.3 查看内存分配(分配的对象数)
      • 1.1.4 在分析时提高应用性能
      • 1.1.5 捕获堆转储
      • 1.1.6 将堆转储另存为 HPROF 文件
  • 2 什么是 MAT?
    • 2.1 使用前提
    • 2.2 使用介绍
  • 3 什么是 LeakCanary?
    • 3.1 LeakCanary 原理
    • 3.2 LeakCanary 使用
  • 4 自定义实现 Demo
    • 4.1 自动化监测 Demo
    • 4.2 重复图片监测 Demo
  • 5 优化思路
    • 5.1 设备分级
    • 5.2 Bitmap 优化
  • 6 更多

一个好的架构可以减少甚至避免我们犯错,而一个好的监控可以帮助我们及时发现问题。

检测工具常见的有 Memory Profiler MAT LeakCanary

1 什么是 Memory Profiler?

Profiler 是 Android Studio 为我们提供的性能分析工具,它包含了 CPU、内存、网络以及 电量 的分析信息,而 Memory Profiler 则是 Profiler 中的其中一个版块。

打开 Profiler 方式

  1. View > Tool Windows > Android Profiler
  2. 下方的 Profiler 标签
  3. 双击 shift 搜索 profiler

1.1 使用介绍

  1. 用于强制执行垃圾回收事件的按钮(强制 GC )。

  2. 用于 1.1.5 捕获堆转储 的按钮。

    注意:只有在连接到搭载 Android 7.1(API 级别 25)或更低版本的设备时,才会在堆转储按钮右侧显示用于 记录内存分配 的按钮。

  3. 用于指定分析器多久捕获一次内存分配的下拉菜单。选择适当的选项可 1.1.4 在分析时提高应用性能。

  4. 用于缩放时间轴的按钮。

  5. 用于跳转到实时内存数据的按钮。

  6. 事件时间轴,显示活动状态、用户输入事件和屏幕旋转事件。

  7. 内存使用量时间轴,它会显示以下内容:

    • 一个堆叠图表,显示 1.1.2 每个内存类别当前使用多少内存 ,如左侧的 y 轴以及顶部的彩色键所示。
    • 一条虚线,表示 1.1.3 分配的对象数,如右侧的 y 轴所示。
    • 每个垃圾回收事件的图标。

如果使用的是搭载 Android 7.1 或更低版本的设备,则并非所有分析数据在默认情况下都可见。如果看到一条消息,显示“Advanced profiling is unavailable for the selected process”,需要 1.1.1 启用高级分析 才能看到以下内容:

  • 事件时间轴
  • 分配的对象数
  • 垃圾回收事件

在 Android 8.0 及更高版本上,始终为可调试应用启用高级分析。

1.1.1 启用高级分析

  1. 依次选择 Run > Edit Configurations。
  2. 在左侧窗格中选择本应用模块。
  3. 点击 Profiling 标签,然后勾选 Enable advanced profiling。
  4. 重新编译并运行应用。

1.1.2 计算内存(每个内存类别当前使用多少内存)

内存计数中的类别如下:

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。

  • Native:从 C 或 C++ 代码分配的对象的内存。

    即使应用中不使用 C++,也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存处理各种任务,如处理图像资源和其他图形时。

  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)

  • Stack:应用中的原生堆栈和 Java 堆栈使用的内存。这通常与应用运行多少线程有关。

  • Code:应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。

  • Others:应用使用的系统不确定如何分类的内存。

  • Allocated:应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。

    如果连接到搭载 Android 7.1 及更低版本的设备,只有在 Memory Profiler 连接到运行的应用时,才开始此分配计数。因此,开始分析之前分配的任何对象都不会被计入。不过,Android 8.0 及更高版本附带一个设备内置分析工具,该工具可跟踪所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示应用中待处理的 Java 对象总数。

注意:高级分析配置会使编译过程变慢,所以仅在想要开始分析应用时,才应启用该配置。

1.1.3 查看内存分配(分配的对象数)

对于 Android 8.0 及更高版本,选择一个现有时间轴区域以查看对象分配

对于 Android 7.1 及更低版本,必须明确记录内存分配,点击 Memory Profiler 工具栏中的 Record memory allocations 图标或者 一个 Record 的按钮。记录时,Memory Profiler 会跟踪应用中发生的所有分配。完成后,请点击 Stop recording 图标或者一个 Stop 的按钮,查看分配。

注意:在 Android 7.1 及更低版本上,最多可以记录 65535 个分配。 如果记录会话超出此限制,则记录中仅保存最新的 65535 个分配。(在 Android 8.0 及更高版本上,则没有实际的限制。)

  1. 浏览列表以查找堆计数异常大且可能存在泄露的对象。为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例。
  2. 在 Instance View 窗格中,点击一个实例。此时下方将出现 Call Stack 标签页,显示该实例被分配到何处以及在哪个线程中。
  3. 在 Call Stack 标签页中,右键点击任意行并选择 Jump to Source,以在编辑器中打开该代码。

Live Allocation 第一个选择框是选择要检查的堆:

  • default heap:当系统未指定堆时。
  • image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失。
  • zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
  • app heap:您的应用在其中分配内存的主堆。
  • JNI heap:显示 Java 原生接口 (JNI) 引用被分配和释放到什么位置的堆。

第二个选择框选择如何安排分配:

  • Arrange by class:根据类名称对所有分配进行分组。这是默认选项。
  • Arrange by package:根据软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。只有在记录分配期间 捕获堆转储 时,此选项才有效。即便如此,堆中也很可能有在开始记录之前分配的对象,所以会先显示这些分配,直接按类名称列出它们。

1.1.4 在分析时提高应用性能

为了在分析时提高应用性能,Memory Profiler 在默认情况下会定期对内存分配进行采样。在运行 API 级别 26 或更高级别的设备上进行测试时,可以使用 Allocation Tracking 下拉菜单来更改此行为。可用选项如下:

  • Full:捕获内存中的所有对象分配。这是 Android Studio 3.2 及更低版本中的默认行为。如果您有一个分配了大量对象的应用,则可能会在分析时观察到应用的运行速度明显减慢。
  • Sampled:定期对内存中的对象分配进行采样。这是默认选项,在分析时对应用性能的影响较小。在短时间内分配大量对象的应用仍可能会表现出明显的速度减慢。
  • Off:停止跟踪应用的内存分配。

1.1.5 捕获堆转储

堆转储显示在捕获堆转储时应用中哪些对象正在使用内存。特别是在长时间的用户会话后,堆转储会显示认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄露。

要捕获堆转储,请点击 Memory Profiler 工具栏中的 Dump Java heap 图标 在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存来收集数据。

  • Allocations:堆中的分配数。

  • Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。

    您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存。

  • Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)。

  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。

点击一个类名称可在右侧打开 Instance View 窗口。

  • Depth:从任意 GC 根到选定实例的最短跳数。
  • Native Size:原生内存中此实例的大小。 只有在使用 Android 7.0 及更高版本时,才会看到此列。
  • Shallow Size:Java 内存中此实例的大小。
  • Retained Size:此实例所支配内存的大小。

如果你 ClassName 选中的是 Bitmap ,在 7.1 及以下版本的设备中 Call Sack位置会出现 Bitmap Preview 功能,来预览是哪张图片

References 标签页,显示对该对象的每个引用,如果发现某个引用可能在泄露内存,右键点击它并选择 Go to Instance。这样会从堆转储中选择相应的实例,从而显示它自己的实例数据。

1.1.6 将堆转储另存为 HPROF 文件

在 Android Studio 3.1 及更低版本中,Export capture to file 按钮 img 位于时间轴下方工具栏的左侧

在 Android Studio 3.2 及更高版本中,Sessions 窗格中每个 Heap Dump条目的右侧都有一个 Export Heap Dump 按钮。在随即显示的 Export As 对话框中,使用 .hprof 文件扩展名保存文件。


2 什么是 MAT?

对于内存泄漏问题,Memory Profiler 只能给我们提供一个简单的分析,不能够帮我们确认具体发生问题的地方。

而 MAT 就可以帮我们做到这一点,MAT 的全称是 Memory Analyzer Tool,它是一款功能强大的 Java 堆内存分析工具,可以用于查找内存泄漏以及查看内存消耗情况。

2.1 使用前提

  1. 到 MAT 的官网 下载 MAT。

  2. 使用 Memory Profiler 的堆转储功能,导出 hprof(Heap Profile)文件。

  3. 配置 platform-tools 环境变量

  4. 使用命令将 Memory Profiler 中导出来的 hprof 文件转换为 MAT 可以解析的 hprof 文件,命令如下

1
hprof-conv ../原始文件.hprof ../输出文件.hprof
  1. 打开 MAT

File > open Heap dump ,选择我们转换后的文件

2.2 使用介绍

  • Overview:概览信息

    • Unreachable Objects Histogram:可被回收的对象,但是仍在内存当中
  • Histogram:直方图,基于类来分析,会显示该类有多少个实例

    第一行输入可以搜索,Tool Bar 中会多出一个 Group by... 选择 package 会变成已报名的形式来进行分类。

    • Object:有多少实例
    • Shallow Heap:自己占了多少内存
    • Retained Heap:应用链总共占了多少内存
    • 选择一个类条目右键会出现 List objects
      • with outgoing references:引用了些什么
      • with incoming references:被谁引用着
  • dominator_tree:基于实例来分析,会显示具体的实例是哪个

    第一行输入可以搜索,Tool Bar 中会多出一个 Group by... 选择 package 会变成已报名的形式来进行分类。

    • Percentage:当前所有对象中,此对象内存占用百分比,可以发现那些内存占用过大的对象
    • 右键点击会出现 List objects ,会显示出具体事例
  • OQL:对象查询语言

    • 例如 slect * from com.package.Activity ,会查询出该类所有的实例
  • thread_overview:dump 内存这个时刻的所有线程

  • top_consumers:用图形列出占用内存比较大的实例

    • Biggest Objects:详细信息,点击出现 List objects
  • Leak Suspects:更详细的分析,可以从这里找到可以的泄漏点

    • Probleam Suspect (x) :点击 Details 会出现可疑的 引用链,找到泄漏点

3 什么是 LeakCanary?

如果使用 MAT 来分析内存问题,会有一些难度,而且效率也不是很高。

为了能迅速发现内存泄漏,Square 公司基于 MAT 开源了 LeakCanary。

LeakCanary 是一个内存泄漏检测框架。

3.1 LeakCanary 原理

  1. 检测保留的实例

    LeakCanary 是基于 LeakSentry 开发的,LeakSentry 会 hook Android 生命周期,自动检测当 Activity 或 Fragment 被销毁时,它们的实例是否被回收了。

    销毁的实例会传给 RefWatcher,RefWatcher 会持有它们的弱引用。

    你也可以观察所有不再需要的实例,比如一个不再使用的 View,不再使用的 Presenter 等。

    如果等待了 5 秒,并且 GC 触发了之后,弱引用还没有被清理,那么 RefWatcher 观察的实例就可能处于内存泄漏状态了。

  2. 堆转储

    当保留对象的数量达到阈值时,LeakCanary 将 Java 堆转储到.hprof 存储在 Android 文件系统中的文件中。

    当应用程序可见时,默认的阈值是5个被保留的对象,当应用程序不可见时,默认的阈值是 1 个被保留的对象。如果您看到保留的对象通知,然后将应用程序放在后台(例如通过按Home按钮),那么阈值将从 5 更改为 1 ,LeakCanary 将在 5 秒内转储堆。点击通知将迫使LeakCanary 立即转储堆。

  3. 分析堆

    LeakCanary .hprof使用 Shark 解析文件,并在该堆转储中找到保留的对象。

    对于每个保留的对象,LeakCanary 会找到引用的路径,以防止对该保留的对象进行垃圾回收:其泄漏跟踪。泄漏跟踪是从垃圾回收根到保留对象的最佳强引用路径的另一个名称。

  4. 泄漏分组

    当有两个泄漏分析结果相同时,LeakCanary 为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏(即,由同一 bug 引起的泄漏)组合在一起,以免重复显示同样的泄漏信息。

3.2 LeakCanary 使用

1
2
3
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}

2.2 不需要初始化,如果你添加了

1
apply plugin: 'android-aspectjx'

需要在文章添加

1
2
3
4
aspectjx { 
exclude 'com.squareup'
exclude 'leakcanary'
}

否则会报 Unable to get provider leakcanary.internal.LeakCanaryFileProvider: java.lang.ClassNotFoundException: Didn't find class "leakcanary.internal.LeakCanaryFileProvider"

4 自定义实现 Demo

Profiler 中使用了 Allocation Tracker 技术,Android内存申请分析 这篇文章讲述了 Allocation Tracker 的使用方式,并且讲解了 Android Studio 是如何使用 Allocation Tracker 的。可以让我们更加熟悉 Android Studio 中 Profiler 各种工具的实现原理后。

文中提到了 Allocation Tracker 的三个缺点。

  • 获取的信息过于分散,中间夹杂着不少其他的信息,很多信息不是应用申请的,可能需要进行不少查找才能定位到具体的问题。
  • 跟 Traceview 一样,无法做到自动化分析,每次都需要开发者手工开始 / 结束,这对于某些问题的分析可能会造成不便,而且对于批量分析来说也比较困难。
  • 虽然在 Allocation Tracking 的时候,不会对手机本身的运行造成过多的性能影响,但是在停止的时候,直到把数据 dump 出来之前,经常会把手机完全卡死,如果时间过长甚至会直接 ANR。

因此我们希望可以做到脱离 Android Studio,实现一个自定义的 Allocation Tracker,实现对象内存的自动化分析。通过这个工具可以获取所有对象的申请信息(大小、类型、堆栈等),可以找到一段时间内哪些对象占用了大量的内存。

我们可以用自定义的 Allocation Tracker 来监控 Java 内存的监控,也可以拓展成实时监控 Java 内存泄漏。不过任何一个工具如果只需要做到线下自动化测试,实现起来会相对简单,但想要移植到线上使用,那就要更加关注兼容性、稳定性和性能。

4.1 自动化检测Demo

这是一个自定义 Allocation Tracker 的 Demo 极客时间 Android 高手课 Chapter03

项目使用了 inline hook 来拦截内存对象分配时候的 RecordAllocation 函数,通过拦截该接口可以快速获取到当时分配对象的类名和分配的内存大小。
在初始化的时候我们设置了一个分配对象数量的最大值,如果从 start 开始对象分配数量超过最大值就会触发内存 dump,然后清空 alloc 对象列表,重新计算。该功能和 Android Studio 里的 Allocation Tracker 类似,只不过可以在代码级别更细粒度的进行控制。可以精确到方法级别。

  • ndk_dlopen 用来获取动态链接库
  • ndk_dlsym 通过动态链接库和函数签名拿到函数地址
  • Substrate MSHookFunction 一款经典的 hook 框架,支持 hook C/C++ 代码
  • fbjni 是从 Facebook 开源的一款jni工具类库,主要提供了工具类,ref utils ,Global JniEnv。
  • native-lib 中 JNI_OnLoad 在 System.loadLibrary("xxx"); 时执行

代码运行流程:

  1. 在 MainActivity 中调用 AllocTracker.initForArt 去初始化,然后通过 ndk_dlopen 和ndk_dlsym 拿到所有需要的函数,只是拿到函数并没有调用

  2. JNI_OnLoad 调用 hookFunc();这个方法就是真正的把系统的 tracking 函数 hook 住的地方

    1. 通过 ndk_dlsym 拿到函数地址。
    2. 通过 MSHookFunction 把 newArtRecordAllocation26 hook 进原函数地址里,同时拿到旧函数的实现并导向 oldArtRecordAllocation26 。这样做可以保留旧函数。
    3. 在新函数中有一句 allocObjectCount > setAllocRecordMax ,当分配对象数量大于初始化时设置的最大值,就把数据保存在 log 文件中。

C/C++ 的 hook 和 java 反射一样,如果系统在新版本把函数名,或者函数签名改了那就无法使用了,所以在文件中出现了很多版本适配的地方。

Logcat 中 saveARTAllocationData write file to XXXX 能找到对应的日志文件路径

看不到日志的输出情况,每个人设备内存情况不一样,GC 回收的频率也不一致,在你不停生产对象的时候,GC 不断的跟随回收,导致无法达到设置的最大的数据量,所以一直没有日志的写入。

总结:

这个 Demo 主要学习了 Substrate fbjni ndk_dlopen 这几个库的简单使用,CmakeList 的很多写法,以及 native 函数简写

1
2
#define JNI_METHOD_DECL(ret_type, method_name) \
extern "C" JNIEXPORT ret_type JNICALL Java_##com_dodola_alloctrack##_##AllocTracker##_##method_name

还了解了一些 hook 点以及 hook 方法。

4.2 重复图片检测Demo

极客时间 Android 高手课 Chapter04 已完成的 Demo

通过分析内存文件 hprof 快速判断内存中是否存在重复的图片,并且将这些重复图片的 PNG 、堆栈等信息输出。

需要注意的是需要使用8.0以下的机器,因为8.0以后Bitmap中的buffer已经放到native内存中了

代码在开始加载了两张相同的图片。

1
2
3
4
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.test); 
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
imageView1.setImageBitmap(bitmap1);
imageView2.setImageBitmap(bitmap2);

然后手动 GC 生成 hprof 文件

1
2
3
4
5
// 手动触发GC
Runtime.getRuntime().gc();
System.runFinalization();
// 生成Hprof文件
Debug.dumpHprofData(file.getAbsolutePath());

接下来在DuplicatedBitmapAnalyzer 的一个 java 项目里,利用 HAHA 库进行文件分析,并输入重复图片。

作者已经将代码打成 JAR 包,可以直接调用

1
java -jar tools/DuplicatedBitmapAnalyzer-1.0.jar hprof文件路径

对于更多的信息,可以参看 LeakCanary 源码

总结:

这个 Demo 主要学到了如何主动生成 hprof 文件,以及如何用 HAHA 这个库解析 hprof 文件。

联想:可以在特定场合去 dump 文件,比如内存大于最大内存 80%,因为 hprof 文件可能会很大,所以在用户 wifi 情况下回传。

Tencent/matrix 这个框架拥有更丰富的检测方式。


5 优化思路

5.1 设备分级

内存优化首先需要根据设备环境来综合考虑

  • 设备分级。使用类似 device-year-class 的策略对设备分级,对于低端机用户可以关闭复杂的动画,或者是某些功能;使用 565 格式的图片,使用更小的缓存内存等。
    • device-year-class 会根据手机的内存、CPU 核心数和频率等信息决定设备属于哪一个年份,这个示例表示对于 2013 年之后的设备可以使用复杂的动画,对于 2010 年之前的低端设备则不添加任何动画。
    • 565 格式图片:一张图片中每一个像素的大小取决于它的解码选项,而 Android 中能够选择的 Bitmap 解码选项有四种。
      • ARGB_8888 :ARGB 四个通道的值都是 8 位,加起来 32 位,也就是每个像素占 4 个字节
      • ARGB_4444: ARGB 四个通道的值都是 4 位,加起来 16 位,也就是每个像素占 2 个字节
      • RGB_565: RGB 三个通道分别是 5 位、6 位、5 位,加起来 16 位,也就是每个像素占 2 个字节
      • ALPHA_8:只有 A 通道,占 8 位,也就是每个像素占 1 个字节
  • 缓存管理。我们需要有一套统一的缓存管理机制,可以适当地使用内存;当“系统有难”时,也要义不容辞地归还。我们可以使用 OnTrimMemory(本文 4.1.1 会讲到) 回调,根据不同的状态决定释放多少内存。
  • 进程模型。一个空的进程也会占用 10MB 的内存,而有些应用启动就有十几个进程,甚至有些应用已经从双进程保活升级到四进程保活,所以减少应用启动的进程数、减少常驻进程、有节操的保活,对低端机内存优化非常重要。
  • 安装包大小。安装包中的代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。一个 80MB 的应用很难在 512MB 内存的手机上流畅运行。这种情况我们需要考虑针对低端机用户推出 4MB 的轻量版本,例如 Facebook Lite、今日头条极速版都是这个思路。

5.2 Bitmap 优化

Bitmap 内存一般占应用总内存很大一部分,所以做内存优化永远无法避开图片内存这个“永恒主题”。

  1. 统一图片库

    图片内存优化的前提是收拢图片的调用,这样我们可以做整体的控制策略。就如设备分级中说的,低端设备策略等。

  2. 统一监控

    1. 大图片监控。我们需要注意某张图片内存占用是否过大,例如长宽远远大于 View 甚至是屏幕的长宽。在开发过程中,如果检测到不合规的图片使用,应该立即弹出对话框提示图片所在的 Activity 和堆栈,让开发同学更快发现并解决问题。Glide 可以有效的解决此类问题,因为 Glide 会根据 ImageView 的大小把图片大小调整成ImageView 的大小加载图片,并且 Glide 有三级缓存,在内存缓存中,Glide 会根据屏幕大小选择合适的大小作为图片内存缓存区的大小。
    2. 重复图片监控。重复图片指的是 Bitmap 的像素数据完全一致,但是有多个不同的对象存在。这个监控不需要太多的样本量,一般只在内部使用。在检测模块中实现过一个内存 Hprof 的分析工具,它可以自动将重复 Bitmap 的图片和引用链输出。
      1. 图片总内存。通过收拢图片使用,我们还可以统计应用所有图片占用的内存,这样在线上就可以按不同的系统、屏幕分辨率等维度去分析图片内存的占用情况。在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。

6 更多

6.1 监听系统内存状态

Android 提供了两种方式让我们可以监听系统内存状态

6.1.1 ComponentCallback2

在 Android 4.0 后,Android 应用可以通过在 Activity 中实现 ComponentCallback2 接口获取系统内存的相关事件,这样就能在系统内存不足时提前知道这件事,提前做出释放内存的操作,避免我们自己的应用被系统干掉。

ComponentCallnback2 提供了 onTrimMemory(level) 回调方法,在这个方法里我们可以针对不同的警告等级做出不同的释放内存操作。

原文是 kotlin 版,这里给出一个 java 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MainActivity2 extends AppCompatActivity implements ComponentCallbacks2 {

@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
// 应用界面处于后台
// 可以在这里释放 UI 对象
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
// 应用正常运行中,不会被杀掉,但是系统内存已经有点低了
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
// 应用正常运行中,不会被杀掉,但是系统内存已经非常低了,
// 这时候应该释放一些不必要的资源以提升系统性能
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
// 应用正常运行,但是系统内存非常紧张,
// 系统已经开始根据 LRU 缓存杀掉了大部分缓存的进程
// 这时候我们要释放所有不必要的资源,不然系统可能会继续杀掉所有缓存中的进程
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
// 系统内存很低,系统准备开始根据 LRU 缓存清理进程,
// 这时我们的程序在 LRU 缓存列表的最近位置,不太可能被清理掉,
// 但是也要去释放一些比较容易恢复的资源,让系统内存变得充足
break;
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
// 系统内存很低,并且我们的应用处于 LRU 列表的中间位置,
// 这时候如果还不释放一些不必要资源,那么我们的应用可能会被系统干掉
break;
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
// 系统内存非常低,并且我们的应用处于 LRU 列表的最边缘位置,
// 系统会有限考虑干掉我们的应用,如果想活下来,就要把所有能释放的资源都释放了
break;
default:
// 应用从系统接收到一个无法识别的内存等级值,
// 跟一般的低内存消息提醒一样对待这个事件
// 释放所有不重要的数据结构。
break;
}
}
}

6.1.2 ActivityManager.getMemoryInfo()

Android 提供了一个 ActivityManager.getMemoryInfo() 方法给我们查询内存信息,这个方法会返回一个 ActivityManager.MemoryInfo 对象,这个对象包含了系统当前内存状态,这些状态信息包括可用内存、总内存以及低杀内存阈值。

MemoryInfo 中包含了一个 lowMemory 布尔值,这个布尔值用于表明系统是否处于低内存状态。

原文是 kotlin 版,这里给出一个 java 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void doSomethingMemoryIntensive() {
// 在做一些需要很多内存的任务前,
// 检查设备是否处于低内存状态、
if (!getAvailableMemory().lowMemory) {
// 做需要很多内存的任务
}
}

private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo MemoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(MemoryInfo);
return MemoryInfo;
}

6.2 内存优化技巧

6.2.1 谨慎使用 Service

让一个没用的 Service 在后台运行对于一个应用的内存管理来说是一件最糟糕的事情。

要在 Service 的任务完成后停止它,不然 Service 占用的这块内存会泄漏。

当你的应用中运行着一个 Service,除非系统内存不足,否则它不会被干掉。

这就导致对于系统来说 Service 的运行成本很高,因为 Service 占用的内存其他的进程是不能使用的。

Android 有一个缓存进程列表,当可用内存减少时,这个列表也会随之缩小,这就会导致应用间的切换变得很慢。

如果我们是用 Service 监听一些系统广播,希望在特定情况下再启动事务,可以考虑使用 JobScheduler。

如果你真的要用 Service,可以考虑使用 IntentService,IntentService 是 Service 的一个子类,在它的内部有一个工作线程来处理耗时任务,当任务执行完后,IntentService 就会自动停止。

6.2.2 选择优化后的数据容器

Java 提供的部分数据容器并不适合 Android,比如 HashMap,HashMap 需要中存储每一个键值对都需要一个额外的 Entry 对象。

Android 提供了几个优化后的数据容器,包括 SparseArray、SparseBooleanArray 以及 LongSparseArray。

SparseArray和ArrayMap是Android专门针对内存优化而设计的取代 Java API 中的HashMap的数据结构。对于 key 是 int 类型则使用SparseArray,可避免自动装箱过程;对于 key 为其他类型则使用ArrayMap。HashMap的查找和插入时间复杂度为 O(1) 的代价是牺牲大量的内存来实现的,而SparseArray和ArrayMap性能略逊于HashMap,但更节省内存。

6.2.3 小心代码抽象

抽象可以优化代码的灵活性和可维护性,但是抽象也会带来其他成本。

抽象会导致更多的代码需要被执行,也就是需要更多的时间和把更多的代码映射到内存中。

如果某段抽象代码带来的好处不大,比如一个地方可以直接实现而不需要用到接口的,那就不用接口。

6.2.4 使用 protobuf 作为序列化数据

Protocol buffers 是 Google 设计的,它可以对结构化的数据序列化,与XML 类似,不过比XML 更小,更快,而且更简单。

如果你决定使用 protobuf 作为序列化数据格式,那在客户端代码中应该使用轻量级的 protobuf。

因为一般的 protobuf 会生成冗长的代码,这样会导致内存增加、APK 大小增加,执行速度变慢等问题。

更多关于 protobuf 的信息可以查看 protobuf readme 中的 “轻量级版本” 。

6.2.5 Apk 瘦身

有些资源和第三方库会在我们不知情的情况下大量消耗内存。

Bitmap 大小、资源、动画以及第三方库会影响到 APK 的大小,Android Studio 提供了 R8 和 ProGuard 帮助我们缩小 Apk,去掉不必要的资源。

如果你使用的 Android Studio 版本是 3.3 以下的,可以使用 ProGuard,3.3 及以上版本的可以使用 R8。

6.2.6 使用 Dagger2 进行依赖注入

依赖注入框架不仅可以简化我们的代码,而且能让我们在测试代码的时候更方便。

如果我们想在应用中使用依赖注入,可以考虑使用 Dagger2。

Dagger2 是在编译期生成代码,而不是用反射实现的,这样就避免了反射带来的内存开销,而是在编译期生成代码,

6.2.7 谨慎使用第三方库

当你决定使用一个不是为移动平台设计的第三方库时,你需要对它进行优化,让它能更好地在移动设备上运行。

这些第三方库包括日志、分析、图片加载、缓存以及其他框架,都有可能带来性能问题。

6.2.8 使用 IntDef 和 StringDef 替代枚举类型

使用枚举类型的 dex size 是普通常量定义的 dex size 的 13 倍以上,同时,运行时的内存分配,一个 enum 值的声明会消耗至少 20 bytes 。

枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。所以Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef 和 StringDef ,用来提供编译期的类型检查。

使用 IntDef和 StringDef 需要在 Gradle 配置中引入相应的依赖包:

1
compile 'com.android.support:support-annotations:22.0.0'

感谢

极客时间 Android开发高手课

慕课 Top团队大牛带你玩转Android性能分析与优化

探索 Android 内存优化方法

Android 性能优化(四)之内存优化实战

LeakCanary

AndroidDeveloper