Resource Canary主要是用来检测Activity级别的内存泄漏、以及重复创建的冗余Bitmap。整体代码分为两部分:客户端检测内存泄漏、裁剪Hprof文件,服务端分析回传的Hprof文件。
客户端监控内存泄漏、裁剪Hprof文件
这部分代码位于matrix-resource-canary-android模块下。监控Activity泄漏的大致流程如下:
- 通过Application的ActivityLifecycleCallbacks回调,获取已经destory的Activity信息;
- 后台线程每一分钟检测一次是否存在内存泄漏;
- 若发现内存泄漏,dump内存信息,并裁剪Hprof文件上报;
原理
ResourceCanary 仍然是借鉴的 LeakCanary,但 ResourceCanary 做了一点点的改进,这个地方查看文档的 细节与改进 部分,这里我贴一下减少误报的改进部分:
- 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
- 直接通过WeakReference.get()来判断对象是否已被回收,避免因延迟导致误判
- 若发现某个Activity无法被回收,再重复判断3次,且要求从该Activity被记录起有2个以上的A- ctivity被创建才认为是泄漏,以防在判断时该Activity被局部变量持有导致误判
- 对已判断为泄漏的Activity,记录其类名,避免重复提示该Activity已泄漏
插件启动
们可以直接看 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();
}
}
获取可能存在泄漏的Activity信息
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
mCurrentCreatedActivityCount.incrementAndGet();
}
@Override
public void onActivityDestroyed(Activity activity) {
//记录已被destory的Activity
pushDestroyedActivityInfo(activity);
}
};
private void pushDestroyedActivityInfo(Activity activity) {
final String activityName = activity.getClass().getName();
//该Activity确认存在泄漏,且已经上报
if (isPublished(activityName)) {
MatrixLog.d(TAG, "activity leak with name %s had published, just ignore", activityName);
return;
}
final UUID uuid = UUID.randomUUID();
final StringBuilder keyBuilder = new StringBuilder();
//生成Activity实例的唯一标识
keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName)
.append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits()));
final String key = keyBuilder.toString();
//构造一个数据结构,表示一个已被destroy的Activity
final DestroyedActivityInfo destroyedActivityInfo
= new DestroyedActivityInfo(key, activity, activityName, mCurrentCreatedActivityCount.get());
//放入后续待检测的Activity list
mDestroyedActivityInfos.add(destroyedActivityInfo);
}
存储过后,静静等待轮训分析。
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() {
// Fake leaks will be generated when debugger is attached.
//Debug调试模式,检测可能失效,直接return
if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
return Status.RETRY;
}
//没有已被destory的Activity实例
if (mDestroyedActivityInfos.isEmpty()) {
return Status.RETRY;
}
//创建一个对象的弱引用
final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
//尝试触发GC
triggerGc();
//系统未执行GC,直接return
if (sentinelRef.get() != null) {
// System ignored our gc request, we will retry later.
MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
return Status.RETRY;
}
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
while (infoIt.hasNext()) {
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
//该实例对应的Activity已被标泄漏,跳过该实例
if (isPublished(destroyedActivityInfo.mActivityName)) {
MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
infoIt.remove();
continue;
}
//若不能通过弱引用获取到Activity实例,表示已被回收,跳过该实例
if (destroyedActivityInfo.mActivityRef.get() == null) {
// The activity was recycled by a gc triggered outside.
MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
infoIt.remove();
continue;
}
//该Activity实例 检测到泄漏的次数+1
++destroyedActivityInfo.mDetectedCount;
//当前显示的Activity实例与泄漏的Activity实例相差几个Activity跳转
long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount;
//若改Activity实例 检测到泄漏的次数未达到阈值,或者泄漏的Activity与当前显示的Activity很靠近,可认为是一种容错手段(实际应用中有这种场景),跳过该实例
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
|| (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) {
// 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.
MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
+ "exists in %s times detection with %s created activities during destroy, wait for next detection to confirm.",
destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount, createdActivityCountFromDestroy);
continue;
}
MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance.", destroyedActivityInfo.mKey);
//若允许dump内存信息
if (mHeapDumper != null) {
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);
infoIt.remove();
} else {
//内存dump失败
MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.",
destroyedActivityInfo.mKey);
infoIt.remove();
}
} else {
// Lightweight mode, just report leaked activity name.
//不允许dump内存的情况下,直接上报泄漏的Activity类名
MatrixLog.i(TAG, "lightweight mode, just report leaked activity name.");
markPublished(destroyedActivityInfo.mActivityName);
if (mResourcePlugin != null) {
final JSONObject resultJson = new JSONObject();
try {
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
} catch (JSONException e) {
MatrixLog.printErrStackTrace(TAG, e, "unexpected exception.");
}
mResourcePlugin.onDetectIssue(new Issue(resultJson));
}
}
}
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) {
//process流程最终调用CanaryWorkerService进行裁剪和上报
CanaryWorkerService.shrinkHprofAndReport(context, result);
}
};
}
public static void shrinkHprofAndReport(Context context, 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);
}
<application>
<service
android:name=".CanaryWorkerService"
android:process=":res_can_worker"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false">
</service>
<service
android:name=".CanaryResultService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false">
</service>
</application>
CanaryWorkerService、CanaryResultService都是在独立进程运行的。其中CanaryWorkerService主要执行doShrinkHprofAndReport方法:
private void doShrinkHprofAndReport(HeapDump heapDump) {
final File hprofDir = heapDump.getHprofFile().getParentFile();
//裁剪之后的Hprof文件名
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;
try {
long startTime = System.currentTimeMillis();
//执行Hprof裁剪
new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);
MatrixLog.i(TAG, "shrink hprof file %s, size: %dk to %s, size: %dk, use time:%d",
hprofFile.getPath(), hprofFile.length() / 1024, shrinkedHProfFile.getPath(), shrinkedHProfFile.length() / 1024, (System.currentTimeMillis() - startTime));
//打成压缩包
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile)));
//记录一些设备信息
final ZipEntry resultInfoEntry = new ZipEntry("result.info");
//裁剪后的Hprof文件
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);
//裁剪后Hprof文件名
pw.println("hprofEntry=" + shrinkedHProfEntry.getName());
//泄漏Activity实例的key
pw.println("leakedActivityKey=" + heapDump.getReferenceKey());
pw.flush();
zos.closeEntry();
zos.putNextEntry(shrinkedHProfEntry);
copyFileToStream(shrinkedHProfFile, zos);
zos.closeEntry();
//原始数据删除
shrinkedHProfFile.delete();
hprofFile.delete();
MatrixLog.i(TAG, "process hprof file use total time:%d", (System.currentTimeMillis() - startTime));
//CanaryResultService执行上报逻辑
CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
} catch (IOException e) {
MatrixLog.printErrStackTrace(TAG, e, "");
} finally {
closeQuietly(zos);
}
}
裁剪的核心代码如下:
public void shrink(File hprofIn, File hprofOut) throws IOException {
FileInputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(hprofIn);
os = new BufferedOutputStream(new FileOutputStream(hprofOut));
final HprofReader reader = new HprofReader(new BufferedInputStream(is));
//1、收集Bitmap和String信息
reader.accept(new HprofInfoCollectVisitor());
// Reset.
is.getChannel().position(0);
//2、找到Bitmap、String中持有的byte数组,并找到内容重复的Bitmap
reader.accept(new HprofKeptBufferCollectVisitor());
// Reset.
is.getChannel().position(0);
//3、裁剪掉内容重复的Bitmap,和其他byte数组
reader.accept(new HprofBufferShrinkVisitor(new HprofWriter(os)));
} finally {
if (os != null) {
try {
os.close();
} catch (Throwable thr) {
// Ignored.
}
}
if (is != null) {
try {
is.close();
} catch (Throwable thr) {
// Ignored.
}
}
}
}
HprofInfoCollectVisitor
private class HprofInfoCollectVisitor extends HprofVisitor {
HprofInfoCollectVisitor() {
super(null);
}
@Override
public void visitHeader(String text, int idSize, long timestamp) {
mIdSize = idSize;
mNullBufferId = ID.createNullID(idSize);
}
@Override
public void visitStringRecord(ID id, String text, int timestamp, long length) {
if (mBitmapClassNameStringId == null && "android.graphics.Bitmap".equals(text)) {
//Bitmap类型String字符串的索引
mBitmapClassNameStringId = id;
} else if (mMBufferFieldNameStringId == null && "mBuffer".equals(text)) {
//mBuffer字段String字符串的索引
mMBufferFieldNameStringId = id;
} else if (mMRecycledFieldNameStringId == null && "mRecycled".equals(text)) {
//mRecycled字段String字符串的索引
mMRecycledFieldNameStringId = id;
} else if (mStringClassNameStringId == null && "java.lang.String".equals(text)) {
//String类型 字符串的索引
mStringClassNameStringId = id;
} else if (mValueFieldNameStringId == null && "value".equals(text)) {
//value字段字符串的索引
mValueFieldNameStringId = id;
}
}
@Override
public void visitLoadClassRecord(int serialNumber, ID classObjectId, int stackTraceSerial, ID classNameStringId, int timestamp, long length) {
if (mBmpClassId == null && mBitmapClassNameStringId != null && mBitmapClassNameStringId.equals(classNameStringId)) {
//找到Bitmap这个类的索引
mBmpClassId = classObjectId;
} else if (mStringClassId == null && mStringClassNameStringId != null && mStringClassNameStringId.equals(classNameStringId)) {
//找到String这个类的索引
mStringClassId = classObjectId;
}
}
@Override
public HprofHeapDumpVisitor visitHeapDumpRecord(int tag, int timestamp, long length) {
return new HprofHeapDumpVisitor(null) {
@Override
public void visitHeapDumpClass(ID id, int stackSerialNumber, ID superClassId, ID classLoaderId, int instanceSize, Field[] staticFields, Field[] instanceFields) {
if (mBmpClassInstanceFields == null && mBmpClassId != null && mBmpClassId.equals(id)) {
/找到Bitmap所有实例的字段信息
mBmpClassInstanceFields = instanceFields;
} else if (mStringClassInstanceFields == null && mStringClassId != null && mStringClassId.equals(id)) {
//找到String所有势力的字段信息
mStringClassInstanceFields = instanceFields;
}
}
};
}
}
这里对Bitmap、String两种类型做了处理(因为后续步骤中要采集掉byte数组)。
Bitmap在android sdk < 26之前,存储像素的byte数组是放在Java层的,26之后是放在native层的。
String在android sdk < 23之前,存储字符的byte数组是放在Java层的,23之后是放在native层的。
HprofKeptBufferCollectVisitor
private class HprofKeptBufferCollectVisitor extends HprofVisitor {
HprofKeptBufferCollectVisitor() {
super(null);
}
@Override
public HprofHeapDumpVisitor visitHeapDumpRecord(int tag, int timestamp, long length) {
return new HprofHeapDumpVisitor(null) {
@Override
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
try {
//找到Bitmap实例
if (mBmpClassId != null && mBmpClassId.equals(typeId)) {
ID bufferId = null;
Boolean isRecycled = null;
final ByteArrayInputStream bais = new ByteArrayInputStream(instanceData);
for (Field field : mBmpClassInstanceFields) {
final ID fieldNameStringId = field.nameId;
final Type fieldType = Type.getType(field.typeId);
if (fieldType == null) {
throw new IllegalStateException("visit bmp instance failed, lost type def of typeId: " + field.typeId);
}
if (mMBufferFieldNameStringId.equals(fieldNameStringId)) {
//找到这个实例mBuffer字段的索引id
bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
} else if (mMRecycledFieldNameStringId.equals(fieldNameStringId)) {
//找到这个实例mRecycled的boolean值(基础数据类型,没有引用关系)
isRecycled = (Boolean) IOUtil.readValue(bais, fieldType, mIdSize);
} else if (bufferId == null || isRecycled == null) {
IOUtil.skipValue(bais, fieldType, mIdSize);
} else {
break;
}
}
bais.close();
//确认Bitmap没有被回收
final boolean reguardAsNotRecycledBmp = (isRecycled == null || !isRecycled);
if (bufferId != null && reguardAsNotRecycledBmp && !bufferId.equals(mNullBufferId)) {
//将mBuffer对应的byte数组索引id加入集合
mBmpBufferIds.add(bufferId);
}
//如果是String类型
} else if (mStringClassId != null && mStringClassId.equals(typeId)) {
ID strValueId = null;
final ByteArrayInputStream bais = new ByteArrayInputStream(instanceData);
for (Field field : mStringClassInstanceFields) {
final ID fieldNameStringId = field.nameId;
final Type fieldType = Type.getType(field.typeId);
if (fieldType == null) {
throw new IllegalStateException("visit string instance failed, lost type def of typeId: " + field.typeId);
}
if (mValueFieldNameStringId.equals(fieldNameStringId)) {
//找到这个String实例的value字段对应的byte数组的索引id
strValueId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
} else if (strValueId == null) {
IOUtil.skipValue(bais, fieldType, mIdSize);
} else {
break;
}
}
bais.close();
if (strValueId != null && !strValueId.equals(mNullBufferId)) {
//将value字段对应的byte数组索引id加入集合
mStringValueIds.add(strValueId);
}
}
} catch (Throwable thr) {
throw new RuntimeException(thr);
}
}
@Override
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
//将所有byte数组的索引id,以及对应byte[]数据加入集合
mBufferIdToElementDataMap.put(id, elements);
}
};
}
@Override
public void visitEnd() {
final Set<Map.Entry<ID, byte[]>> idDataSet = mBufferIdToElementDataMap.entrySet();
final Map<String, ID> duplicateBufferFilterMap = new HashMap<>();
for (Map.Entry<ID, byte[]> idDataPair : idDataSet) {
final ID bufferId = idDataPair.getKey();
final byte[] elementData = idDataPair.getValue();
//如果这块byte数组不属于Bitmap,continue
if (!mBmpBufferIds.contains(bufferId)) {
// Discard non-bitmap buffer.
continue;
}
计算byte[]数据的md5
final String buffMd5 = DigestUtil.getMD5String(elementData);
final ID mergedBufferId = duplicateBufferFilterMap.get(buffMd5);
//若内存中Bitmap不存在重复的byte[]数据
if (mergedBufferId == null) {
duplicateBufferFilterMap.put(buffMd5, bufferId);
} else {
//若Bitmap存在重复的byte[]数据,所有引用都指向同一块byte数组的索引(方便后续裁剪掉重复的byte[]数据)
mBmpBufferIdToDeduplicatedIdMap.put(mergedBufferId, mergedBufferId);
mBmpBufferIdToDeduplicatedIdMap.put(bufferId, mergedBufferId);
}
}
// Save memory cost.
mBufferIdToElementDataMap.clear();
}
}
HprofBufferShrinkVisitor
private class HprofBufferShrinkVisitor extends HprofVisitor {
HprofBufferShrinkVisitor(HprofWriter hprofWriter) {
super(hprofWriter);
}
@Override
public HprofHeapDumpVisitor visitHeapDumpRecord(int tag, int timestamp, long length) {
return new HprofHeapDumpVisitor(super.visitHeapDumpRecord(tag, timestamp, length)) {
@Override
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
try {
//如果是Bitmap类型
if (typeId.equals(mBmpClassId)) {
ID bufferId = null;
int bufferIdPos = 0;
final ByteArrayInputStream bais = new ByteArrayInputStream(instanceData);
for (Field field : mBmpClassInstanceFields) {
final ID fieldNameStringId = field.nameId;
final Type fieldType = Type.getType(field.typeId);
if (fieldType == null) {
throw new IllegalStateException("visit instance failed, lost type def of typeId: " + field.typeId);
}
if (mMBufferFieldNameStringId.equals(fieldNameStringId)) {
bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
break;
} else {
bufferIdPos += IOUtil.skipValue(bais, fieldType, mIdSize);
}
}
//如果该实例的mBuffer字段的索引不为null
if (bufferId != null) {
//获取去重后的byte数组索引(若有内容重复的byte[]数据,最后都会指向一个id索引)
final ID deduplicatedId = mBmpBufferIdToDeduplicatedIdMap.get(bufferId);
if (deduplicatedId != null && !bufferId.equals(deduplicatedId) && !bufferId.equals(mNullBufferId)) {
//更新byte数组的索引id
modifyIdInBuffer(instanceData, bufferIdPos, deduplicatedId);
}
}
}
} catch (Throwable thr) {
throw new RuntimeException(thr);
}
super.visitHeapDumpInstance(id, stackId, typeId, instanceData);
}
private void modifyIdInBuffer(byte[] buf, int off, ID newId) {
final ByteBuffer bBuf = ByteBuffer.wrap(buf);
bBuf.position(off);
bBuf.put(newId.getBytes());
}
@Override
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
//重复的byte数组索引 重定向之后的 索引id
final ID deduplicatedID = mBmpBufferIdToDeduplicatedIdMap.get(id);
// Discard non-bitmap or duplicated bitmap buffer but keep reference key.
if (deduplicatedID == null || !id.equals(deduplicatedID)) {
//不记录重复的byte[]数据,直接return
if (!mStringValueIds.contains(id)) {
return;
}
}
super.visitHeapDumpPrimitiveArray(tag, id, stackId, numElements, typeId, elements);
}
};
}
}
Hprof文件裁剪的过程主要是裁剪了重复Bitmap的byte[]数据,裁剪的力度不是很大。(是不是可以只保留引用链,丢弃所有的PrimitiveArray?这里保留Bitmap的原因是回传之后,可以还原出png图片信息;感觉Bitmap用处不是很多,还狠很多裁剪的空间)。
最后是裁剪后的Hprof文件的上报,在CanaryResultService这个Service中
@Override
protected void onHandleWork(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_REPORT_HPROF_RESULT.equals(action)) {
final String resultPath = intent.getStringExtra(EXTRA_PARAM_RESULT_PATH);
final String activityName = intent.getStringExtra(EXTRA_PARAM_ACTIVITY);
if (resultPath != null && !resultPath.isEmpty()
&& activityName != null && !activityName.isEmpty()) {
doReportHprofResult(resultPath, activityName);
} else {
MatrixLog.e(TAG, "resultPath or activityName is null or empty, skip reporting.");
}
}
}
}
private void doReportHprofResult(String resultPath, String activityName) {
try {
final JSONObject resultJson = new JSONObject();
// resultJson = DeviceUtil.getDeviceInfo(resultJson, getApplication());
resultJson.put(SharePluginInfo.ISSUE_RESULT_PATH, resultPath);
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityName);
Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class);
if (plugin != null) {
plugin.onDetectIssue(new Issue(resultJson));
}
} catch (Throwable thr) {
MatrixLog.printErrStackTrace(TAG, thr, "unexpected exception, skip reporting.");
}
}
服务端分析裁剪后的Hprof文件
Java内存回收的原理是判断该对象是否有到GCRoot的引用链。此处分析Hprof的原则,也是获取泄漏的Activity到GCRoot的引用链。
首先,明确一下哪些对象属于GCRoot;
在Resource Canary的代码中,通过以下这些GCRoot类型来查找引用链
private void enqueueGcRoots(Snapshot snapshot) {
for (RootObj rootObj : snapshot.getGCRoots()) {
switch (rootObj.getRootType()) {
//Java栈帧中的局部变量
case JAVA_LOCAL:
Instance thread = HahaSpy.allocatingThread(rootObj);
String threadName = threadName(thread);
Exclusion params = excludedRefs.threadNames.get(threadName);
if (params == null || !params.alwaysExclude) {
enqueue(params, null, rootObj, null, null);
}
break;
case INTERNED_STRING:
case DEBUGGER:
case INVALID_TYPE:
// An object that is unreachable from any other root, but not a root itself.
case UNREACHABLE:
case UNKNOWN:
// An object that is in a queue, waiting for a finalizer to run.
case FINALIZING:
break;
//系统确认的一些GCRoot
case SYSTEM_CLASS:
//JNI的局部变量
case VM_INTERNAL:
// A local variable in native code.
//JNI的全局变量
case NATIVE_LOCAL:
// A global variable in native code.
//active线程持有的
case NATIVE_STATIC:
// An object that was referenced from an active thread block.
//用于同步锁的监控对象
case THREAD_BLOCK:
// Everything that called the wait() or notify() methods, or that is synchronized.
case BUSY_MONITOR:
case NATIVE_MONITOR:
case REFERENCE_CLEANUP:
// Input or output parameters in native code.
case NATIVE_STACK:
//Java类的静态变量
case JAVA_STATIC:
enqueue(null, null, rootObj, null, null);
break;
default:
throw new UnsupportedOperationException("Unknown root type:" + rootObj.getRootType());
}
}
}
下面来看下分析的入口方法
private static void analyzeAndStoreResult(File hprofFile, int sdkVersion, String manufacturer,
String leakedActivityKey, JSONObject extraInfo) throws IOException {
final HeapSnapshot heapSnapshot = new HeapSnapshot(hprofFile);
//系统问题可能导致的一些泄漏,可以认为排除掉
final ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults(sdkVersion, manufacturer).build();
//获取到Activity泄漏的结果
final ActivityLeakResult activityLeakResult
= new ActivityLeakAnalyzer(leakedActivityKey, excludedRefs).analyze(heapSnapshot);
DuplicatedBitmapResult duplicatedBmpResult = DuplicatedBitmapResult.noDuplicatedBitmap(0);
//Android sdk 26以下获取重复Bitmap的结果
if (sdkVersion < 26) {
final ExcludedBmps excludedBmps = AndroidExcludedBmpRefs.createDefaults().build();
duplicatedBmpResult = new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot);
} else {
System.err.println("\n ! SDK version of target device is larger or equal to 26, "
+ "which is not supported by DuplicatedBitmapAnalyzer.");
}
...
}
ActivityLeakAnalyzer这个类就是分析从GCRoot到泄漏Activity实例的引用链。
private ActivityLeakResult checkForLeak(HeapSnapshot heapSnapshot, String refKey) {
long analysisStartNanoTime = System.nanoTime();
try {
final Snapshot snapshot = heapSnapshot.getSnapshot();
//找到泄漏的Activity实例
final Instance leakingRef = findLeakingReference(refKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
//若找不到,说明已被回收
if (leakingRef == null) {
return ActivityLeakResult.noLeak(AnalyzeUtil.since(analysisStartNanoTime));
}
//寻找GCRoot到泄漏Activity的引用链
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
} catch (Throwable e) {
e.printStackTrace();
return ActivityLeakResult.failure(e, AnalyzeUtil.since(analysisStartNanoTime));
}
}
寻找泄漏Activity实例,是通过检测Activity泄漏时使用到的DestroyedActivityInfo类来判断的。
public class DestroyedActivityInfo {
//通过判断内存dump文件Hprof中实例的key与传入的key是否一致,判断是泄漏的Activity实例
public final String mKey;
public final String mActivityName;
//通过弱引用获取到这个实例
public final WeakReference<Activity> mActivityRef;
public final long mLastCreatedActivityCount;
public int mDetectedCount = 0;
public DestroyedActivityInfo(String key, Activity activity, String activityName, long lastCreatedActivityCount) {
mKey = key;
mActivityName = activityName;
mActivityRef = new WeakReference<>(activity);
mLastCreatedActivityCount = lastCreatedActivityCount;
}
}
private Instance findLeakingReference(String key, Snapshot snapshot) {
// private static final String DESTROYED_ACTIVITY_INFO_CLASSNAME= "com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo";
final ClassObj infoClass = snapshot.findClass(DESTROYED_ACTIVITY_INFO_CLASSNAME);
if (infoClass == null) {
throw new IllegalStateException("Unabled to find destroy activity info class with name: "
+ DESTROYED_ACTIVITY_INFO_CLASSNAME);
}
List<String> keysFound = new ArrayList<>();
//遍历DestroyedActivityInfo的所有实例
for (Instance infoInstance : infoClass.getInstancesList()) {
final List<ClassInstance.FieldValue> values = classInstanceValues(infoInstance);
// private static final String ACTIVITY_REFERENCE_KEY_FIELDNAME = "mKey";
final String keyCandidate = asString(fieldValue(values, ACTIVITY_REFERENCE_KEY_FIELDNAME));
if (keyCandidate.equals(key)) {
// private static final String ACTIVITY_REFERENCE_FIELDNAME = "mActivityRef";
final Instance weakRefObj = fieldValue(values, ACTIVITY_REFERENCE_FIELDNAME);
if (weakRefObj == null) {
continue;
}
final List<ClassInstance.FieldValue> activityRefs = classInstanceValues(weakRefObj);
//获取弱引用中的真正对象实例
return fieldValue(activityRefs, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
获取到泄漏的Activity实例之后,就需要找到GCToot到该实例的引用链。
private ActivityLeakResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef) {
//路径搜索帮助类,可以设置一些不用考虑的规则(不用搜索相关分叉)
ShortestPathFinder pathFinder = new ShortestPathFinder(mExcludedRefs);
//找到最短引用链,并返回结果
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
// False alarm, no strong reference path to GC Roots.
//无引用链
if (result.referenceChainHead == null) {
return ActivityLeakResult.noLeak(AnalyzeUtil.since(analysisStartNanoTime));
}
final ReferenceChain referenceChain = result.buildReferenceChain();
final String className = leakingRef.getClassObj().getClassName();
//若是命中exclude规则,返回无引用链
if (result.excludingKnown || referenceChain.isEmpty()) {
return ActivityLeakResult.noLeak(AnalyzeUtil.since(analysisStartNanoTime));
} else {
//返回Activity泄漏结果
return ActivityLeakResult.leakDetected(false, className, referenceChain,
AnalyzeUtil.since(analysisStartNanoTime));
}
}
findPath是发现引用链的核心方法
public Result findPath(Snapshot snapshot, Instance targetReference) {
final List<Instance> targetRefList = new ArrayList<>();
targetRefList.add(targetReference);
final Map<Instance, Result> results = findPath(snapshot, targetRefList);
if (results == null || results.isEmpty()) {
return new Result(null, false);
} else {
return results.get(targetReference);
}
}
public Map<Instance, Result> findPath(Snapshot snapshot, Collection<Instance> targetReferences) {
final Map<Instance, Result> results = new HashMap<>();
if (targetReferences.isEmpty()) {
return results;
}
clearState();
//找到GCRoot对象,并放入队列中
enqueueGcRoots(snapshot);
//是否忽略String对象
canIgnoreStrings = true;
for (Instance targetReference : targetReferences) {
if (isString(targetReference)) {
canIgnoreStrings = false;
break;
}
}
final Set<Instance> targetRefSet = new HashSet<>(targetReferences);
while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
ReferenceNode node;
if (!toVisitQueue.isEmpty()) {
node = toVisitQueue.poll();
} else {
node = toVisitIfNoPathQueue.poll();
if (node.exclusion == null) {
throw new IllegalStateException("Expected node to have an exclusion " + node);
}
}
// Termination
//找到完整引用链 GCRoot -> targetRef
if (targetRefSet.contains(node.instance)) {
results.put(node.instance, new Result(node, node.exclusion != null));
targetRefSet.remove(node.instance);
if (targetRefSet.isEmpty()) {
break;
}
}
//当前节点是否已经查看过
if (checkSeen(node)) {
continue;
}
if (node.instance instanceof RootObj) {
//如果是GCRoot,按照GCRoot的规则查找子节点
visitRootObj(node);
} else if (node.instance instanceof ClassObj) {
//如果是Class,按照Class的规则查找子节点
visitClassObj(node);
} else if (node.instance instanceof ClassInstance) {
//如果是实例,按照实例的规则查找子节点
visitClassInstance(node);
} else if (node.instance instanceof ArrayInstance) {
//如果是数组,按照数组的规则查找子节点
visitArrayInstance(node);
} else {
throw new IllegalStateException("Unexpected type for " + node.instance);
}
}
return results;
}
private void visitRootObj(ReferenceNode node) {
RootObj rootObj = (RootObj) node.instance;
Instance child = rootObj.getReferredInstance();
//Java栈帧中的局部变量
if (rootObj.getRootType() == RootType.JAVA_LOCAL) {
Instance holder = HahaSpy.allocatingThread(rootObj);
// We switch the parent node with the thread instance that holds
// the local reference.
Exclusion exclusion = null;
if (node.exclusion != null) {
exclusion = node.exclusion;
}
//将父节点替换为Thread(GCRoot),
ReferenceNode parent = new ReferenceNode(null, holder, null, null, null);
enqueue(exclusion, parent, child, "<Java Local>", LOCAL);
} else {
enqueue(null, node, child, null, null);
}
}
private void visitClassObj(ReferenceNode node) {
ClassObj classObj = (ClassObj) node.instance;
Map<String, Exclusion> ignoredStaticFields =
excludedRefs.staticFieldNameByClassName.get(classObj.getClassName());
for (Map.Entry<Field, Object> entry : classObj.getStaticFieldValues().entrySet()) {
Field field = entry.getKey();
//不是引用类型,不会有下一层引用链;可以排查
if (field.getType() != Type.OBJECT) {
continue;
}
String fieldName = field.getName();
if ("$staticOverhead".equals(fieldName)) {
continue;
}
Instance child = (Instance) entry.getValue();
boolean visit = true;
if (ignoredStaticFields != null) {
Exclusion params = ignoredStaticFields.get(fieldName);
if (params != null) {
visit = false;
if (!params.alwaysExclude) {
enqueue(params, node, child, fieldName, STATIC_FIELD);
}
}
}
if (visit) {
enqueue(null, node, child, fieldName, STATIC_FIELD);
}
}
}
private void visitClassInstance(ReferenceNode node) {
ClassInstance classInstance = (ClassInstance) node.instance;
Map<String, Exclusion> ignoredFields = new LinkedHashMap<>();
ClassObj superClassObj = classInstance.getClassObj();
Exclusion classExclusion = null;
while (superClassObj != null) {
Exclusion params = excludedRefs.classNames.get(superClassObj.getClassName());
if (params != null && (classExclusion == null || !classExclusion.alwaysExclude)) {
// true overrides null or false.
classExclusion = params;
}
Map<String, Exclusion> classIgnoredFields =
excludedRefs.fieldNameByClassName.get(superClassObj.getClassName());
if (classIgnoredFields != null) {
ignoredFields.putAll(classIgnoredFields);
}
superClassObj = superClassObj.getSuperClassObj();
}
if (classExclusion != null && classExclusion.alwaysExclude) {
return;
}
for (ClassInstance.FieldValue fieldValue : classInstance.getValues()) {
Exclusion fieldExclusion = classExclusion;
Field field = fieldValue.getField();
if (field.getType() != Type.OBJECT) {
continue;
}
Instance child = (Instance) fieldValue.getValue();
String fieldName = field.getName();
Exclusion params = ignoredFields.get(fieldName);
// If we found a field exclusion and it's stronger than a class exclusion
if (params != null && (fieldExclusion == null || (params.alwaysExclude
&& !fieldExclusion.alwaysExclude))) {
fieldExclusion = params;
}
enqueue(fieldExclusion, node, child, fieldName, INSTANCE_FIELD);
}
}
private void visitArrayInstance(ReferenceNode node) {
ArrayInstance arrayInstance = (ArrayInstance) node.instance;
Type arrayType = arrayInstance.getArrayType();
//每个元素都是引用类型
if (arrayType == Type.OBJECT) {
Object[] values = arrayInstance.getValues();
for (int i = 0; i < values.length; i++) {
Instance child = (Instance) values[i];
enqueue(null, node, child, "[" + i + "]", ARRAY_ENTRY);
}
}
}
通过以上流程,一旦找到完整的引用链,就会跳出findPath方法的while循环,返回引用链。
Resource Canary还是有重复Bitmap检测的功能,位于DuplicatedBitmapAnalyzer中
public DuplicatedBitmapResult analyze(HeapSnapshot heapSnapshot) {
final long analysisStartNanoTime = System.nanoTime();
try {
final Snapshot snapshot = heapSnapshot.getSnapshot();
new ShortestDistanceVisitor().doVisit(snapshot.getGCRoots());
return findDuplicatedBitmap(analysisStartNanoTime, snapshot);
} catch (Throwable e) {
e.printStackTrace();
return DuplicatedBitmapResult.failure(e, AnalyzeUtil.since(analysisStartNanoTime));
}
}
最终返回的DuplicatedBitmapResult中有一个DuplicatedBitmapEntry的list,这就是最后分析的结果。
public static class DuplicatedBitmapEntry implements Serializable {
private final String mBufferHash;
private final int mWidth;
private final int mHeight;
private final byte[] mBuffer;
private final List<ReferenceChain> mReferenceChains;
public DuplicatedBitmapEntry(int width, int height, byte[] rawBuffer, Collection<ReferenceChain> referenceChains) {
mBufferHash = DigestUtil.getMD5String(rawBuffer);
mWidth = width;
mHeight = height;
mBuffer = rawBuffer;
mReferenceChains = Collections.unmodifiableList(new ArrayList<>(referenceChains));
}
}
Resource Canary的Hprof文件分析逻辑,加深了对Java内存模型的理解。内存分析代码底层引用了'com.squareup.haha:haha:2.0.3',想要深入原理需要再仔细阅读Haha这个库。
Hprof 文件格式
Hprof 文件使用的基本数据类型为:u1、u2、u4、u8,分别表示 1 byte、2 byte、4 byte、8 byte 的内容,由文件头和文件内容两部分组成。
其中,文件头包含以下信息:
| 长度 | 含义 |
|---|---|
| [u1]* | 以 null 结尾的一串字节,用于表示格式名称及版本,比如 JAVA PROFILE 1.0.1(由 18 个 u1 字节组成) |
| u4 | size of identifiers,即字符串、对象、堆栈等信息的 id 的长度(很多 record 的具体信息需要通过 id 来查找) |
| u8 | 时间戳,时间戳,1970/1/1 以来的毫秒数 |
文件内容由一系列 records 组成,每一个 record 包含如下信息:
| 长度 | 含义 |
|---|---|
| u1 | TAG,表示 record 类型 |
| u4 | TIME,时间戳,相对文件头中的时间戳的毫秒 |
| u4 | LENGTH,即 BODY 的字节长度 |
| u4 | BODY,具体内容 |
查看 hprof.cc 可知,Hprof 文件定义的 TAG 有:
enum HprofTag {
HPROF_TAG_STRING = 0x01, // 字符串
HPROF_TAG_LOAD_CLASS = 0x02, // 类
HPROF_TAG_UNLOAD_CLASS = 0x03,
HPROF_TAG_STACK_FRAME = 0x04, // 栈帧
HPROF_TAG_STACK_TRACE = 0x05, // 堆栈
HPROF_TAG_ALLOC_SITES = 0x06,
HPROF_TAG_HEAP_SUMMARY = 0x07,
HPROF_TAG_START_THREAD = 0x0A,
HPROF_TAG_END_THREAD = 0x0B,
HPROF_TAG_HEAP_DUMP = 0x0C, // 堆
HPROF_TAG_HEAP_DUMP_SEGMENT = 0x1C,
HPROF_TAG_HEAP_DUMP_END = 0x2C,
HPROF_TAG_CPU_SAMPLES = 0x0D,
HPROF_TAG_CONTROL_SETTINGS = 0x0E,
};
需要重点关注的主要是三类信息:
- 字符串信息:保存着所有的字符串,在解析时可通过索引 id 引用
- 类的结构信息:包括类内部的变量布局,父类的信息等等
- 堆信息:内存占用与对象引用的详细信息
如果是堆信息,即 TAG 为 HEAP_DUMP 或 HEAP_DUMP_SEGMENT 时,那么其 BODY 由一系列子 record 组成,这些子 record 同样使用 TAG 来区分:
enum HprofHeapTag {
// Traditional.
HPROF_ROOT_UNKNOWN = 0xFF,
HPROF_ROOT_JNI_GLOBAL = 0x01, // native 变量
HPROF_ROOT_JNI_LOCAL = 0x02,
HPROF_ROOT_JAVA_FRAME = 0x03,
HPROF_ROOT_NATIVE_STACK = 0x04,
HPROF_ROOT_STICKY_CLASS = 0x05,
HPROF_ROOT_THREAD_BLOCK = 0x06,
HPROF_ROOT_MONITOR_USED = 0x07,
HPROF_ROOT_THREAD_OBJECT = 0x08,
HPROF_CLASS_DUMP = 0x20, // 类
HPROF_INSTANCE_DUMP = 0x21, // 实例对象
HPROF_OBJECT_ARRAY_DUMP = 0x22, // 对象数组
HPROF_PRIMITIVE_ARRAY_DUMP = 0x23, // 基础类型数组
// Android.
HPROF_HEAP_DUMP_INFO = 0xfe,
HPROF_ROOT_INTERNED_STRING = 0x89,
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
HPROF_ROOT_DEBUGGER = 0x8b,
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
HPROF_ROOT_VM_INTERNAL = 0x8d,
HPROF_ROOT_JNI_MONITOR = 0x8e,
HPROF_UNREACHABLE = 0x90, // Obsolete.
HPROF_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3, // Obsolete.
};
每一个 TAG 及其对应的内容可参考 HPROF Agent,比如,String record 的格式如下:
因此,在读取 Hprof 文件时,如果 TAG 为 0x01,那么,当前 record 就是字符串,第一部分信息是字符串 ID,第二部分就是字符串的内容。
Hprof 文件裁剪
Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 之外的所有对象的基础类型数组的值移除,因为 Hprof 文件的分析功能只需要用到字符串数组和 Bitmap 的 buffer 数组。另一方面,如果存在不同的 Bitmap 对象其 buffer 数组值相同的情况,则可以将它们指向同一个 buffer,以进一步减小文件尺寸。裁剪后的 Hprof 文件通常比源文件小 1/10 以上。
代码结构和 ASM 很像,主要由 HprofReader、HprofVisitor、HprofWriter 组成,分别对应 ASM 中的 ClassReader、ClassVisitor、ClassWriter。
HprofReader 用于读取 Hprof 文件中的数据,每读取到一种类型(使用 TAG 区分)的数据,就交给一系列 HprofVisitor 处理,最后由 HprofWriter 输出裁剪后的文件(HprofWriter 继承自 HprofVisitor)。
裁剪流程如下:
// 裁剪
public void shrink(File hprofIn, File hprofOut) throws IOException {
// 读取文件
final HprofReader reader = new HprofReader(new BufferedInputStream(is));
// 第一遍读取
reader.accept(new HprofInfoCollectVisitor());
// 第二遍读取
is.getChannel().position(0);
reader.accept(new HprofKeptBufferCollectVisitor());
// 第三遍读取,输出裁剪后的 Hprof 文件
is.getChannel().position(0);
reader.accept(new HprofBufferShrinkVisitor(new HprofWriter(os)));
}
可以看到,Matrix 为了完成裁剪功能,需要对输入的 hprof 文件重复读取三次,每次都由一个对应的 Visitor 处理。
读取 Hprof 文件
HprofReader 的源码很简单,先读取文件头,再读取 record,根据 TAG 区分 record 的类型,接着按照 HPROF Agent 给出的格式依次读取各种信息即可,读取完成后交给 HprofVisitor 处理。
读取文件头:
// 读取文件头
private void acceptHeader(HprofVisitor hv) throws IOException {
final String text = IOUtil.readNullTerminatedString(mStreamIn); // 连续读取数据,直到读取到 null
mIdSize = IOUtil.readBEInt(mStreamIn); // int 是 4 字节
final long timestamp = IOUtil.readBELong(mStreamIn); // long 是 8 字节
hv.visitHeader(text, idSize, timestamp); // 通知 Visitor
}
读取 record(以字符串为例):
// 读取文件内容
private void acceptRecord(HprofVisitor hv) throws IOException {
while (true) {
final int tag = mStreamIn.read(); // TAG 区分类型
final int timestamp = IOUtil.readBEInt(mStreamIn); // 时间戳
final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL; // Body 字节长
switch (tag) {
case HprofConstants.RECORD_TAG_STRING: // 字符串
acceptStringRecord(timestamp, length, hv);
break;
... // 其它类型
}
}
}
// 读取 String record
private void acceptStringRecord(int timestamp, long length, HprofVisitor hv) throws IOException {
final ID id = IOUtil.readID(mStreamIn, mIdSize); // IdSize 在读取文件头时确定
final String text = IOUtil.readString(mStreamIn, length - mIdSize); // Body 字节长减去 IdSize 剩下的就是字符串内容
hv.visitStringRecord(id, text, timestamp, length);
}
记录 Bitmap 和 String 类信息
为了完成上述裁剪目标,首先需要找到 Bitmap 及 String 类,及其内部的 mBuffer、value 字段,这也是裁剪流程中的第一个 Visitor 的作用:记录 Bitmap 和 String 类信息。
包括字符串 ID:
// 找到 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:
// 找到 Bitmap 和 String 的 Class ID
public void visitLoadClassRecord(int serialNumber, ID classObjectId, int stackTraceSerial, ID classNameStringId, int timestamp, long length) {
if (mBmpClassId == null && mBitmapClassNameStringId != null && mBitmapClassNameStringId.equals(classNameStringId)) {
mBmpClassId = classObjectId;
} else if (mStringClassId == null && mStringClassNameStringId != null && mStringClassNameStringId.equals(classNameStringId)) {
mStringClassId = classObjectId;
}
}
以及它们拥有的字段:
// 记录 Bitmap 和 String 类的字段信息
public void visitHeapDumpClass(ID id, int stackSerialNumber, ID superClassId, ID classLoaderId, int instanceSize, Field[] staticFields, Field[] instanceFields) {
if (mBmpClassInstanceFields == null && mBmpClassId != null && mBmpClassId.equals(id)) {
mBmpClassInstanceFields = instanceFields;
} else if (mStringClassInstanceFields == null && mStringClassId != null && mStringClassId.equals(id)) {
mStringClassInstanceFields = instanceFields;
}
}
第二个 Visitor 用于记录所有 String 对象的 value ID:
// 如果是 String 对象,则添加其内部字段 "value" 的 ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (mStringClassId != null && mStringClassId.equals(typeId)) {
if (mValueFieldNameStringId.equals(fieldNameStringId)) {
strValueId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
}
mStringValueIds.add(strValueId);
}
}
以及 Bitmap 对象的 Buffer ID 与其对应的数组本身:
// 如果是 Bitmap 对象,则添加其内部字段 "mBuffer" 的 ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (mBmpClassId != null && mBmpClassId.equals(typeId)) {
if (mMBufferFieldNameStringId.equals(fieldNameStringId)) {
bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
}
mBmpBufferIds.add(bufferId);
}
}
// 保存 Bitmap 对象的 mBuffer ID 及数组的映射关系
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
mBufferIdToElementDataMap.put(id, elements);
}
接着分析所有 Bitmap 对象的 buffer 数组,如果其 MD5 相等,说明是同一张图片,就将这些重复的 buffer ID 映射起来,以便之后将它们指向同一个 buffer 数组,删除其它重复的数组:
final String buffMd5 = DigestUtil.getMD5String(elementData);
final ID mergedBufferId = duplicateBufferFilterMap.get(buffMd5); // 根据该 MD5 值对应的 buffer id
if (mergedBufferId == null) { // 如果 buffer id 为空,说明是一张新的图片
duplicateBufferFilterMap.put(buffMd5, bufferId);
} else { // 否则是相同的图片,将当前的 Bitmap buffer 指向之前保存的 buffer id,以便之后删除重复的图片数据
mBmpBufferIdToDeduplicatedIdMap.put(mergedBufferId, mergedBufferId);
mBmpBufferIdToDeduplicatedIdMap.put(bufferId, mergedBufferId);
}
裁剪 Hprof 文件数据
将上述数据收集完成之后,就可以输出裁剪后的文件了,裁剪后的 Hprof 文件的写入功能由 HprofWriter 完成,代码很简单,HprofReader 读取到数据之后就由 HprofWriter 原封不动地输出到新的文件即可,唯二需要注意的就是 Bitmap 和基础类型数组。
先看 Bitmap,在输出 Bitmap 对象时,需要将相同的 Bitmap 数组指向同一个 buffer ID,以便接下来剔除重复的 buffer 数据:
// 将相同的 Bitmap 数组指向同一个 buffer ID
public void visitHeapDumpInstance(ID id, int stackId, ID typeId, byte[] instanceData) {
if (typeId.equals(mBmpClassId)) {
ID bufferId = (ID) IOUtil.readValue(bais, fieldType, mIdSize);
// 找到共同的 buffer id
final ID deduplicatedId = mBmpBufferIdToDeduplicatedIdMap.get(bufferId);
if (deduplicatedId != null && !bufferId.equals(deduplicatedId) && !bufferId.equals(mNullBufferId)) {
modifyIdInBuffer(instanceData, bufferIdPos, deduplicatedId);
}
// 修改完毕后再写入到新文件中
super.visitHeapDumpInstance(id, stackId, typeId, instanceData);
}
// 修改成对应的 buffer id
private void modifyIdInBuffer(byte[] buf, int off, ID newId) {
final ByteBuffer bBuf = ByteBuffer.wrap(buf);
bBuf.position(off);
bBuf.put(newId.getBytes());
}
}
对于基础类型数组,如果不是 Bitmap 中的 mBuffer 字段或者 String 中的 value 字段,则不写入到新文件中:
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {
final ID deduplicatedID = mBmpBufferIdToDeduplicatedIdMap.get(id);
// 如果既不是 Bitmap 中的 mBuffer 字段, 也不是 String 中的 value 字段,则舍弃该数据
// 如果当前 id 不等于 deduplicatedID,说明这是另一张重复的图片,它的图像数据不需要重复输出
if (!id.equals(deduplicatedID) && !mStringValueIds.contains(id)) {
return; // 直接返回,不写入新文件中
}
super.visitHeapDumpPrimitiveArray(tag, id, stackId, numElements, typeId, elements);
}