今天来分析下 Matrix 框架的 resource-canary-android 部分,相关文档可查看《 Matrix Android ResourceCanary 》,这份文档是阅读源码的关键,强烈建议先看文档再读源码。
ResourceCanary 仍然是借鉴的 LeakCanary,但 ResourceCanary 做了一点点的改进,这个地方查看文档的 细节与改进 部分,这里我贴一下减少误报的改进部分:
- 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
- 直接通过
WeakReference.get()
来判断对象是否已被回收,避免因延迟导致误判 - 若发现某个Activity无法被回收,再重复判断3次,且要求从该Activity被记录起有2个以上的Activity被创建才认为是泄漏,以防在判断时该Activity被局部变量持有导致误判
- 对已判断为泄漏的Activity,记录其类名,避免重复提示该Activity已泄漏
配置
分析的入口在 sample-android 项目的 MatrixApplication 当中,我们看一段初始化配置的代码:
Intent intent = new Intent(); ResourceConfig.DumpMode mode = ResourceConfig.DumpMode.AUTO_DUMP; intent.setClassName(this.getPackageName(), "com.tencent.mm.ui.matrix.ManualDumpActivity"); ResourceConfig resourceConfig = new ResourceConfig.Builder() .dynamicConfig(dynamicConfig) .setAutoDumpHprofMode(mode) .setDetectDebuger(true) // debug 时,记得打开,不然无法看到泄漏信息 .setNotificationContentIntent(intent) .build(); builder.plugin(new ResourcePlugin(resourceConfig)); ResourcePlugin.activityLeakFixer(this); ... Matrix.init(builder.build()); // 初始化所有的配有的插件 复制代码
Intent 的部分我们先不管,这个类在 sample 中其实并不存在,但在配置的过程中可以看到,他是一个点开通知展现的 Activity。
1、setAutoDumpHprofMode :设置 dump Hprof 的模式,他是一个枚举型:
- NO_DUMP :仅报告泄漏的 Activity,会重复提示
- AUTO_DUMP:生成 dump 文件并进行压缩,不会重复提示
- MANUAL_DUMP:仅报告泄漏信息生成通知,打开通知栏,跳转到配置的 Intent Activity,会重复提示
- SILENCE_DUMP:仅报告泄漏的 Activity,不会重复提示
2、setDetectDebuger
debug 模式下面建议打开,可以查看相关的泄漏信息,并且当前的源码分析也是基于 setDetectDebuger 为 true 的情况
3、ResourcePlugin.activityLeakFixer(this)
这个部分就不在源码中分析了,这里大致说下实现,在 onDestroy 时他会设置 Activity 与 InputMethodManager 相关联的字段为 null,并且从根布局中移除所有的 view 布局,这个部分也是比较容易产生泄漏的部分,特别是 InputMethodManager
4、Matrix.init(builder.build())
他会调用所有注册的插件的 init 方法,例如,ResourcePlugin 的 init 方法 :
mWatcher = new ActivityRefWatcher(app, this);
启动
在 MatrixApplication 当中有一段代码会启动所有配置的 Plugin ,内部会调用每个 plugin 的 start 方法
Matrix.with().startAllPlugins();
那么我们可以直接看 ResourcePlugin 的 start 方法:
@Override public void start() { super.start(); ... mWatcher.start(); } 复制代码
启动步骤非常简单,mWatcher 是在配置阶段中初始化的 ActivityRefWatcher
,那么,我们接下来分析查看 ActivityRefWatcher
的 start 方法 :
@Override public void start() { // 停止检测,多次调用 start,会停止上一个检测 stopDetect(); final Application app = mResourcePlugin.getApplication(); if (app != null) { // 注册 ActivityLifecycleCallbacks,只监听 onActivityDestroyed 方法 app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor); // 注册应用在前后台的监听 AppActiveMatrixDelegate.INSTANCE.addListener(this); // 启动定时器轮训检测 scheduleDetectProcedure(); } } 复制代码
接下来会分析 start 的整个过程,前后台的监听放在最后一个部分再讲
监测 Activity
registerActivityLifecycleCallbacks(mRemovedActivityMonitor) 注册获取 Activity 的整个创建流程,该处的代码正如文档 检测阶段 所言:找一个Activity被销毁时的必经调用点记录下当前Activity的信息,显然 Activity.onDestroy()
方法是一个不错的选择。
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() { @Override public void onActivityDestroyed(Activity activity) { pushDestroyedActivityInfo(activity); } }; private void pushDestroyedActivityInfo(Activity activity) { // 获取类名 final String activityName = activity.getClass().getName(); // 如果不是 debugger 并且已经上报过,则直接 return 不处理(这就是为啥要打开 debugger 的原因) if (!mResourcePlugin.getConfig().getDetectDebugger() && isPublished(activityName)) { MatrixLog.i(TAG, "activity leak with name %s had published, just ignore", activityName); return; } // 以下代码在下面分析 final UUID uuid = UUID.randomUUID(); final StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName) .append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits())); final String key = keyBuilder.toString(); final DestroyedActivityInfo destroyedActivityInfo = new DestroyedActivityInfo(key, activity, activityName); mDestroyedActivityInfos.add(destroyedActivityInfo); } 复制代码
在拿到 Activity 示例后,会给 Activity 生成一个唯一对应的 key,并将 key 与 WeakReference<>(activity) 绑定在 DestroyedActivityInfo 类中,然后存储到 Map,这个部分是和 LeakCanary 的做法是一致的,我们也可以查看 Matrix 的文档对于这块的说明:
我们可以通过创建一个持有已销毁的Activity的WeakReference,然后主动触发一次GC,如果这个Activity能被回收,则持有它的WeakReference会被置空,且这个被回收的Activity一段时间后会被加入事先注册到WeakReference的一个队列里。这样我们就能间接知道某个被销毁的Activity能否被回收了
存储过后,静静等待轮训分析
分析 Activity
scheduleDetectProcedure 方法会启动轮训来分析:
private void scheduleDetectProcedure() { mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask); } 复制代码
executeInBackground 会走到如下方法中:
private void postToBackgroundWithDelay(final RetryableTask task, final int failedAttempts) { mBackgroundHandler.postDelayed(new Runnable() { @Override public void run() { RetryableTask.Status status = task.execute(); if (status == RetryableTask.Status.RETRY) { postToBackgroundWithDelay(task, failedAttempts + 1); } } }, mDelayMillis); } 复制代码
最终会以间隔 mDelayMillis 毫秒进行轮训执行 RetryableTask 任务,当 RetryableTask.Status 为 RETRY 时停止,那么,我们来看看传入进来的 RetryableTask ,也就是 mScanDestroyedActivitiesTask 的 execute 方法
execute 方法的内容比较多,我们只分析 ResourceConfig.DumpMode == _AUTO_DUMP _的情况 :
private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() { @Override public Status execute() { // 如果监测 Map 没有数据,则返回 RETRY,这时候会一直空轮训 if (mDestroyedActivityInfos.isEmpty()) { return Status.RETRY; } // ①、此处的 sentinelRef 就是文档说的哨兵 final WeakReference<Object> sentinelRef = new WeakReference<>(new Object()); // ②、尝试手动触发一次 gc triggerGc(); // ③、如果弱引用为空,则说明 gc 触发了 if (sentinelRef.get() != null) { return Status.RETRY; } // 遍历 Map 实例 final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator(); while (infoIt.hasNext()) { final DestroyedActivityInfo destroyedActivityInfo = infoIt.next(); ... // 如果 WeakReference<>(activity) 为空,则 continue if (destroyedActivityInfo.mActivityRef.get() == null) { // The activity was recycled by a gc triggered outside. continue; } // 累加监测次数 ++destroyedActivityInfo.mDetectedCount; // ④、尽管哨兵告诉我们 mActivityRef 应该已经被回收,系统仍然可以忽略它,所以请重试直到达到最大重试时间。 if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes || !mResourcePlugin.getConfig().getDetectDebugger()) { continue; } if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) { // 将泄漏的类名回调给 onDetectIssue 和 onLeak } else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) { // ⑤、生成 dump 文件,详细操作后面说 final File hprofFile = mHeapDumper.dumpHeap(); if (hprofFile != null) { // 标记该类已经泄漏过 markPublished(destroyedActivityInfo.mActivityName); final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName); // ⑥、将 dump 信息进行压缩、上报 mHeapDumpHandler.process(heapDump); // 从监测 Map 中移除该实例 infoIt.remove(); } else { infoIt.remove(); } } else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) { // 将泄漏的类名发送到通知 } else { // 将泄漏的类名回调给 onDetectIssue } } // 无论最终结果是什么,都会返回 RETRY ,一直轮训 return Status.RETRY; } }; 复制代码
① 哨兵
这个哨兵是判断 gc 真正触发的条件,为什么要做哨兵呢,正如文档所言:
VM并没有提供强制触发GC的API,通过
System.gc()
或Runtime.getRuntime().gc()
只能“建议”系统进行GC,如果系统忽略了我们的GC请求,可回收的对象就不会被加入ReferenceQueue
#### ② 手动 gc 虽然 `Runtime.getRuntime().gc()` 无法保证,但还是要手动去触发一下 gc
③ 哨兵是否被回收
如果哨兵被回收了,则说明 gc 被真正触发了,如果未回收,则让轮训接着重试
④ 累计检测次数
这个地方是有一句英文注释的,主要是为了延长检测的时间,哨兵并不能是他的唯一条件
Although the sentinel tell us the activity should have been recycled,system may still ignore it, so try again until we reach max retry times.
⑤ dump 泄漏实例
mHeapDumper 是 AndroidHeapDumper 类,在 ComponentFactory 中有提供,然后来看看 dumpHeap 方法:
public File dumpHeap() { // 通过存储 Manager 来生成一个 File 文件 final File hprofFile = mDumpStorageManager.newHprofFile(); ... try { // 将 dump 信息存储到 hprofFile 中 Debug.dumpHprofData(hprofFile.getAbsolutePath()); ... // 返回 dump file return hprofFile; } catch (Exception e) { MatrixLog.printErrStackTrace(TAG, e, "failed to dump heap into file: %s.", hprofFile.getAbsolutePath()); return null; } } 复制代码
⑥ 压缩、上报 dumpFile
mHeapDumpHandler 是 AndroidHeapDumper.HeapDumpHandler 类,它也是在 ComponentFactory 提供,然后我们来看看 process 方法:
protected AndroidHeapDumper.HeapDumpHandler createHeapDumpHandler(final Context context, ResourceConfig resourceConfig) { return new AndroidHeapDumper.HeapDumpHandler() { @Override public void process(HeapDump result) { CanaryWorkerService.shrinkHprofAndReport(context, result); } }; } 复制代码
然后继续跟踪 shrinkHprofAndReport 方法:
public static void shrinkHprofAndReport(Context context, final HeapDump heapDump) { final Intent intent = new Intent(context, CanaryWorkerService.class); intent.setAction(ACTION_SHRINK_HPROF); intent.putExtra(EXTRA_PARAM_HEAPDUMP, heapDump); enqueueWork(context, CanaryWorkerService.class, JOB_ID, intent); } 复制代码
最终会走到父类 MatrixJobIntentService 的 enqueueWork 静态方法,该方法最终会是启动一个 service 服务
Intent intent = new Intent(work); intent.setComponent(mComponentName); try { if (mContext.startService(intent) != null) { ... } } 复制代码
注意:这个地方的启动服务是一个后台服务,从 Android 8.0 开始是禁止了后台服务的启动,所以,在运行该 Demo 时,你会发现 logcat 的报错提示:
Reject service :Intent { cmp=sample.tencent.matrix/com.tencent.matrix.resource.CanaryWorkerService } userId : 0 uid : 10269
为了能使代码运行下去,我们可以更改策略,采用依赖源码 library 的方式,我们自己来修改源码,摈弃 service :
public static void shrinkHprofAndReport(Context context, final HeapDump heapDump) { // final Intent intent = new Intent(context, CanaryWorkerService.class); // intent.setAction(ACTION_SHRINK_HPROF); // intent.putExtra(EXTRA_PARAM_HEAPDUMP, heapDump); // enqueueWork(context, CanaryWorkerService.class, JOB_ID, intent); new Thread(){ @Override public void run() { super.run(); CanaryWorkerService.doShrinkHprofAndReport(heapDump); } }.start(); } 复制代码
我们直接注释掉原来 startService 的方式,直接在子线程开始分析 heapDump 文件
我们来看看 CanaryWorkerService.doShrinkHprofAndReport :
private static void doShrinkHprofAndReport(HeapDump heapDump) { final File hprofDir = heapDump.getHprofFile().getParentFile(); // 生成裁剪后的 shrinkedHProfFile final File shrinkedHProfFile = new File(hprofDir, getShrinkHprofName(heapDump.getHprofFile())); // 生成和 dumpFile 相同目录的 zip 文件 final File zipResFile = new File(hprofDir, getResultZipName("dump_result_" + android.os.Process.myPid())); final File hprofFile = heapDump.getHprofFile(); ZipOutputStream zos = null; try { long startTime = System.currentTimeMillis(); // 开始裁剪 hprofFile,并将裁剪结果写入 shrinkedHProfFile new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile); // 开始压缩裁剪后的 HProfFile 文件,并写入元信息 zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile))); final ZipEntry resultInfoEntry = new ZipEntry("result.info"); final ZipEntry shrinkedHProfEntry = new ZipEntry(shrinkedHProfFile.getName()); zos.putNextEntry(resultInfoEntry); final PrintWriter pw = new PrintWriter(new OutputStreamWriter(zos, Charset.forName("UTF-8"))); pw.println("# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !!"); pw.println("sdkVersion=" + Build.VERSION.SDK_INT); pw.println("manufacturer=" + Build.MANUFACTURER); pw.println("hprofEntry=" + shrinkedHProfEntry.getName()); pw.println("leakedActivityKey=" + heapDump.getReferenceKey()); pw.flush(); zos.closeEntry(); zos.putNextEntry(shrinkedHProfEntry); copyFileToStream(shrinkedHProfFile, zos); zos.closeEntry(); // 删除裁剪后的 HProfFile shrinkedHProfFile.delete(); // 删除 hprofFile hprofFile.delete(); // CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName()); } catch (IOException e) { MatrixLog.printErrStackTrace(TAG, e, ""); } finally { closeQuietly(zos); } } 复制代码
我们来解释下 doShrinkHprofAndReport 做了哪些事情:
- 根据 dumpFile 文件路径创建 裁剪后的 shrinkedHProfFile 文件和待压缩文件
- 根据 dumpFile 文件开始裁剪,将裁剪的内容写入 shrinkedHProfFile
- 压缩 shrinkedHProfFile 文件,并写入相关元信息
- 删除裁剪文件
- 删除 dumpFile 文件
在运行 Matrix 的 TestLeakActivity 代码时,你就会发现,在应用的 external 目录下面会生成对应的 zip 文件:
至此,整个 ResourceCanary 的 dump 部分就结束了。当然,大家可以看到最后部分有我注释的代码:
CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
他仍然是一个 startService,然后将 zip 信息回调给 onDetectIssue
应用前后台运行的监听
如何将应用优化做到极致呢?当然是条件分类处理,在剖析 ResourceCanary 的源码时,会发现轮训间隔的时间是 2s,在 DynamicConfigImplDemo 中即可看到,那么,如果我们应用切换到了后台,那还有必要一直 2s 的去轮训检测吗?当然不需要,这就是注册前后台监听的原因,我们来看看最终会调用到的 onForeground 方法:
if (isForeground) { // 前台 mDetectExecutor.clearTasks(); mDetectExecutor.setDelayMillis(mFgScanTimes); mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask); } else { // 后台 mDetectExecutor.setDelayMillis(mBgScanTimes); } 复制代码
- 前台:
当应用切回到前台时,设置一下前台延时时间 2s,并立马执行任务
- 后台:
当应用切回到后台时,设置一下后台延时时间 20 分钟,这样,空轮训在拿到 mDelayMillis 时间时,就会等到 mDelayMillis 时间到时再执行
分析泄漏文件
由于 ResourceCanary 是客户端采集,服务端进行分析,所以,分析泄漏文件的源码是在 matrix-resource-canary-analyzer 下面,我们将上面生成的 zip 文件保存任意的盘符,然后打开 matrix-resource-canary-analyzer 的 CLIMain
类,我们修改一部分的代码:
public static void main(String[] args) { // if (args.length == 0) { // printUsage(System.out); // System.exit(ERROR_NEED_ARGUMENTS); // } String arr[]=new String[4]; arr[0]="-i"; // zip 文件 arr[1] ="/Users/codelang/Desktop/matrix/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/dump.zip"; arr[2] ="-o"; // 输出结果的目录 arr[3] ="/Users/codelang/Desktop/matrix/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer"; try { final CommandLine cmdline = new DefaultParser().parse(sOptions, arr); if (cmdline.hasOption(OPTION_HELP.mOption.getLongOpt())) { printUsage(System.out); System.exit(ERROR_SUCCESS); } ... } 复制代码
运行 main 方法,会在输出结果目录看到一个 result.json
文件:
{ "activityLeakResult": { "failure": "null", "referenceChain": [ "android.os.HandlerThread contextClassLoader", "dalvik.system.PathClassLoader runtimeInternalObjects", "array java.lang.Object[] [594]", "static sample.tencent.matrix.resource.TestLeakActivity testLeaks", "java.util.HashSet map", "java.util.HashMap table", "array java.util.HashMap$Node[] [12]", "java.util.HashMap$Node key", "sample.tencent.matrix.resource.TestLeakActivity instance" ], "leakFound": true, "className": "sample.tencent.matrix.resource.TestLeakActivity", "analysisDurationMs": 522, "excludedLeak": false }, "duplicatedBitmapResult": { "duplicatedBitmapEntries": [], "mFailure": "null", "targetFound": false, "analyzeDurationMs": 0 } } 复制代码
该文件信息就是整个分析的结果,根据 TestLeakActivity 泄漏的代码处与之对应,还是非常清晰的还原泄漏原因的
注:对 bitmap 的分析,需要在低于 26 的 sdk version 中才有用,报错信息如下
! SDK version of target device is larger or equal to 26, which is not supported by DuplicatedBitmapAnalyzer.