Matrix---ResourceCanary源码分析


一、Matrix内存泄漏检测

Hprof文件中包含了Dump时刻内存中的所有对象的信息, 包括类的描述、实例的数据和引用关系、线程的栈信息等.

分析Matrix–ResourceCanary模块的代码时, 注意一定要与LeakCanary对比着分析, 然后顺便思考ResourceCanary为什么要这样设计, 弥补了LeakCanary上的哪些问题.
切记中间千万不要深入到每一行代码的细节, 这样太浪费精力了, 事倍功半, 平时在阅读源码时就陷入到这个细节里面了, 精力被这些不重要的细节代码占用, 导致没有更多的精力去分析经典代码

1.1 监听Activity的onDestroy
private void pushDestroyedActivityInfo(Activity activity) {
	// 获取当前执行Activity的名称
    final String activityName = activity.getClass().getName();
    // 为每一个Activity分配一个唯一标识
    final UUID uuid = UUID.randomUUID();
	final StringBuilder keyBuilder = new StringBuilder();
	// 对唯一标识进行处理, 顺便添加Matrix标识, 用于后续GC Roots
	keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName)
              .append('_')
              .append(Long.toHexString(uuid.getMostSignificantBits()))
              .append(Long.toHexString(uuid.getLeastSignificantBits()));
	final String key = keyBuilder.toString();
	// 构造一个对象持有该Activity的引用(弱引用持有)
 	final DestroyedActivityInfo destroyedActivityInfo
            = new DestroyedActivityInfo(key, activity, activityName);
    // 这里是与LeakCanary不同的地方
	mDestroyedActivityInfos.add(destroyedActivityInfo);
}

  Activity执行onDestroy之后, Matrix的这段逻辑, 只有最后一段逻辑与LeakCanary不同, 前面的代码只是写法上与LeakCanary不同, 都是生成唯一标识并添加自身独有的一个标识, 用于后续的GC Roots, 然后将执行了onDestroy的Activity与一个弱引用对象进行关联.
  不同的是LeakCanary将Activity与弱引用绑定之后, 将弱引用与引用队列关联, 然后开始通过引用队列判断Activity是否被回收. 接下来看Matrix是如何处理

2.2 ResourceCanary如何判断Activity泄漏

将destroyedActivityInfo添加到mDestroyedActivityInfos, 就没有下文了, 那么必然有一个地方会对mDestroyedActivityInfos进行 读操作
postToBackgroundWithDelay 递归调用

private void postToBackgroundWithDelay(final RetryableTask task, final int failedAttempts) {
    mBackgroundHandler.postDelayed(new Runnable() {
		@Override
		public void run() {
			// task执行返回RETRY, 重试
			RetryableTask.Status status = task.execute();
			if (status == RetryableTask.Status.RETRY) {
				// 如果重试, 次数+1, 递归调用
				postToBackgroundWithDelay(task, failedAttempts + 1);
            }
        }
    }, mDelayMillis);
}
2.3 mScanDestroyedActivitiesTask内存泄漏检测

这代码实在是太长了

public Status execute() {
    // If destroyed activity list is empty, just wait to save power.
    // 如果当前集合为空, 返回RETRY, 但是结合重试机制, 这里返回RETRY之后, 又会递归调用
    // execute, 为何这里没有考虑使用阻塞方式呢?
	if (mDestroyedActivityInfos.isEmpty()) {
        MatrixLog.i(TAG, "DestroyedActivityInfo isEmpty!");
        return Status.RETRY;
    }
	final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
    triggerGc();
    // 1.这里的哨兵就比较秀了, LeakCanary采用调用gc然后延迟100ms判断引用队列是否有数据来判断
    // Activity是否被回收, 但是正如LeakCanary调用gc处的注释所说, 显示调用gc并不一定会
    // 触发GC的执行, 但是从实际情况来看, 调用gc时, 通常会触发gc的执行
    // 2.这里很秀的一点就是加一个只要GC被执行就一定会被回收的对象, 所以这里判断sentinelRef
    //	 是否为空可以得知GC是否执行
	if (sentinelRef.get() != null) {
		// 说明GC并没有被触发, 重试
		return Status.RETRY;
	}
    final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
    while (infoIt.hasNext()) {
		final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
		if (!mResourcePlugin.getConfig().getDetectDebugger() && isPublished(destroyedActivityInfo.mActivityName) && mDumpHprofMode != ResourceConfig.DumpMode.SILENCE_DUMP) {
			MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
            infoIt.remove();
            continue;
        }
        if (destroyedActivityInfo.mActivityRef.get() == null) {
            // 说明GC被触发
            infoIt.remove();
        	continue;
        }
        ++destroyedActivityInfo.mDetectedCount;
        // 执行到这里说明GC确实被触发了, 并且执行了onDestroy的Activity也没有被回收, 
        // 针对每一个泄漏的Activity都设置一个阈值, 如果未超过这个阈值, RETRY, 如果超过了
        // 继续向下
        if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
                    || !mResourcePlugin.getConfig().getDetectDebugger()) {
			continue;
		}
		// Activity执行了onDestroy, 没有被GC回收, 重试次数超过阈值
        if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) {
        	// 记录Activity并回调该Activity名称
        } 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);
                // 生成该文件之后, 处理该HPROF文件
                mHeapDumpHandler.process(heapDump);
                // 移除Activity
                infoIt.remove();
            } else {
            	infoIt.remove();
            }
        } else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) {
            // 生成通知
		} else {
            // 记录Activity名称并回调
		}
	}
	return Status.RETRY;
}

二、Matrix HPROF文件的分析

采集到当前进程的内存快照之后, 接下来就是对该内存快照进行分析, 方法调用链省略

2.1 HPROF文件分析入口doShrinkHprofAndReport
private void doShrinkHprofAndReport(HeapDump heapDump) {
	final File hprofDir = heapDump.getHprofFile().getParentFile();
    final File shrinkedHProfFile = new File(hprofDir, getShrinkHprofName(heapDump.getHprofFile()));
    final File zipResFile = new File(hprofDir, getResultZipName("dump_result_" + android.os.Process.myPid()));
    final File hprofFile = heapDump.getHprofFile();
    ZipOutputStream zos = null;
    long startTime = System.currentTimeMillis();
    // HPROF文件分析
    new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);
    // 分析完成之后将文件上传
}
2.2 HPROF文件分析

  Matrix的HPROF文件裁剪功能的目标是将Bitmap和String之外的所有对象的基础类型数组的值移除, 因为Hprof文件的分析功能只需要用到 字符串数组(这里目前不懂为何只需要用到字符串数组就可以了, 需要再学习GC Roots相关的知识) 和Bitmap的buffer数组. 另一方面, 如果存在不同的Bitmpa对象其buffer数组值相同的情况, 则可以将它们指向同一个buffer, 以进一步减小文件尺寸. 裁剪后的Hprof文件通常比源文件小1/10以上.

  代码结构和ASM很像, 主要由 HprofReaderHprofVisitorHprofWriter 组成, 分别对应ASM中的ClassReader、ClassVisitor、ClassWriter.

HprofReader: 读取Hprof文件中的数据, 每读取到一种类型(使用TAG区分)的数据, 就交给一系列的HprofVisitor处理
HprofVisitor: 输出裁剪后的文件(HprofWriter继承自HprofVisitor)

2.3 HprofBufferShrinker.shrink
public void shrink(File hprofIn, File hprofOut) throws IOException {
    FileInputStream is = null;
    OutputStream os = null;
    is = new FileInputStream(hprofIn);
    os = new BufferedOutputStream(new FileOutputStream(hprofOut));
    // 读取文件
    final HprofReader reader = new HprofReader(new BufferedInputStream(is));
    // 第一遍读取
    reader.accept(new HprofInfoCollectVisitor());
    // 第二遍读取
    is.getChannel().position(0);
    reader.accept(new HprofKeptBufferCollectVisitor());
    // 第三遍读取
    is.getChannel().position(0);
    reader.accept(new HprofBufferShrinkVisitor(new HprofWriter(os)));
}

Matrix为了完成裁剪功能, 需要对输入的hprof文件重复读取 三次, 每次都由一个对应的Visitor处理.

为什么是三次? 带着疑问阅读接下来的代码

  • 1、第一个 Visitor(HprofInfoCollectVisitor): 用于记录Bitmap和String类信息, 包括字符串ID、Class ID以及它们拥有的字段.
  • 2、第二个 Visitor(HprofKeptBufferCollectVisitor): 用于记录所有String对象的value ID以及Bitmap对象的Buffer Id与其对应的数组本身.
  • 3、第三个 Visitor(HprofBufferShrinkVisitor): 该HprofBufferShrinkVisitor构造函数中传入了一个HprofWriter, 用于输出裁剪后的Hprof文件, 流程为 HprofReader读取到数据之后就由HprofWriter原封不动的输出到新的文件即可.

在分析三个HprofVisitor之前再来巩固一下Hprof的结构

2.3.1 文件头
长度含义
[u1]*以null结尾的一串字节, 用于表示格式名称及版本, 比如java profile 1.0.1(由18个u1字节组成)
u4size of identifiers, 即字符串、对象、堆栈等信息的id的长度(很多record的具体信息需要通过id来查找)
u8时间戳, 1970/1/1以来的毫秒数
2.3.2 文件内容

文件内存由一系列records组成, 每一个record包含如下信息

长度含义
u1TAG, 表示record类型
u4TIME, 时间戳, 相对文件头中的时间戳的毫秒数
u4LENGTH, 即BODY的字节长度
u8BODY, 具体内容
2.4 HprofReader.accept读取Hprof文件
public void accept(HprofVisitor hv) throws IOException {
	// 读取Hprof文件头
    acceptHeader(hv);
    acceptRecord(hv);
    hv.visitEnd();
}
2.4.1 HprofReader.acceptHeader读取文件头
private void acceptHeader(HprofVisitor hv) throws IOException {
	// 1.对照着文件头的格式: 读取数据, 直到读取到null
    final String text = IOUtil.readNullTerminatedString(mStreamIn);
    // 2.读取id的长度, 4个字节
    final int idSize = IOUtil.readBEInt(mStreamIn);
    // 3.读取时间戳, 8个字节
    final long timestamp = IOUtil.readBELong(mStreamIn);
    mIdSize = idSize;
    // 通知Visitor
    hv.visitHeader(text, idSize, timestamp);
}
2.5 HprofInfoCollectVisitor第一个HprofVisitor

摘抄自Matrix: 通过分析Hprof文件格式可知, Hprof文件中buffer区存放了所有对象的数据, 包括字符串数据、所有的数组等, 而我们的分析过程却只需要用到部分字符串数据和Bitmap的buffer数组, 其余的buffer数据都可以直接剔除, 这样处理之后的Hprof文件通常能比原始文件小1/10以上.

2.5.1 HprofReader.accpetRecord读取record

tag类型太多, 以String为例进行分析, 在分析之前先了解tag_string的结构:
在这里插入图片描述

private void acceptRecord(HprofVisitor hv) throws IOException {
	while (true) {
		// 1.读取BODY的类型tag
		final int tag = mStreamIn.read();
		// 2.读取时间戳
		final int timestamp = IOUtil.readBEInt(mStreamIn);
		// 3.读取BODY的长度
		final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL;
		switch (tag) {
			// 4.BODY类型为String时, 对应HPROF Agent文件中的0x01
			case HprofConstants.RECORD_TAG_STRING:
				acceptStringRecord(timestamp, length, hv);
            break;
        }
    }
}

private void acceptStringRecord(int timestamp, long length, HprofVisitor hv) {
	// 1.读取String对应的id
    final ID id = IOUtil.readID(mStreamIn, mIdSize);
    // 2.读取BODY内容, 0x01时BODY对应的是String的内容
    // BODY字节长度减去IdSize剩下的就是字符串内容
    final String text = IOUtil.readString(mStreamIn, length - mIdSize);
    hv.visitStringRecord(id, text, timestamp, length);
}
2.5.2 HprofVisitor.visitStringRecord处理目标数据

为了完成第一个Visitor的裁剪目标, 首先需要找到Bitmap及String类, 及其内部的mBuffer、value字段, 这也是裁剪流程中的第一个Visitor的作用: 记录Bitmap和String类信息

// 找到Bitmap、String类及其内部字段的字符串id
public void visitStringRecord(ID id, String text, int timestamp, long length) {
	if (mBitmapClassNameStringId == null && "android.graphics.Bitmap".equals(text)) {
		mBitmapClassNameStringId = id;
    } else if (mMBufferFieldNameStringId == null && "mBuffer".equals(text)) {
 		mMBufferFieldNameStringId = id;
    } else if (mMRecycledFieldNameStringId == null && "mRecycled".equals(text)) {
        mMRecycledFieldNameStringId = id;
    } else if (mStringClassNameStringId == null && "java.lang.String".equals(text)) {
        mStringClassNameStringId = id;
    } else if (mValueFieldNameStringId == null && "value".equals(text)) {
		mValueFieldNameStringId = id;
	}
}

Class ID
以及他们拥有的字段

2.6 HprofKeptBufferCollectVisitor第二个HprofVisitor

第二个Visitor用于记录所有String对象的value ID

2.7 HprofBufferShrinkVisitor第三个HprofVisitor
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值