修复内存泄漏
在开始监测 Activity 内存泄漏之前,Resource Canary 首先会尝试修复可能的内存泄漏问题,它是通过监听 ActivityLifeCycleCallbacks 实现的,在 Activity 回调 onDestroy 时,它会尝试解除 Activity 和 InputMethodManager、View 之间的引用关系:
public static void activityLeakFixer(Application application) {
application.registerActivityLifecycleCallbacks(new ActivityLifeCycleCallbacksAdapter() {
@Override
public void onActivityDestroyed(Activity activity) {
ActivityLeakFixer.fixInputMethodManagerLeak(activity);
ActivityLeakFixer.unbindDrawables(activity);
}
});
}
对于 InputMethodManager,它可能引用了 Activity 中的某几个 View,因此,将它和这几个 View 解除引用关系即可:
public static void fixInputMethodManagerLeak(Context destContext) {
final InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE);
final String[] viewFieldNames = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
for (String viewFieldName : viewFieldNames) {
final Field paramField = imm.getClass().getDeclaredField(viewFieldName);
...
// 如果 IMM 引用的 View 引用了该 Activity,则切断引用关系
if (view.getContext() == destContext) {
paramField.set(imm, null);
}
}
}
对于 View,它可能通过监听器或 Drawable 的形式关联 Activity,因此,我们需要把每一个可能的引用关系解除掉:
public static void unbindDrawables(Activity ui) {
final View viewRoot = ui.getWindow().peekDecorView().getRootView();
unbindDrawablesAndRecycle(viewRoot);
}
private static void unbindDrawablesAndRecycle(View view) {
// 解除通用的 View 引用关系
recycleView(view);
// 不同类型的 View 可能有不同的引用关系,一一处理即可
if (view instanceof ImageView) {
recycleImageView((ImageView) view);
}
if (view instanceof TextView) {
recycleTextView((TextView) view);
}
...
}
// 将 Listener、Drawable 等可能存在的引用关系切断
private static void recycleView(View view) {
view.setOnClickListener(null);
view.setOnFocusChangeListener(null);
view.getBackground().setCallback(null);
view.setBackgroundDrawable(null);
...
}
监测内存泄漏
具体的监测工作,ResourcePlugin 交给了 ActivityRefWatcher 来完成。
ActivityRefWatcher 主要的三个方法:start、stop、destroy 分别用于启动监听线程、停止监听线程、结束监听。以 start 为例:
public class ActivityRefWatcher extends FilePublisher implements Watcher, IAppForeground {
@Override
public void start() {
stopDetect();
final Application app = mResourcePlugin.getApplication();
if (app != null) {
// 监听 Activity 的 onDestroy 回调,记录 Activity 信息
app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
// 监听 onForeground 回调,以便根据应用可见状态修改轮询间隔时长
AppActiveMatrixDelegate.INSTANCE.addListener(this);
// 启动监听线程
scheduleDetectProcedure();
}
}
}
记录 Activity 信息
其中 mRemovedActivityMonitor 用于在 Activity 回调 onDestroy 时记录 Activity 信息,主要包括 Activity 的类名和一个根据 UUID 生成的 key:
// 用于记录 Activity 信息
private final ConcurrentLinkedQueue<DestroyedActivityInfo> mDestroyedActivityInfos;
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
@Override
public void onActivityDestroyed(Activity activity) {
pushDestroyedActivityInfo(activity);
}
};
// 在 Activity 销毁时,记录 Activity 信息
private void pushDestroyedActivityInfo(Activity activity) {
final String activityName = activity.getClass().getName();
final UUID uuid = UUID.randomUUID();
final String key = keyBuilder.toString(); // 根据 uuid 生成
final DestroyedActivityInfo destroyedActivityInfo = new DestroyedActivityInfo(key, activity, activityName);
mDestroyedActivityInfos.add(destroyedActivityInfo);
}
DestroyedActivityInfo 包含信息如下:
public class DestroyedActivityInfo {
public final String mKey; // 根据 uuid 生成
public final String mActivityName; // 类名
public final WeakReference<Activity> mActivityRef; // 弱引用
public int mDetectedCount = 0; // 重复检测次数,默认检测 10 次后,依然能通过弱引用获取,才认为发生了内存泄漏
}
启动监听线程
线程启动后,应用可见时,默认每隔 1min(通过 IDynamicConfig 指定) 将轮询任务发送到默认的后台线程(MatrixHandlerThread)执行:
// 自定义的线程切换机制,用于将指定的任务延时发送到主线程/后台线程执行
private final RetryableTaskExecutor mDetectExecutor;
private ActivityRefWatcher(...) {
HandlerThread handlerThread = MatrixHandlerThread.getDefaultHandlerThread();
mDetectExecutor = new RetryableTaskExecutor(config.getScanIntervalMillis(), handlerThread);
}
private void scheduleDetectProcedure() {
// 将任务发送到 MatrixHandlerThread 执行
mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
下面看轮询任务 mScanDestroyedActivitiesTask,它是一个内部类,代码很长,我们一点一点分析。
设置哨兵检测 GC 是否执行
首先,在上一篇文章关于原理的部分介绍过,ResourceCanary 会设置了一个哨兵元素,检测是否真的执行了 GC,如果没有,它不会往下执行:
private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {
@Override
public Status execute() {
// 创建指向一个临时对象的弱引用
final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
// 尝试触发 GC
triggerGc();
// 检测弱引用指向的对象是否存活来判断虚拟机是否真的执行了GC
if (sentinelRef.get() != null) {
// System ignored our gc request, we will retry later.
return Status.RETRY;
}
...
return Status.RETRY; // 返回 retry,这个任务会一直执行
}
};
private void triggerGc() {
Runtime.getRuntime().gc();
Runtime.getRuntime().runFinalization();
}
过滤已上报的 Activity
接着,遍历所有 DestroyedActivityInfo,并标记该 Activity,避免重复上报:
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
while (infoIt.hasNext()) {
if (!mResourcePlugin.getConfig().getDetectDebugger()
&& isPublished(destroyedActivityInfo.mActivityName) // 如果已标记,则跳过
&& mDumpHprofMode != ResourceConfig.DumpMode.SILENCE_DUMP) {
infoIt.remove();
continue;
}
if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) {
if (mResourcePlugin != null && !isPublished(destroyedActivityInfo.mActivityName)) { // 如果已标记,则跳过
...
}
if (null != activityLeakCallback) { // 但还会回调 ActivityLeakCallback
activityLeakCallback.onLeak(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey);
}
} else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) {
...
markPublished(destroyedActivityInfo.mActivityName); // 标记
} else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) {
...
markPublished(destroyedActivityInfo.mActivityName); // 标记
} else { // NO_DUMP
...
markPublished(destroyedActivityInfo.mActivityName); // 标记
}
}
多次检测,避免误判
同时,在重复检测大于等于 mMaxRedetectTimes 次时(由 IDynamicConfig 指定,默认为 10),如果还能获取到该 Activity 的引用,才会认为出现了内存泄漏问题:
while (infoIt.hasNext()) {
...
// 获取不到,Activity 已回收
if (destroyedActivityInfo.mActivityRef.get() == null) {
continue;
}
// Activity 未回收,可能出现了内存泄漏,但为了避免误判,需要重复检测多次,如果都能获取到 Activity,才认为出现了内存泄漏
// 只有在 debug 模式下,才会上报问题,否则只会打印一个 log
++destroyedActivityInfo.mDetectedCount;
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
|| !mResourcePlugin.getConfig().getDetectDebugger()) {
MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
+ "exists in %s times, wait for next detection to confirm.",
destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
continue;
}
}
需要注意的是,只有在 debug 模式下,才会上报问题,否则只会打印一个 log。
上报问题
对于 silence_dump 和 no_dump 模式,它只会记录 Activity 名,并回调 onDetectIssue:
final JSONObject resultJson = new JSONObject();
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
mResourcePlugin.onDetectIssue(new Issue(resultJson));
对于 manual_dump 模式,它会使用 ResourceConfig 指定的 Intent 生成一个通知:
...
Notification notification = buildNotification(context, builder);
notificationManager.notify(NOTIFICATION_ID, notification);
对于 auto_dump,它会自动生成一个 hprof 文件并对该文件进行分析:
final File hprofFile = mHeapDumper.dumpHeap();
final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
mHeapDumpHandler.process(heapDump);
生成 hprof 文件
dumpHeap 方法做了两件事:生成一个文件,写入 Hprof 数据到文件中:
public File dumpHeap() {
final File hprofFile = mDumpStorageManager.newHprofFile();
Debug.dumpHprofData(hprofFile.getAbsolutePath());
}
之后 HeapDumpHandler 就会处理该文件:
protected AndroidHeapDumper.HeapDumpHandler createHeapDumpHandler(...) {
return new AndroidHeapDumper.HeapDumpHandler() {
@Override
public void process(HeapDump result) {
CanaryWorkerService.shrinkHprofAndReport(context, result);
}
};
}
处理流程如下:
private void doShrinkHprofAndReport(HeapDump heapDump) {
// 裁剪 hprof 文件
new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);
// 压缩裁剪后的 hprof 文件
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile)));
copyFileToStream(shrinkedHProfFile, zos);
// 删除旧文件
shrinkedHProfFile.delete();
hprofFile.delete();
// 上报结果
CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
}
private void doReportHprofResult(String resultPath, String activityName) {
final JSONObject resultJson = new JSONObject();
resultJson.put(SharePluginInfo.ISSUE_RESULT_PATH, resultPath);
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityName);
Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class);
plugin.onDetectIssue(new Issue(resultJson));
}
可以看到,由于原始 hprof 文件很大,因此 Matrix 先对它做了一个裁剪优化,接着再压缩裁剪后的文件,并删除旧文件,最后回调 onDetectIssue,上报文件位置、Activity 名称等信息。
分析结果
示例
检测到内存泄漏问题后,ActivityRefWatcher 会打印日志如下:
activity with key [MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLeakActivity_...] was suspected to be a leaked instance. mode[AUTO_DUMP]
如果模式为 AUTO_DUMP,且设置了 mDetectDebugger 为 true,那么,还会生成一个 hprof 文件:
hprof: heap dump "/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/dump_*.hprof" starting...
裁剪压缩后在 /sdcard/data/[package name]/matrix_resource 文件夹下会生成一个 zip 文件,比如:
/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/dump_result_*.zip
zip 文件里包括一个 dump_*_shinked.hprof 文件和一个 result.info 文件,其中 result.info 包含设备信息和关键 Activity 的信息,比如:
# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !!
sdkVersion=23
manufacturer=vivo
hprofEntry=dump_323ff84d95424d35b0f62ef6a3f95838_shrink.hprof
leakedActivityKey=MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLeakActivity_8c5f3e9db8b54a199da6cb2abf68bd12
拿到这个 zip 文件,输入路径参数,执行 matrix-resource-canary-analyzer 中的 CLIMain 程序,即可得到一个 result.json 文件:
{
"activityLeakResult": {
"failure": "null",
"referenceChain": ["static sample.tencent.matrix.resource.TestLeakActivity testLeaks", ..., "sample.tencent.matrix.resource.TestLeakActivity instance"],
"leakFound": true,
"className": "sample.tencent.matrix.resource.TestLeakActivity",
"analysisDurationMs": 185,
"excludedLeak": false
},
"duplicatedBitmapResult": {
"duplicatedBitmapEntries": [],
"mFailure": "null",
"targetFound": false,
"analyzeDurationMs": 387
}
}
注意,CLIMain 在分析重复 Bitmap 时,需要反射 Bitmap 中的 "mBuffer" 字段,而这个字段在 API 26 已经被移除了,因此,对于 API 大于等于 26 的设备,CLIMain 只能分析 Activity 内存泄漏,无法分析重复 Bitmap。
分析过程
下面简单分析一下 CLIMain 的执行过程,它是基于 Square Haha 开发的,执行过程分为 5 步:
- 根据 result.info 文件拿到 hprof 文件、sdkVersion 等信息
- 分析 Activity 泄漏
- 分析重复 Bitmap
- 生成 result.json 文件并写入结果
- 输出重复的 Bitmap 图像到本地
public final class CLIMain {
public static void main(String[] args) {
doAnalyze();
}
private static void doAnalyze() throws IOException {
// 从 result.info 文件中拿到 hprof 文件、sdkVersion 等信息,接着开始分析
analyzeAndStoreResult(tempHprofFile, sdkVersion, manufacturer, leakedActivityKey, extraInfo);
}
private static void analyzeAndStoreResult(...) {
// 分析 Activity 内存泄漏
ActivityLeakResult activityLeakResult
= new ActivityLeakAnalyzer(leakedActivityKey, ).analyze(heapSnapshot);
// 分析重复 Bitmap
DuplicatedBitmapResult duplicatedBmpResult
= new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot);
// 生成 result.json 文件并写入结果
final File resultJsonFile = new File(outputDir, resultJsonName);
resultJsonPW.println(resultJson.toString());
// 输出重复的 Bitmap 图像
for (int i = 0; i < duplicatedBmpEntryCount; ++i) {
final BufferedImage img = BitmapDecoder.getBitmap(...);
ImageIO.write(img, "png", os);
}
}
}
Activity 内存泄漏检测的关键是找到最短引用路径,原理是:
- 根据 result.info 中的 leakedActivityKey 字段获取 Activity 结点
- 使用一个集合,存储与该 Activity 存在强引用的所有结点
- 从这些结点出发,使用宽度优先搜索算法,找到最近的一个 GC Root,GC Root 可能是静态变量、栈帧中的本地变量、JNI 变量等
重复 Bitmap 检测的原理在上一篇文章有介绍,这里跳过。
总结
Resource Canary 的实现原理
- 注册 ActivityLifeCycleCallbacks,监听 onActivityDestroyed 方法,通过弱引用判断是否出现了内存泄漏,使用后台线程(MatrixHandlerThread)周期性地检测
- 通过一个“哨兵”对象来确认系统是否进行了 GC
- 若发现某个 Activity 无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),且要求从该 Activity 被记录起有 2 个以上的 Activity 被创建才认为是泄漏(没发现对应的代码),以防在判断时该 Activity 被局部变量持有导致误判
- 不会重复报告同一个 Activity
Resource Canary 的限制
- 只能在 Android 4.0 以上的设备运行,因为 ActivityLifeCycleCallbacks 是在 API 14 才加入进来的
- 无法分析 Android 8.0 及以上的设备的重复 Bitmap 情况,因为 Bitmap 的 mBuffer 字段在 API 26 被移除了
可配置的选项
- DumpMode。有 no_dump(报告 Activity 类名)、silence_dump(报告 Activity 类名,回调 ActivityLeakCallback)、auto_dump(生成堆转储文件)、manual_dump(发送一个通知) 四种
- debug 模式,只有在 debug 模式下,DumpMode 才会起作用,否则会持续打印日志
- ContentIntent,在 DumpMode 模式为 manual_dump 时,会生成一个通知,ContentIntent 可指定跳转的目标 Activity
- 应用可见/不可见时监测线程的轮询间隔,默认分别是 1min、20min
- MaxRedetectTimes,只有重复检测大于等于 MaxRedetectTimes 次之后,如果依然能获取到 Activity,才认为出现了内存泄漏
修复内存泄漏
在监测的同时,Resource Canary 使用 ActivityLeakFixer 尝试修复内存泄漏问题,实现原理是切断 InputMethodManager、View 和 Activity 的引用
hprof 文件处理
- 在 debug 状态下,且 DumpMode 为 audo_dump 时,Matrix 才会在监测到内存泄漏问题后,自动生成一个 hprof 文件
- 由于原文件很大,因此 Matrix 会对该文件进行裁剪优化,并将裁剪后的 hprof 文件和一个 result.info 文件压缩到一个 zip 包中,result.info 包括 hprof 文件名、sdkVersion、设备厂商、出现内存泄漏的 Activity 类名等信息
- 拿到这个 zip 文件,输入路径参数,执行 matrix-resource-canary-analyzer 中的 CLIMain 程序,即可得到一个 result.json 文件,从这个文件能获取 Activity 的关键引用路径、重复 Bitmap 等信息
CLIMain 的解析步骤
- 根据 result.info 文件拿到 hprof 文件、Activity 类名等关键信息
- 分析 Activity 泄漏
- 分析重复 Bitmap
- 生成 result.json 文件并写入结果
- 输出重复的 Bitmap 图像到本地
最短路径查找
Activity 内存泄漏检测的关键是找到最短引用路径,原理是:
- 根据 result.info 中的 leakedActivityKey 字段获取 Activity 结点
- 使用一个集合,存储与该 Activity 存在强引用的所有结点
- 从这些结点出发,使用宽度优先搜索算法,找到最近的一个 GC Root,GC Root 可能是静态变量、栈帧中的本地变量、JNI 变量等
重复 Bitmap 的分析原理
把所有未被回收的 Bitmap 的数据 buffer 取出来,然后先对比所有长度为 1 的 buffer,找出相同的,记录所属的 Bitmap 对象;再对比所有长度为 2 的、长度为 3 的 buffer……直到把所有 buffer 都比对完,这样就记录了所有冗余的 Bitmap 对象。
