在Android App开发过程中空指针
和内存泄漏
是影响性能、稳定性的两座大山, Kotlin
的出现在很大程度上避免了空指针导致的Crash, 而几年前LeakCanary
的出现也大大的提高了Android工程师查找内存泄漏的效率。两年前, 我在初创团队的Android应用质量保障之道 一文中阐述了我们团队如何改造LeakCanary
实现自动分析App内存泄漏的方案.
通过LeakCanary
查找内存泄漏的基本原理是在App运行时注册ActivityLifecycleCallbacks、FragmentLifecycleCallbacks, 在Activity、Fragment的onDestroy
函数执行的一段时间之后检测Activity、Fragment对象是否还存在, 如果还存在内存中则代表产生了内存泄漏。如果有内存泄漏则进行dump hprof, 然后分析该泄漏的GC ROOT, 最终通过通知栏通知用户内存泄漏的情况. 这种方式能够在开发app时简单、有效地找出内存泄漏.
LeakCanary 1.6.2 相关核心逻辑如下(只简单列出核心代码, 不作详细分析):
public final class ActivityRefWatcher {
public static void install(Context context, RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
// 注册 ActivityLifecycleCallbacks
application.registerActivityLifecycleCallbacks(
activityRefWatcher.lifecycleCallbacks);
}
// Activity ActivityLifecycleCallbacks 回调
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
// Activity调用了onDestroy之后调用RefWatcher的watch函数
refWatcher.watch(activity);
}
};
}
RefWatcher 的watch函数核心代码如下:
public void watch(Object watchedReference) {
watch(watchedReference, "");
}
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
// 省略代码
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
// 检测一段时间之后Activity或者Fragment引用是否被回收
ensureGoneAsync(watchStartNanoTime, reference);
}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
// 检测引用是否被回收
return ensureGone(reference, watchStartNanoTime);
}
});
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
// 代码省略
// 强制触发GC
gcTrigger.runGc();
removeWeaklyReachableReferences();
// 如果引用没有被销毁, 则dump 内存快照,然后分析内存快照找到GC ROOT
if (!gone(reference)) {
// dump 内存快照
File heapDumpFile = heapDumper.dumpHeap();
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
// 分析内存快照
heapdumpListener.analyze(heapDump);
}
return DONE;
}
但是由于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进行离线分析, 最终得到内存泄漏的完整报告.
adb shell kill -10
命令执行app的force gc (需要手机是root)点击这里进入MMAT Github项目, Demo演示请点击下面的视频:
使用MMAT有两种方式, 一种是通过mmat gradle插件、一种是直接执行jar文件, 请参考 2.1章节 和 2.2章节
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.1'
}
}
apply plugin: 'com.mrcd.mmat.plugin'
// 配置 mmat plugin
mmat {
// json config file (在github的项目README中会有配置说明)
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.
将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中, 但是没有在任何地方删除, 因此造成了内存泄漏, 去掉相关的代码即可解决这个内存泄漏.
至此,整个自动化的内存泄漏分析就已经完成. 我们需要做的就是对app、MMAT进行一些相关的配置,然后执行 ./gradlew startMmatRunner
任务, 任务结束之后就会得到App的完整内存泄漏记录以及该内存泄漏的GC ROOT.
如果你不想通过Monkey操作App, 也可以自己操作App进入、退出各个Activity, 然后将App退到后台, 再将 mmat 配置文件中的monkey_command
置空, 然后再执行 ./gradlew startMmatRunner
, 此时就会直接dump app的内存快照, 然后进行后续的拉取内存快照以及分析内存的工作.
而如果你已经有了一份App的内存快照hprof文件, 你可以在mmat配置中执行 hprof的路径 (即 hprofFile
参数),然后执行 ./gradlew startMmatRunner
任务, MMAT将直接分析这个 hprof文件, 最终分析出该内存快照中的内存泄漏.
如何配置请参考mmat config配置
MMAT可以与LeakCanary一起协作, LeakCanary用于在App运行时实时检测内存泄漏, 而 MMAT 可以在App开发到达一定阶段之后进行整体性测试,通过 Monkey的压力测试使得可能造成内存泄漏的隐患尽早的暴露,然后通过MMAT将这些内存泄漏一网打尽. 如何将MMAT的分析任务放在Jenkins 等CI平台上周期性的执行,那么将进一步减少人工干预,保证内存泄漏检测能够自动、定时的执行,让你的App慢慢摆脱内存泄漏的阴影。
MMAT github
主页在这里 , 欢迎各路豪杰提交代码、提供好的想法。
如果你也喜欢探索Android开发中的各类技术,对Android App开发充满热情,而恰好你在寻找一份有良好开发学习氛围的工作 (工作地点: 北京, 公司名: 北京明日虫洞科技有限公司, 薪资范围: 20k ~ 35K),你可以把简历发到我的邮箱[email protected], 我们期待你的加入.
另外,哈啰出行正在招聘一名Android高级专家的岗位, 薪资 + 期权每年超过百万,如果你对自己的技术充满信心, 也可以将简历发到上述的邮箱中.