MatrixResourcePlugin

Summary:

  1. 参考了leakcanary并进行优化,如哨兵,hprof裁剪等
  2. dump hprof文件仍然在主进程,无法在线上使用
  3. 先dump hprof文件,后使用HprofVisitor进行裁剪压缩,而不是在dump的同时通过native hook直接裁剪
  4. hprof文件的生成和分析相分离,分析过程在server端命令行执行分析任务

ResourcePlugin.init

private final ResourceConfig mConfig;
private ActivityRefWatcher mWatcher = null;
@Override
 public void init(Application app, PluginListener listener) {
   super.init(app, listener);
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
     MatrixLog.e(TAG, "API is low Build.VERSION_CODES.ICE_CREAM_SANDWICH(14), ResourcePlugin is not supported");
     unSupportPlugin();
     return;
   }
   mWatcher = new ActivityRefWatcher(app, this);
 }

ResourcePlugin.start

@Override
 public void start() {
   super.start();
   mWatcher.start();
 }
@Override
 public void stop() {
   super.stop();
   mWatcher.stop();
 }
@Override
 public void destroy() {
   super.destroy();
   mWatcher.destroy();
 }

ActivityRefWatcher

public ActivityRefWatcher(Application app,
              final ResourcePlugin resourcePlugin) {
   this(app, resourcePlugin, new ComponentFactory());
 }

private ActivityRefWatcher(Application app,
              ResourcePlugin resourcePlugin,
              ComponentFactory componentFactory) {
   super(app, FILE_CONFIG_EXPIRED_TIME, resourcePlugin.getTag(), resourcePlugin);
   this.mResourcePlugin = resourcePlugin;
   final ResourceConfig config = resourcePlugin.getConfig();
   final Context context = app;
   HandlerThread handlerThread = MatrixHandlerThread.getDefaultHandlerThread();
   mDumpHprofMode = config.getDumpHprofMode();
   mBgScanTimes = config.getBgScanIntervalMillis();
   mFgScanTimes = config.getScanIntervalMillis();
   mContentIntent = config.getNotificationContentIntent();
   mDetectExecutor = componentFactory.createDetectExecutor(config, handlerThread);
   mMaxRedetectTimes = config.getMaxRedetectTimes();
   mDumpStorageManager = componentFactory.createDumpStorageManager(context);
   mHeapDumper = componentFactory.createHeapDumper(context, mDumpStorageManager);
   mHeapDumpHandler = componentFactory.createHeapDumpHandler(context, config);
   mDestroyedActivityInfos = new ConcurrentLinkedQueue<>();
 }

start

@Override
 public void start() {
   stopDetect();//main
   final Application app = mResourcePlugin.getApplication();
   if (app != null) {
     app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);//main
     AppActiveMatrixDelegate.INSTANCE.addListener(this);//main-->onForeground
     scheduleDetectProcedure();//main
     MatrixLog.i(TAG, "watcher is started.");
   }
 }

@Override
 public void onForeground(boolean isForeground) {
   if (isForeground) {
     MatrixLog.i(TAG, "we are in foreground, modify scan time[%sms].", mFgScanTimes);
     mDetectExecutor.clearTasks();
     mDetectExecutor.setDelayMillis(mFgScanTimes);
     mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);//main
   } else {
     MatrixLog.i(TAG, "we are in background, modify scan time[%sms].", mBgScanTimes);
     mDetectExecutor.setDelayMillis(mBgScanTimes);
   }
 }

private void scheduleDetectProcedure() {
  mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
@Override
 public void stop() {
   stopDetect();//main
   MatrixLog.i(TAG, "watcher is stopped.");
 }
private void stopDetect() {
   final Application app = mResourcePlugin.getApplication();
   if (app != null) {
     app.unregisterActivityLifecycleCallbacks(mRemovedActivityMonitor);
     AppActiveMatrixDelegate.INSTANCE.removeListener(this);
     unscheduleDetectProcedure();
   }
 }

Application.ActivityLifecycleCallbacks.onActivityDestroyed

private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
   @Override
   public void onActivityDestroyed(Activity activity) {
     pushDestroyedActivityInfo(activity);
   }
}

ActivityRefWatcher.mScanDestroyedActivitiesTask

private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {

   @Override
   public Status execute() {
     // If destroyed activity list is empty, just wait to save power.
     if (mDestroyedActivityInfos.isEmpty()) {
       MatrixLog.i(TAG, "DestroyedActivityInfo isEmpty!");
       return Status.RETRY;
     }

final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
 triggerGc();
 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();

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;
 }

++destroyedActivityInfo.mDetectedCount;

if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
   || !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, wait for next detection to confirm.",
     destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
   continue;
 }

MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance. mode[%s]", destroyedActivityInfo.mKey, mDumpHprofMode);

if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) {
 ....

mResourcePlugin.onDetectIssue(new Issue(resultJson));

} else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) {
   final File hprofFile = mHeapDumper.dumpHeap();//main
   if (hprofFile != null) {
     markPublished(destroyedActivityInfo.mActivityName);
     final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
     mHeapDumpHandler.process(heapDump);//main
     infoIt.remove();
   } else {
     MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.",
         destroyedActivityInfo.mKey);
     infoIt.remove();
   }
 } else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) {

......

MatrixLog.i(TAG, "show notification for notify activity leak. %s", destroyedActivityInfo.mActivityName);
}

AndroidHeapDumper.dumpHeap//ported from LeakCanary

This class is ported from LeakCanary.

public File dumpHeap() {
   final File hprofFile = mDumpStorageManager.newHprofFile();
   try {
       Debug.dumpHprofData(hprofFile.getAbsolutePath());
       return hprofFile;
   } catch (Exception e) {
     MatrixLog.printErrStackTrace(TAG, e, "failed to dump heap into file: %s.", hprofFile.getAbsolutePath());
     return null;
  }
}

CanaryWorkerService.shrinkHprofAndReport

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);
 }
@Override
 protected void onHandleWork(Intent intent) {
   if (intent != null) {
     final String action = intent.getAction();
     if (ACTION_SHRINK_HPROF.equals(action)) {
       try {
         intent.setExtrasClassLoader(this.getClassLoader());
         final HeapDump heapDump = (HeapDump) intent.getSerializableExtra(EXTRA_PARAM_HEAPDUMP);
         if (heapDump != null) {
           doShrinkHprofAndReport(heapDump);//main
         } else {
           MatrixLog.e(TAG, "failed to deserialize heap dump, give up shrinking and reporting.");
         }
       } catch (Throwable thr) {
         MatrixLog.printErrStackTrace(TAG, thr, "failed to deserialize heap dump, give up shrinking and reporting.");
       }
     }
   }
 }
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;
   try {
     long startTime = System.currentTimeMillis();
     new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);//main
     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");
     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);
     pw.println("hprofEntry=" + shrinkedHProfEntry.getName());
     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));
 
     //回调通知shrinkedHProfile文件路径等信息 
     CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
}}

HprofBufferShrinker.shrink

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));
     reader.accept(new HprofInfoCollectVisitor());//main
     // Reset.
     is.getChannel().position(0);
     reader.accept(new HprofKeptBufferCollectVisitor());//main
     // Reset.
     is.getChannel().position(0);
     reader.accept(new HprofBufferShrinkVisitor(new HprofWriter(os)));//main
   } 

HprofReader.accept

public HprofReader(InputStream in) {
   mStreamIn = in;
 }

public void accept(HprofVisitor hv) throws IOException {
   acceptHeader(hv);
   acceptRecord(hv);
   hv.visitEnd();
 }

private void acceptHeader(HprofVisitor hv) throws IOException {
   final String text = IOUtil.readNullTerminatedString(mStreamIn);
   final int idSize = IOUtil.readBEInt(mStreamIn);
   if (idSize <= 0 || idSize >= (Integer.MAX_VALUE >> 1)) {
     throw new IOException("bad idSize: " + idSize);
   }
   final long timestamp = IOUtil.readBELong(mStreamIn);
   mIdSize = idSize;
   hv.visitHeader(text, idSize, timestamp);
 }

HprofInfoCollectVisitor.visitHeader

class HprofInfoCollectVisitor extends HprofVisitor {
@Override
 public void visitHeader(String text, int idSize, long timestamp) {
   mIdSize = idSize;
   mNullBufferId = ID.createNullID(idSize);
 }
}

分析Hprof

对hprof文件的生成和分析相分离,分析位于matrix-resource-canary/matrix-resource-canary-analyzer模块中的com.tencent.matrix.resource.analyzer.CLIMain.main方法

//CLIMain
public static void main(String[] args) {
   if (args.length == 0) {
     printUsage(System.out);
     System.exit(ERROR_NEED_ARGUMENTS);
   }
   try {
     final CommandLine cmdline = new DefaultParser().parse(sOptions, args);
     if (cmdline.hasOption(OPTION_HELP.mOption.getLongOpt())) {
       printUsage(System.out);
       System.exit(ERROR_SUCCESS);
     }

     parseArguments(cmdline);
     
     doAnalyze();//main
     }
}
private static void doAnalyze() throws IOException {
// Then do analyzing works and output into directory or zip according to the option. Besides,
 // store extra info into the result json by the way.
 analyzeAndStoreResult(tempHprofFile, sdkVersion, manufacturer, leakedActivityKey, extraInfo);
}

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();
   final ActivityLeakResult activityLeakResult
       = new ActivityLeakAnalyzer(leakedActivityKey, excludedRefs).analyze(heapSnapshot);//main

   DuplicatedBitmapResult duplicatedBmpResult = DuplicatedBitmapResult.noDuplicatedBitmap(0);
   if (sdkVersion < 26) {//main
     final ExcludedBmps excludedBmps = AndroidExcludedBmpRefs.createDefaults().build();
     duplicatedBmpResult = new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot);//main
   } else {
     System.err.println("\n ! SDK version of target device is larger or equal to 26, "
         \+ "which is not supported by DuplicatedBitmapAnalyzer.");
   }

其他

ComponentFactory

public static class ComponentFactory {

   protected RetryableTaskExecutor createDetectExecutor(ResourceConfig config, HandlerThread handlerThread) {
     return new RetryableTaskExecutor(config.getScanIntervalMillis(), handlerThread);
   }

   protected DumpStorageManager createDumpStorageManager(Context context) {
     return new DumpStorageManager(context);
   }

   protected AndroidHeapDumper createHeapDumper(Context context, DumpStorageManager dumpStorageManager) {
     return new AndroidHeapDumper(context, dumpStorageManager);
   }

   protected AndroidHeapDumper.HeapDumpHandler createHeapDumpHandler(final Context context, ResourceConfig resourceConfig) {
     return new AndroidHeapDumper.HeapDumpHandler() {
       @Override
       public void process(HeapDump result) {//main
         CanaryWorkerService.shrinkHprofAndReport(context, result);//main
       }
     };
   }
 }