mmat
English README
在开发 Android App 过程中我们通常使用LeakCanary进行内存泄漏检测, 它基本原理是在 App 运行时检测 Activity、Fragment 是否产生内存泄漏, 如果有内存泄漏则进行 dump hprof, 然后分析该泄漏的 GC ROOT, 最终通过通知栏通知用户内存泄漏的情况. 这种方式能够在开发 app 时简单、有效地找出内存泄漏.
但是由于LeakCanary是 app 运行时发现泄漏之后立即 dump 内存快照,并且实时进行内存分析, 而由于移动设备的计算能力有限, 导致如果内存泄漏较多时使用LeakCanary并不能在运行时分析出所有内存泄漏,例如当LeakCanary正在分析一次内存泄漏时又产生了另外的内存泄漏, 而在 LeakCanary 分析完所有内存泄漏之前用户可能已经退出了 app.
因此需要另外一种补充机制, 能够在 App 运行结束之后进行全面、自动地离线分析 app 内存泄漏, 一次性分析出本次 App 运行产生的所有 Activity、Fragment 的内存泄漏, 那么将会让内存泄漏分析更加全面、高效.
MMAT 就是为了解决这个问题, 它的核心思路是用户在操作完 app (可以是工程师自己操作,也可以通过运行 Monkey 进行随机操作)之后,通过adb shell命令将 app 退回到主页面, 然后再退回桌面 (此时应用的 Application 还存在, 但是所有 Activity 都应该被销毁, app 处于后台状态。如果此时还有 Activity、Fragment 实例, 那么代表产生了内存泄漏). 当 App 处于后台状态时 dump App 内存快照到 pc 上, 然后再通过 MMAT 进行离线分析, 最终得到内存泄漏的完整报告.
一、MMAT 工作流程

- 如果有配置 monkey 测试命令, 则执行 monkey 测试 (monkey 测试会使得 App 会随机进入各种 Activity, 这种压力测试也容易产生内存泄漏; )
- 1.1 执行 monkey 测试
- 1.2 回到 app 主页面
- 1.3 将 app 退到后台, 回到手机桌面
- 1.4 通过
adb shell kill -10命令执行 app 的 force gc (需要手机是 root)
- 如果你不想使用 monkey, 也可以自己手动操作 App, 完成所有操作之后将 App 退到后台
- 运行 MMAT, dump hprof 内存快照
- 分析 hprof, 得到所有 Activity、Fragment 泄漏的记录以及超过一定大小的 Bitmap 文件
- 将分析结果输出为 html 报告
MMAT Demo 演示请点击下面的视频:
二、使用 MMAT
注意: 因为 MMAT 在通过 monkey 操作 app 后会使用 adb 命令 dump 应用的内存信息, 因此如果你需要 dump release 版的 app 的内存信息, 请确保你的 app 在测试时可调试的, 即需要在 AndroidManifest.xml 的 application 节点中添加
android:debuggable="true", 请参考AndroidManifest.xml 配置 (风险提示: 建议只在测试时开启 debuggable=true, 对外发布的 apk 不要设置为 true).
使用 MMAT 有两种方式, 一种是通过 mmat gradle 插件、一种是直接执行 jar 文件, 请参考 2.1 章节 和 2.2 章节
2.1 通过 gradle plugin 使用 MMAT
- 在项目的 build.gradle 中添加
mmat-plugin引用;
示例如下:
buildscript {
repositories {
// ...
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
// add mmat plugin library
classpath 'org.mrcd:mmat-plugin:0.9.2'
}
}
- 在 app module 的 build.gralde 中添加 mmat-plugin 与配置
apply plugin: 'com.mrcd.mmat.plugin'
// 配置 mmat plugin
mmat {
// json config file
jsonConfigFile 'app/mmat-config.json'
// 是否禁用 monkey 测试
disableMonkey false
// 如果设定了该参数, MMAT 会直接分析该内存快照,monkey 脚本会被忽略
// hprofFile "your-hprof-file-path"
}
- 执行
./gradlew startMmatRunner进行 app 自动内存分析, 最终报告会存在被测试 app 的hprof_analysis/report/目录下, 报告示例参考Hprof Analysis Report.
2.2 通过 jar 文件使用 MMAT
将mmat-1.0.jar下载到项目的根目录, 另外在项目的根目录下添加mmat-config.json配置 (如何配置请参考mmat config 配置 ), 然后执行 mmat 的可执行 jar 文件. 例如我的测试项目路径是 /User/mrsimple/test-project/, 添加 mmat-config.json 文件,并且进行相关的配置, 最后安装要测试的 apk 之后, 在项目根目录下执行进入到如下命令:
java -jar mmat-1.0.jar /User/mrsimple/test-project/mmat-config.json
执行完之后即可在 /User/mrsimple/test-project/hprof-analysis/report 下看到内存分析的 html 报告. 如下图所示:

报告中第一行为报告的标题,第二行给出了运行测试的手机型号、系统版本等信息。再往后就是内存泄漏的记录列表,例如第一条记录为:
1. com.example.mmat.MemoryLeakActivity (0x12d552d0) instance (2.06 MB)
* leaked ==> com.example.mmat.MemoryLeakActivity (0x12d552d0) instance (2.06 MB)
item -> java.util.LinkedList$Node
first -> java.util.LinkedList
sActivityLeaked -> static com.example.mmat.MemoryLeakActivity
[15] -> array java.lang.Object[]
runtimeInternalObjects -> dalvik.system.PathClassLoader
contextClassLoader -> thread java.lang.Thread (named 'null')
其中 leaked ==> 后的 MemoryLeakActivity 就是泄漏的 Activity,它的内存地址为 0x12d552d0, 持有的内存大小为 2.06MB. 后续跟着的就是这个 Activity 实例的引用链, 通过分析这个引用链就能够找到造成内存泄漏的关键点。整个引用链的输出顺序是按照 MAT 的格式输出, 如果我们将本次得到的内存快照经过 hprof-conv 之后导入 MAT (也就是 Memory Analyzer Tool,我们常用来分析内存泄漏的工具, MMAT 就是基于 MAT 命名的.), 找到地址为 0x12d552d0 的 MemoryLeakActivity,得到的引用链如下图所示:

可以看到,我们通过MMAT输出的引用链与通过 MAT 得到的基本一致。
再回过头来分析上图中的第一条内存泄漏记录, 对于我们来说, 造成内存泄漏的关键点是static com.example.mmat.MemoryLeakActivity.sActivityLeaked (这个需要根据自己的代码以及报告中的信息具体分析),
表示的是 MemoryLeakActivity 类中的静态字段 sActivityLeaked 引用了这个MemoryLeakActivity实例. 从后续的引用链信息看, 这个sActivityLeaked应该是一个 LinkedList 类型, MemoryLeakActivity是它的其中一个元素.
我们再到 MemoryLeakActivity 中查看 sActivityLeaked 相关的代码, 如下所示:
/**
* 产生内存泄漏的页面
*/
public class MemoryLeakActivity extends AppCompatActivity {
private static List<Activity> sActivityLeaked = new LinkedList<>() ;
// ... 其他代码
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
// memory leaked
sActivityLeaked.add(this) ;
}
}
可以看到, 我们在 onCreate 函数中将 MemoryLeakActivity 添加到 sActivityLeaked 中, 但是没有在任何地方删除, 因此造成了内存泄漏, 去掉相关的代码即可解决这个内存泄漏.
其他的内存泄漏也是通过类似的方法根据报告进行分析即可.
其他重要配置
如果你不想通过 Monkey 操作 App, 也可以自己操作 App 进入、退出各个 Activity, 然后将 App 退到后台, 再将 mmat 配置文件中的monkey_command置空, 然后再执行 ./gradlew startMmatRunner, 此时就会直接 dump app 的内存快照, 然后进行后续的拉取内存快照以及分析内存的工作.
而如果你已经有了一份 App 的内存快照 hprof 文件, 你可以在 mmat 配置中执行 hprof 的路径 (即 hprofFile 参数),然后执行 ./gradlew startMmatRunner任务, MMAT 将直接分析这个 hprof 文件, 最终分析出该内存快照中的内存泄漏.
三、mmat-config.json 配置说明
package: 要测试的应用包名, 必填main_activity: 应用的主页面的类路径,必填 (需要在 AndroidManifest.xml 注册时添加exported=true)monkey_command: monkey 命令(字符串命令)或者 shell 脚本路径(完整的文件路径)enable_force_gc: dump hprof 文件之前是否对目标 App force gc (默认为 true)hprof_dir: dump 出来的 hprof 存放在 Android 设备中的根目录(默认为/sdcard/), Android 系统默认不允许访问/sdcard/时可以配置这个参数修改 hprof 存储路径, 否则无法adb pull hprof文件.detect_leak_classes: 要检测内存泄漏的类列表, 主要是 Activity、Fragment 的子类. 如果你没有其他的类型需要检测,则不需要修改这个配置;excluded_refs: 要排除的产生泄漏的对象 (例如 Android 系统自己的内存泄漏),只有弱引用和软引用的对象通常也要排除- class : 要排除的类名
- fields : 字段列表, 即排除某个类的某个字段产生的内存泄漏
- type: 代表是静态字段(static) 还是实例字段(instance)产生的内存泄漏
bitmap_report: bitmap 报告配置max_report_count: 报告中最多输出多少张图片, 设置为 -1 时不做数量限制min_width: 报告中如果图片的宽度小于这个值则不输出min_height: 报告中如果图片的高度小于这个值则不输出
mmat-config.json 示例如下:
{
"package": "com.example.mmat",
"main_activity": "com.example.mmat.MainActivity",
"monkey_command": "adb shell monkey -p com.example.mmat --ignore-crashes --ignore-timeouts --ignore-native-crashes --ignore-security-exceptions --pct-touch 40 --pct-motion 25 --pct-appswitch 10 --pct-rotation 5 -s 12358 -v -v -v --throttle 300 200",
// "monkey_command": "/User/mrsimple/test_monkey.sh", // monkey 脚本配置
"enable_force_gc": true,
"hprof_dir": "/data/local/tmp/",
"detect_leak_classes": [
"android.app.Activity",
"android.app.Fragment",
"android.support.v4.app.Fragment"
],
"excluded_refs": [
{
"class": "java.lang.ref.WeakReference",
"fields": ["referent"],
"type": "instance"
},
{
"class": "java.lang.ref.SoftReference",
"fields": ["referent"],
"type": "instance"
},
{
"class": "java.lang.ref.FinalizerReference",
"fields": ["referent"],
"type": "instance"
},
{
"class": "android.arch.lifecycle.ReportFragment",
"fields": [],
"type": "static"
}
],
"bitmap_report": {
"max_report_count": 20,
"min_width": 200,
"min_height": 200
}
}
上述示例中 Monkey 的事件数量只有 200 次, 真实的测试场景可以设置为更高的事件数, 例如我们的 Jenkins 平台中 Monkey 测试事件数会设置为几万次. 具体事件数量开发者可以根据情况进行设定, 但不应该设置为太低的事件数, 否则内存泄漏可能不容易出现.
四、AndroidManifest.xml application 配置样例
注意, 只在【测试时】添加这些配置即可 (可以通过 flavor 对不同版本进行不同的配置).
- application 中配置
android:debuggable="true", 这样确保在 debug、release 模式下可以通过 adb shell dump 内存数据 (hprof) - MainActivity (App 主页面) 中配置
android:exported="true"和android:launchMode="singleTask"android:exported="true": 保证可以通过 adb shell 启动主页面android:launchMode="singleTask": 保证通过 adb shell 启动主页面时, Activity 栈中的其他页面会被清除. 此时只有主页面在栈顶, 即除了主页面之外的其他 Activity 都应该已经销毁. 这样通过 Monkey 测试完之后, 再通过 adb shell 启动主页面后就只有主页面存在, 然后再通过 adb shell 将 app 退到后台, 此时主页面也会被销毁, 然后再 dump 内存快照, 再分析内存, 此时如果还存在 Activity、Fragment 则表示产生了内存泄漏.
示例如下:
<application
android:debuggable="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
License
Copyright (C) 2019 Mr.Simple <simplecoder.h@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

