preface

In the Android mainstream tripartite library source code analysis series of the first few articles, the author has been on the network, pictures, databases, responsive programming in the most popular third-party open source framework for a more in-depth explanation, if there are friends interested in these four pieces, you can go to understand. In this article, I’ll take a closer look at the source code flow for Leakcanary, the memory leak detection framework in Android.

I. Principle overview

First, I took a closer look at the Leakcanary official Github repository and, most importantly, explained how Leakcanary works, which I translated myself into easy-to-understand text in seven steps:

  • RefWatcher. Watch () creates a KeyedWeakReference for observing the object.
  • 2. Then, in the background thread, it checks to see if the reference has been cleared and if GC has not been triggered.
  • 3. If the reference is not cleared, it will store the stack information in the.hprof file on the file system.
  • 4. HeapAnalyzerService is started in a separate process, and HeapAnalyzer uses the HAHA open source library to parse the heap dump, a stack snapshot file, at a given time.
  • 5. From the heap dump, HeapAnalyzer finds the KeyedWeakReference based on a unique reference key and locates the leaked reference.
  • 6. In order to determine if there is a leak, HeapAnalyzer calculates the shortest strong reference path to GC Roots, and then establishes the chain reference that leads to the leak.
  • 7. The result is passed back to DisplayLeakService in the app process, and a leak notification is displayed.

The official principle is simply explained as follows: After an Activity executes onDestroy(), place it in a WeakReference, and then associate the WeakReference Activity object with the ReferenceQueque. In this case, the ReferenceQueque is used to check whether the object exists. If not, gc is performed and the object is checked again. If not, a memory leak is determined. Finally, the open source HAHA library is used to analyze heap memory after dump.

2. Simple examples

This is the example code for the Leakcanary official repository:

Build. Gradle in your project app:

Dependencies {debugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 1.6.2' releaseImplementation 'com. Squareup. Leakcanary: leakcanary - android - no - op: 1.6.2' / / optional, If you use the support library fragments could debugImplementation 'com. Squareup. Leakcanary: leakcanary - support - fragments: 1.6.2'}Copy the code

Then configure it in your Application:

public class WanAndroidApp extends Application { private RefWatcher refWatcher; public static RefWatcher getRefWatcher(Context context) { WanAndroidApp application = (WanAndroidApp) context.getApplicationContext(); return application.refWatcher; } @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // 1 return; } // 2 refWatcher = LeakCanary.install(this); }}Copy the code

In comment 1, it first determines whether the current process is the same one that has been created specifically to analyze heap memory, that is, the HeapAnalyzerService, and if so, does not perform initialization in Application. If it is the main process where the current application resides, the LeakCanary. Install (this) in Comment 2 will be executed to LeakCanary installation. With just a few lines of code like this, we can detect memory leaks in our application. Of course, using this method only detects whether the Activity and standard Fragment are leaking memory. If you want to detect whether the Fragment of the V4 package is leaking memory after executing onDestroy(), Add two lines of code to the Fragment onDestroy() method to monitor the current Fragment:

RefWatcher refWatcher = WanAndroidApp.getRefWatcher(_mActivity);
refWatcher.watch(this);
Copy the code

The RefWatcher above is a reference observer object that monitors the reference status of the current instance object. From the above analysis, we can see that the core code is LeakCanary. Install (this). Next, we will unpack LeakCanary step by step from here.

Third, source code analysis

1, LeakCanary# install ()

public static @NonNull RefWatcher install(@NonNull Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}
Copy the code

The processing in the install() method can be broken down into the following four steps:

  • 1, refWatcher (application)
  • ListenerServiceClass (DisplayLeakService.class)
  • 3, chain call excludedRefs (AndroidExcludedRefs createAppDefaults (). The build ())
  • 4. Chain call buildAndInstall()

First, let’s look at the first step, which calls the refWatcher method of the LeakCanary class, as shown below:

public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
  return new AndroidRefWatcherBuilder(context);
}
Copy the code

And I’m going to create a new Object called AndroidRefWatcherBuilder, and look at this class called AndroidRefWatcherBuilder.

2, AndroidRefWatcherBuilder

/** A {@link RefWatcherBuilder} with appropriate Android defaults. */ public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> { ... AndroidRefWatcherBuilder(@NonNull Context context) { this.context = context.getApplicationContext(); }... }Copy the code

The constructor of AndroidRefWatcherBuilder simply saves the applicationContext object passed in from the outside. RefWatcherBuilder is a reference observer constructor object adapted to the Android platform. It inherits RefWatcherBuilder, which is a base class constructor responsible for creating reference observer RefWatcher instances. Keep looking at the RefWatcherBuilder class.

3, RefWatcherBuilder

public class RefWatcherBuilder<T extends RefWatcherBuilder<T>> { ... public RefWatcherBuilder() { heapDumpBuilder = new HeapDump.Builder(); }... }Copy the code

A HeapDump constructor object is created in the constructor of RefWatcher’s base class, RefWatcherBuilder. HeapDump is a data structure that stores heap dump information.

Next, examine the logic of the listenerServiceClass(DisplayLeakService.class) chained call in the Install () method.

4, AndroidRefWatcherBuilder# listenerServiceClass ()

public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
  @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}
Copy the code

Here, a DisplayLeakService Class object is passed in, which shows the result log of leak analysis, followed by a notification to jump to the DisplayLeakActivity display screen. Let’s take a look inside the ServiceHeapDumpListener object created in the listenerServiceClass() method.

5, ServiceHeapDumpListener

public final class ServiceHeapDumpListener implements HeapDump.Listener { ... public ServiceHeapDumpListener(@NonNull final Context context, @NonNull final Class<? extends AbstractAnalysisResultService> listenerServiceClass) { this.listenerServiceClass = checkNotNull(listenerServiceClass, "listenerServiceClass"); this.context = checkNotNull(context, "context").getApplicationContext(); }... }Copy the code

You can see that the DisplayLeakService Class object and application object are only saved in ServiceHeapDumpListener. Its purpose is to receive a heap dump for analysis.

And then we continue to see the install chain () method calls. ExcludedRefs (AndroidExcludedRefs. CreateAppDefaults (). The build ()) this part of the code. See first AndroidExcludedRefs. CreateAppDefaults ().

6, AndroidExcludedRefs# createAppDefaults ()

public enum AndroidExcludedRefs { ... public static @NonNull ExcludedRefs.Builder createAppDefaults() { return createBuilder(EnumSet.allOf(AndroidExcludedRefs.class)); } public static @NonNull ExcludedRefs.Builder createBuilder(EnumSet<AndroidExcludedRefs> refs) { ExcludedRefs.Builder excluded = ExcludedRefs.builder(); for (AndroidExcludedRefs ref : refs) { if (ref.applies) { ref.add(excluded); ((ExcludedRefs.BuilderWithParams) excluded).named(ref.name()); } } return excluded; }... }Copy the code

AndroidExcludedRefs is an enum class that declares cases of memory leaks in the Android SDK and vENDOR-specific SDKS. As you can see from the name of the AndroidExcludedRefs class, these cases will be filtered by the Leakcanary monitor. At present, 46 such cases are included in this version, and there may be more cases in the future. Then enumsets. AllOf (AndroidExcludedRefs. Class) this method will return a contain AndroidExcludedRefs enumsets element type. Enum is an abstract class. In this case, the concrete implementation class is a RegularEnumSet of the general normal type. If the number of elements in an Enum is greater than 64, the JumboEnumSet is used to store large amounts of data. Finally, createBuilder built a reference-excluded builder in the createBuilder method, which saved various cases and then returned them.

Finally, we see the last step in the chain call, buildAndInstall().

7, AndroidRefWatcherBuilder# buildAndInstall ()

private boolean watchActivities = true; private boolean watchFragments = true; public @NonNull RefWatcher buildAndInstall() { // 1 if (LeakCanaryInternals.installedRefWatcher ! = null) { throw new UnsupportedOperationException("buildAndInstall() should only be called once."); } // 2 RefWatcher refWatcher = build(); if (refWatcher ! = DISABLED) { // 3 LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true); if (watchActivities) { // 4 ActivityRefWatcher.install(context, refWatcher); } if (watchFragments) { // 5 FragmentRefWatcher.Helper.install(context, refWatcher); } } // 6 LeakCanaryInternals.installedRefWatcher = refWatcher; return refWatcher; }Copy the code

First of all, in the note 1, can judge LeakCanaryInternals installedRefWatcher whether has already been assigned, if the assignment, will throw an exception, warning buildAndInstall () should only call this method once, at the end of this method, In 6, namely the LeakCanaryInternals. InstalledRefWatcher will be assigned. Note 2 calls the build() method of AndroidRefWatcherBuilder, its base class RefWatcherBuilder, and we see how it is built.

8, RefWatcherBuilder# build ()

public final RefWatcher build() {
    if (isDisabled()) {
      return RefWatcher.DISABLED;
    }

    if (heapDumpBuilder.excludedRefs == null) {
      heapDumpBuilder.excludedRefs(defaultExcludedRefs());
    }

    HeapDump.Listener heapDumpListener = this.heapDumpListener;
    if (heapDumpListener == null) {
      heapDumpListener = defaultHeapDumpListener();
    }

    DebuggerControl debuggerControl = this.debuggerControl;
    if (debuggerControl == null) {
      debuggerControl = defaultDebuggerControl();
    }

    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
      heapDumper = defaultHeapDumper();
    }

    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
      watchExecutor = defaultWatchExecutor();
    }

    GcTrigger gcTrigger = this.gcTrigger;
    if (gcTrigger == null) {
      gcTrigger = defaultGcTrigger();
    }

    if (heapDumpBuilder.reachabilityInspectorClasses == null) {
      heapDumpBuilder.reachabilityInspectorClasses(defa  ultReachabilityInspectorClasses());
    }

    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
        heapDumpBuilder);
}
Copy the code

As you can see, the RefWatcherBuilder contains the following seven components:

  • 1. ExcludedRefs: Record leak paths that can be ignored.

  • HeapDumpListener: dumps heap information to the hprof file. After parsing the hprof file, the heapDumpListener performs a callback and notifies DisplayLeakService of a leak alert.

  • 3. DebuggerControl: Judge whether it is in debugging mode, in which memory leak detection is not carried out. Why is that? Error messages may be reported because the previous reference may be retained during debugging.

  • HeapDumper: dump heap information from memory leaks to hprof files.

  • WatchExecutor: Thread controller that executes memory leak detection after onDestroy() and when the main thread is idle.

  • 6. GcTrigger: Used for GC. WatchExecutor first detects a possible memory leak and then actively GC.

  • 7, reachabilityInspectorClasses: used to test the accessibility to the class list.

Finally, these components are built into a new RefWatcher using builder mode and returned.

We continue to look back to AndroidRefWatcherBuilder comments 3 LeakCanaryInternals. SetEnabledAsync (context, DisplayLeakActivity. Class, true) this line of code.

9, LeakCanaryInternals# setEnabledAsync ()

public static void setEnabledAsync(Context context, final Class<?> componentClass,
final boolean enabled) {
  final Context appContext = context.getApplicationContext();
  AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
    @Override public void run() {
      setEnabledBlocking(appContext, componentClass, enabled);
    }
  });
}
Copy the code

In this case, the built-in AsyncTask THREAD_POOL_EXECUTOR thread pool is used directly to block DisplayLeakActivity.

Then we continue to look at the code in comment 4 of AndroidRefWatcherBuilder.

10, ActivityRefWatcher# install ()

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    // 1
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    
    // 2
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
Copy the code

As you can see, in note 1 to create a own activityRefWatcher instance, and calls the application in 2 place registerActivityLifecycleCallbacks () method, This allows you to listen for activity lifecycle events. Continue to look at activityRefWatcher. LifecycleCallbacks operating inside.

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { @Override public void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); }}; public abstract class ActivityLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { }Copy the code

Obviously, implemented here and write the Application ActivityLifecycleCallbacks onActivityDestroyed () method, Refwatcher.watch (Activity) is called to detect memory leaks after all activities have executed onDestroyed().

We’ll see note 5 FragmentRefWatcher. Helper. Install (context, refWatcher) this line of code,

11, FragmentRefWatcher. Helper# install ()

public interface FragmentRefWatcher { void watchFragments(Activity activity); final class Helper { private static final String SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME = "com.squareup.leakcanary.internal.SupportFragmentRefWatcher"; public static void install(Context context, RefWatcher refWatcher) { List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>(); // 1 if (SDK_INT >= O) { fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher)); } // 2 try { Class<? > fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME); Constructor<? > constructor = fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class); FragmentRefWatcher supportFragmentRefWatcher = (FragmentRefWatcher) constructor.newInstance(refWatcher); fragmentRefWatchers.add(supportFragmentRefWatcher); } catch (Exception ignored) { } if (fragmentRefWatchers.size() == 0) { return; } Helper helper = new Helper(fragmentRefWatchers); // 3 Application application = (Application) context.getApplicationContext(); application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks); }... }Copy the code

Here the logic is simple, first of all, in the note 1 will Android standard fragments RefWatcher classes, namely AndroidOfFragmentRefWatcher added to the newly created fragmentRefWatchers. Using reflection will leakcanary at 2 – support – fragments SupportFragmentRefWatcher added under the package, if you are in the build of the app. Gradle does not add the following line under reference, will take less than this, LeakCanary detects only the Activity and standard Fragment cases.

DebugImplementation 'com. Squareup. Leakcanary: leakcanary - support - fragments: 1.6.2'Copy the code

Continue to see note 3 helper. ActivityLifecycleCallbacks inside the code.

private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { for (FragmentRefWatcher watcher : fragmentRefWatchers) { watcher.watchFragments(activity); }}};Copy the code

As you can see, after the Activity executes the onActivityCreated() method, the watchFragments() method that specifies watcher is called. Note that there may be two types of Watcher, but in either case, Will use the current incoming activity to obtain the corresponding FragmentManager/SupportFragmentManager object, call its registerFragmentLifecycleCallbacks () method, After the corresponding onDestroyView() and onDestoryed() methods are executed, refwatcher.watch (view) and refwatcher.watch (fragment) are used to detect memory leaks, as shown below.

@Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) { View view = fragment.getView(); if (view ! = null) { refWatcher.watch(view); } } @Override public void onFragmentDestroyed(FragmentManagerfm, Fragment fragment) { refWatcher.watch(fragment); }Copy the code

Notice that now we get to the really important part, which is the refwatcher.watch () line.

12, RefWatcher# watch ()

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    // 1
    String key = UUID.randomUUID().toString();
    // 2
    retainedKeys.add(key);
    // 3
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    // 4
    ensureGoneAsync(watchStartNanoTime, reference);
}
Copy the code

Note that the use of random UUID in comment 1 guarantees the uniqueness of the corresponding key for each detection object. At comment 2, add the generated key to the Set Set of type CopyOnWriteArraySet. Create a custom KeyedWeakReference at note 3 to see its internal implementation.

13, KeyedWeakReference

final class KeyedWeakReference extends WeakReference<Object> { public final String key; public final String name; KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) { // 1 super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue")); this.key = checkNotNull(key, "key"); this.name = checkNotNull(name, "name"); }}Copy the code

It can be seen that in KeyedWeakReference, key and name are used to identify a detected WeakReference object. In comment 1, you associate the weak reference with the ReferenceQueue of the reference queue. If the object held by the weak reference is reclaimed by the GC, the JVM adds the weak reference to the ReferenceQueue of the reference queue with which it is associated. That is, if the Activity object held by KeyedWeakReference is recovered by GC, the object will be added to the referenceQueue referenceQueue.

Then we go back to the ensureGoneAsync() method at comment 4 in refwatcher.watch ().

14, RefWatcher# ensureGoneAsync ()

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) { // 1 watchExecutor.execute(new Retryable() { @Override public Retryable.Result run() { // 2 return ensureGone(reference watchStartNanoTime); }}); }Copy the code

In the ensureGoneAsync() method, the ensureGone method at comment 2 is executed using watchExecutor at comment 1, which is an instance of AndroidWatchExecutor.

Let’s look at the logic inside watchExecutor.

15, AndroidWatchExecutor

public final class AndroidWatchExecutor implements WatchExecutor { ... public AndroidWatchExecutor(long initialDelayMillis) { mainHandler = new Handler(Looper.getMainLooper()); HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME); handlerThread.start(); // 1 backgroundHandler = new Handler(handlerThread.getLooper()); this.initialDelayMillis = initialDelayMillis; maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis; } @Override public void execute(@NonNull Retryable retryable) { // 2 if (Looper.getMainLooper().getThread() == Thread.currentThread()) { waitForIdle(retryable, 0); } else { postWaitForIdle(retryable, 0); }}... }Copy the code

In the constructor for AndroidWatchExecutor in comment 1, notice that looper of the HandlerThread creates a new backgroundHandler, which will be used later. In comment 2, it determines whether the current thread is the main thread, if so, it calls waitForIdle() directly, if not, it calls postWaitForIdle(), and looks at the method.

private void postWaitForIdle(final Retryable retryable, final int failedAttempts) { mainHandler.post(new Runnable() { @Override public void run() { waitForIdle(retryable, failedAttempts); }}); }Copy the code

It is clear that the mainHandler constructed in the constructor with the main thread looper is used to post, so waitForIdle() will eventually execute on the main thread as well. Then look at the implementation of waitForIdle().

private void waitForIdle(final Retryable retryable, final int failedAttempts) { Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { postToBackgroundWithDelay(retryable, failedAttempts); return false; }}); }Copy the code

The messagequeue.idleHandler () callback calls queueIdle when looper is idle. This mechanism allows lazy initialization of third-party libraries. Then perform internal postToBackgroundWithDelay () method. Let’s look at the implementation.

private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
  long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts),     maxBackoffFactor);
  // 1
  long delayMillis = initialDelayMillis * exponentialBackoffFactor;
  // 2
  backgroundHandler.postDelayed(new Runnable() {
    @Override public void run() {
      // 3
      Retryable.Result result = retryable.run();
      // 4
      if (result == RETRY) {
        postWaitForIdle(retryable, failedAttempts +   1);
      }
    }
  }, delayMillis);
}
Copy the code

First see note 4 place, can understand, postToBackgroundWithDelay () is a recursive method, if the result has been equal to RETRY, will have been performing postWaitForIdle () method. Back in comment 1, the default value of initialDelayMillis is 5s, so delayMillis is 5s. In comment 2, the backgroundHandler created by Looper of HandlerThread in the constructor is used to asynchronously delay the run() method that retryable uses. The run() method executes the ensureGone() line at comment 2 of RefWatcher’s ensureGoneAsync() method.

16, RefWatcher# ensureGone ()

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) { long gcStartNanoTime = System.nanoTime(); long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); // 1 removeWeaklyReachableReferences(); // 2 if (debuggerControl.isDebuggerAttached()) { // The debugger can create false leaks. return RETRY; } // 3 if (gone(reference)) { return DONE; } // 4 gcTrigger.runGc(); removeWeaklyReachableReferences(); // 5 if (! gone(reference)) { long startDumpHeap = System.nanoTime(); long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime); File heapDumpFile = heapDumper.dumpHeap(); if (heapDumpFile == RETRY_LATER) { // Could not dump the heap. return RETRY; } long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap); HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key) .referenceName(reference.name) .watchDurationMs(watchDurationMs) .gcDurationMs(gcDurationMs) .heapDumpDurationMs(heapDumpDurationMs) .build(); heapdumpListener.analyze(heapDump); } return DONE; }Copy the code

In note 1, performed removeWeaklyReachableReferences () this method, the analysis of the meaning of it.

private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        retainedKeys.remove(ref.key);
    }
}
Copy the code

A while loop is used to iterate over the ReferenceQueue and remove the corresponding Reference from the retainedKeys.

Note 2 shows that if the Android device is in the debug state, it directly returns to RETRY. In comment 3, we look at the logic of the gone(reference) method.

private boolean gone(KeyedWeakReference reference) { return ! retainedKeys.contains(reference.key); }Copy the code

This determines whether reference is still in the retainedKeys collection, if not, it has been reclaimed, and if it has, a memory leak may have occurred (or the Gc has not yet reclaimed). In the previous analysis, we know that reference will be added into the referenceQueue when it is recycled. And then we’ll call removeWeaklyReachableReferences () traversal referenceQueue remove retainedKeys refrence inside.

And we see note 4, enforcing gcTrigger runGc () method for garbage collection, then use the removeWeaklyReachableReferences () method removes reference has been recovered. Let’s take a closer look at the implementation of runGc().

GcTrigger DEFAULT = new GcTrigger() {
    @Override public void runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libc  ore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every   time. Runtime.gc() is
      // more likely to perform a gc.
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

    private void enqueueReferences() {
      // Hack. We don't have a programmatic way to wait   for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        throw new AssertionError();
      }
    }
};
Copy the code

The system.gc() method is not used for collection because system.gc() is not executed every time. Instead, a copy of the code collected by GC is copied from AOSP to ensure that garbage collection works better than system.gc ().

Finally, let’s look at the code handling in comment 5. If the activity has not been reclaimed, a memory leak has occurred. If the activity has not been reclaimed, a memory leak has occurred. Inside, heapDumper’s dumpHeap() is called to generate the corresponding Hprof file. Here heapDumper is a heapDumper interface, the specific implementation is AndroidHeapDumper. Let’s examine how AndroidHeapDumper’s dumpHeap() method generates the hprof file.

public File dumpHeap() { File heapDumpFile = leakDirectoryProvider.newHeapDumpFile(); if (heapDumpFile == RETRY_LATER) { return RETRY_LATER; }... try { Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); . return heapDumpFile; } catch (Exception e) { ... // Abort heap dump return RETRY_LATER; }}Copy the code

The core action here is to call the Android SDK API debug.dumphprofData () to generate the hprof file.

If the file is RETRY_LATER, the generation fails and RETRY is performed. If it is not, the HeapDump object is successfully generated. Finally, the Analyze () function of heapdumpListener is executed to perform leak analysis on the newly created HeapDump object. ServiceHeapDumpListener = ServiceHeapDumpListener = ServiceHeapDumpListener Then you see the Analyze method of ServiceHeapDumpListener.

17, ServiceHeapDumpListener# analyze ()

@Override public void analyze(@NonNull HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
    
Copy the code

As you can see, the runAnalysis() method of HeapAnalyzerService is executed here. To avoid slowing down the app process or taking up memory, the HeapAnalyzerService is set up in a separate process. We continue to examine the processing inside the runAnalysis() method.

public final class HeapAnalyzerService extends ForegroundService implements AnalyzerProgressListener { ... public static void runAnalysis(Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass) { ... ContextCompat.startForegroundService(context, intent); }... @Override protected void onHandleIntentInForeground(@Nullable Intent intent) { ... // 1 HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses); // 2 AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey, heapDump.computeRetainedHeapSize); // 3 AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result); }... }Copy the code

HeapAnalyzerService is a ForegroundService of type IntentService. After executing startForegroundService(), Will the callback onHandleIntentInForeground () method. At note 1, a New HeapAnalyzer object is created that, as the name suggests, analyzes suspected leaks based on the Heap dumps information generated by RefWatcher. At comment 2, its checkForLeak() method is then called to parse the hprof file using the Haha library, as shown below:

public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile, @NonNull String referenceKey, boolean computeRetainedSize) { ... try { listener.onProgressUpdate(READING_HEAP_DUMP_FILE); // 1 HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile); // 2 HprofParser parser = new HprofParser(buffer); listener.onProgressUpdate(PARSING_HEAP_DUMP); Snapshot snapshot = parser.parse(); listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS); // 3 deduplicateGcRoots(snapshot); listener.onProgressUpdate(FINDING_LEAKING_REF); // 4 Instance leakingRef = findLeakingReference(referenceKey, snapshot); // 5 if (leakingRef == null) { return noLeak(since(analysisStartNanoTime)); } // 6 return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize); } catch (Throwable e) { return failure(e, since(analysisStartNanoTime)); }}Copy the code

At comment 1, a new memory mapped cache file, Buffer, is created. In comment 2, a new HprofParser is created using Buffer to parse the corresponding reference memory snapshot file, snapshot. In note 3, to reduce the memory stress effect of repeating GCRoots in Android version 6.0, the duplicate root object RootObj in GCRoots is removed using deduplicateGcRoots(). At comment 4, the findLeakingReference() method is called to compare the referenceKey passed in with the keyCandidate corresponding to the field values of all class instances in the Snapshot object. If none is equal, no memory leak has occurred. Calling the code directly at comment 5 returns a leakless AnalysisResult object. If an equal is found, a memory leak has occurred, and the findLeakTrace() method at comment 6 returns an AnalysisResult object with leak analysis results.

Finally, we come to analysis the note 3 HeapAnalyzerService AbstractAnalysisResultService. SendResultToListener () method, it is clear that Here AbstractAnalysisResultService implementation class is our first analysis is used to show the leakage path information DisplayLeakService object. Inside, create a leak notification directly built with PendingIntent for the user to click to display the detailed leak interface DisplayLeakActivity. The core code is as follows:

public class DisplayLeakService extends AbstractAnalysisResultService { @Override protected final void onHeapAnalyzed(@NonNull AnalyzedHeap analyzedHeap) { ... boolean resultSaved = false; boolean shouldSaveResult = result.leakFound || result.failure ! = null; if (shouldSaveResult) { heapDump = renameHeapdump(heapDump); // 1 resultSaved = saveResult(heapDump, result); } if (! shouldSaveResult) { ... showNotification(null, contentTitle, contentText); } else if (resultSaved) { ... // 2 PendingIntent pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey); . showNotification(pendingIntent, contentTitle, contentText); } else { onAnalysisResultFailure(getString(R.string.leak_canary_could_not_save_text)); }... } @Override protected final void onAnalysisResultFailure(String failureMessage) { super.onAnalysisResultFailure(failureMessage); String failureTitle = getString(R.string.leak_canary_result_failure_title); showNotification(null, failureTitle, failureMessage); }Copy the code

As you can see, only when the parsed heap information file is saved successfully, i.e. the resultSaved returned in comment 1 is true, will the logic in Comment 2 be executed, that is, to create a delay notification for the user to click to jump to DisplayLeakActivity. Finally, a source code flow chart is presented to review the LeakCanary operation in this article:

Four,

Performance optimization has always been one of the advanced and in-depth direction in Android, and memory leaks have always been an important part of performance optimization. Android Studio itself provides tools such as MAT to analyze memory leaks, but it takes a lot of time and energy to analyze, thus resulting in LeakCanary, which is very simple to use. However, after further analysis, it turns out that there is a lot of complex logic behind a simple API, and trying to understand it can lead to a different world.

Reference links:

LeakCanary V1.6.2 source code

2. Unpack LeakCanary step by step

3. Understand Android LeakCanary source code

Thank you for reading this article and I hope you can share it with your friends or technical group, it means a lot to me.