概述
年前,微信開源了Matrix專案,提供了Android、ios的APM實現方案。對於Android端實現,主要包括APK Checker
、Resource Canary
、Trace Canary
、SQLite Lint
、IO Canary
五部分。本文主要介紹Resource Canary
的原始碼實現,其他部分的原始碼分析將在後續推出。
程式碼框架分析
Resource Canary主要是用來檢測Activit級別的記憶體洩漏、以及重複建立的冗餘Bitmap。整體程式碼分為兩部分:客戶端檢測記憶體洩漏、裁剪Hprof檔案,服務端分析回傳的Hprof檔案。
客戶端監控記憶體洩漏、裁剪Hprof檔案
這部分程式碼位於matrix-resource-canary-android
模組下。監控Activity洩漏的大致流程如下:
- 通過Application的ActivityLifecycleCallbacks回撥,獲取已經destory的Activity資訊;
- 後臺執行緒每一分鐘檢測一次是否存在記憶體洩漏;
- 若發現記憶體洩漏,dump記憶體資訊,並裁剪Hprof檔案上報;
獲取可能存在洩漏的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);
}
複製程式碼
檢測是否存在記憶體洩漏
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;
}
};
複製程式碼
裁剪Hprof檔案上報
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;
GCRoot型別 | 說明 |
---|---|
Stack Local | Java方法的local變數或引數 |
Held by JVM | 用於JVM特殊目的由GC保留的物件,但實際上這個與JVM的實現是有關的。可能已知的一些型別是:系統類載入器、一些JVM知道的重要的異常類、一些用於處理異常的預分配物件以及一些自定義的類載入器等 |
JNI Local | JNI方法的local變數或引數 |
JNI Global | 全域性JNI引用 |
Thread | 活著的執行緒 |
Monitor Used | 用於同步的監控物件 |
Java static field | Java類的靜態屬性 |
常見的就是這幾種,完整說明可以檢視dalvik中的定義
在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));
}
}
複製程式碼
findDuplicatedBitmap
這塊邏輯比較複雜,暫時沒看懂~~最終返回的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的線上分析流程就結束了。
總結
Resource Canary的Hprof檔案分析邏輯,加深了對Java記憶體模型的理解。記憶體分析程式碼底層引用了'com.squareup.haha:haha:2.0.3'
,想要深入原理需要再仔細閱讀Haha這個庫。後續有時間可以再深入研究。