Android App内存泄漏自动化分析工具 - MMAT发布

在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进行离线分析, 最终得到内存泄漏的完整报告.

一、MMAT 工作流程

  1. 如果有配置monkey测试命令, 则执行monkey测试 (monkey测试会使得App会随机进入各种Activity, 这种压力测试也容易产生内存泄漏; )
    • 1.1 执行monkey测试
    • 1.2 回到app 主页面
    • 1.3 将app退到后台, 回到手机桌面
    • 1.4 通过adb shell kill -10 命令执行app的force gc (需要手机是root)
  2. 如果你不想使用monkey, 也可以自己手动操作App, 完成所有操作之后将App退到后台
  3. 运行MMAT, dump hprof内存快照
  4. 分析hprof, 得到所有Activity、Fragment泄漏的记录以及超过一定大小的Bitmap文件
  5. 将分析结果输出为html报告

点击这里进入MMAT Github项目, Demo演示请点击下面的视频:

二、使用MMAT

使用MMAT有两种方式, 一种是通过mmat gradle插件、一种是直接执行jar文件, 请参考 2.1章节2.2章节

2.1 通过 gradle plugin 使用MMAT

  • 2.1.1 在项目的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.1'
    }
}
  • 2.1.2 在app module 的 build.gralde 中添加mmat-plugin与配置
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"
}
  • 2.1.3 执行 ./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命名的), 找到地址为0x12d552d0MemoryLeakActivity,得到的引用链如下图所示:

可以看到,我们通过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),你可以把简历发到我的邮箱simplecoder.h@gmail.com, 我们期待你的加入.

另外,哈啰出行正在招聘一名Android高级专家的岗位, 薪资 + 期权每年超过百万,如果你对自己的技术充满信心, 也可以将简历发到上述的邮箱中.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值