文章目录
一、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很像, 主要由 HprofReader
、HprofVisitor
、HprofWriter
组成, 分别对应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字节组成) |
u4 | size of identifiers, 即字符串、对象、堆栈等信息的id的长度(很多record的具体信息需要通过id来查找) |
u8 | 时间戳, 1970/1/1以来的毫秒数 |
2.3.2 文件内容
文件内存由一系列records组成, 每一个record包含如下信息
长度 | 含义 |
---|---|
u1 | TAG, 表示record类型 |
u4 | TIME, 时间戳, 相对文件头中的时间戳的毫秒数 |
u4 | LENGTH, 即BODY的字节长度 |
u8 | BODY, 具体内容 |
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