LeakCanary introduction

LeakCanary provides a very convenient way for us to test for memory leaks during development, we don’t need to analyze the cause of memory leaks by memory blocks ourselves, we just need to integrate it into the project and it will help us detect memory leaks and give us a reference chain of memory leaks

integration

  • Add dependencies to Gradle without distinction between debug and release
    implementation 'com. Squareup. Leakcanary: leakcanary - android: 1.5.1'
Copy the code
  • Rewrite the Application
public class App extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate(a) {
        super.onCreate();

        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }

        mRefWatcher = LeakCanary.install(this);

    }


    public static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        returnapplication.mRefWatcher; }}Copy the code
  • For example, we create a memory leak in the Activity where the Handler sends delayed messages
public class ActivityOne extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_one);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run(a) {}},100000);
    }


    @Override
    protected void onDestroy(a) {
        super.onDestroy(); }}Copy the code
  • Then we open the activity and close it, and a Leaks icon appears on the desktop, and we open it

    If you want to monitor other memory leaks, for example, if you want to monitor Fragment memory leaks, you can write: actively monitor the object you want to monitor

public abstract class BaseFragment extends Fragment {
 
  @Override public void onDestroy(a) {
    super.onDestroy();
    RefWatcher refWatcher = App.getRefWatcher(getActivity());
    refWatcher.watch(this); }}Copy the code

The principle of overview

By listening to the onDestory of the Activity, manually calling GC, and then through the ReferenceQueue+WeakReference, judge whether the Activity object is reclaimed, and then combined with the HPOF file of dump Heap, analyze the leak location through Haha open source library

Key points

Registers listeners for the Activity’s lifecycle

Through the Application. RegisterActivityLifecycleCallbacks () method to register the Activity lifecycle listener, Every Actvity lifecycle callback to the ActivityLifecycleCallbacks, if an Activity to the onDestory, that means he will no longer exist, and then test whether this Activity is really be destroyed

ReferenceQueue+WeakReference is used to judge whether the object is reclaimed

When WeakReference is created, a ReferenceQueue object can be passed in. If the reference object in WeakReference is reclaimed, then the WeakReference object will be added to the ReferenceQueue. The ReferenceQueue is null to determine whether the ReferenceQueue is reclaimed

Detailed recommendations blog: www.jianshu.com/p/964fbc301…

MessageQueue adds an IdleHandler to get the main thread idle callback

For details, see Android Handler source code Parsing

After calling GC manuallySystem.runFinalization();This forces a call to the Finalize method of an object that has lost its reference

In accessibility algorithm, inaccessible objects, also do not have to die, when they are in the stage of “probation”, proclaim an object really need at least two death mark phase, if found no reference object, will mark for the first time, and conduct a screening, screening is the condition of whether these objects are necessary to finalize () method, When an object does not override Finalize (), or when Finalize () has already been called, both are considered “not necessary to execute.”

In Apolication, processName can be used to determine whether the task execution process is correct

The process is judged by processName

  public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
      CanaryLog.d(e, "Could not get package info for %s", context.getPackageName());
      return false;
    }
    String mainProcess = packageInfo.applicationInfo.processName;

    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
      serviceInfo = packageManager.getServiceInfo(component, 0);
    } catch (PackageManager.NameNotFoundException ignored) {
      // Service is disabled.
      return false;
    }

    if (serviceInfo.processName.equals(mainProcess)) {
      CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false;
    }

    int myPid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    List<ActivityManager.RunningAppProcessInfo> runningProcesses =
        activityManager.getRunningAppProcesses();
    if(runningProcesses ! =null) {
      for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
        if (process.pid == myPid) {
          myProcess = process;
          break; }}}if (myProcess == null) {
      CanaryLog.d("Could not find running process for %d", myPid);
      return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
  }
Copy the code

Source code analysis

The SDK initialization

mRefWatcher = LeakCanary.install(this);
Copy the code

This is the method that the SDK exposes to the outside, and we use this as the entry point for source analysis

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

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

The install method first initializes an AndroidRefWatcherBuilder class and then sets DisplayLeakService via the listenerServiceClass method. This class is used to analyze the result information of memory leaks and then send a notification to the user

public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {

  /**
   * Sets a custom {@link AbstractAnalysisResultService} to listen to analysis results. This
   * overrides any call to {@link #heapDumpListener(HeapDump.Listener)}.
   */
  public AndroidRefWatcherBuilder listenerServiceClass( Class
        listenerServiceClass) {
    return heapDumpListener(newServiceHeapDumpListener(context, listenerServiceClass)); }... }public class RefWatcherBuilder<T extends RefWatcherBuilder<T>> {.../ * *@see HeapDump.Listener */
  public final T heapDumpListener(HeapDump.Listener heapDumpListener) {
    this.heapDumpListener = heapDumpListener;
    returnself(); }... }Copy the code

In the AndroidExcludedRefs enumeration class, the ignore list information is defined. If there is a memory leak in the list of classes, it will not be displayed. HeapAnalyzer will also ignore the classes when calculating the GCRoot strong reference path. If you want a class in your project to leak, but you don’t want it to show, you can add the class to it

public enum AndroidExcludedRefs {

  // ######## Android SDK Excluded refs ########

  ACTIVITY_CLIENT_RECORD__NEXT_IDLE(SDK_INT >= KITKAT && SDK_INT <= LOLLIPOP) {
    @Override void add(ExcludedRefs.Builder excluded) {
      excluded.instanceField("android.app.ActivityThread$ActivityClientRecord"."nextIdle")
          .reason("Android AOSP sometimes keeps a reference to a destroyed activity as a"
              + " nextIdle client record in the android.app.ActivityThread.mActivities map."
              + " Not sure what's going on there, input welcome."); }}... }Copy the code

Finally, the buildAndInstall method is called, and a RefWatcher object is created and returned. This object is used to detect memory leaks due to unreclaimed objects

  /**
   * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
   */
  public RefWatcher buildAndInstall(a) {
    RefWatcher refWatcher = build();
    if(refWatcher ! = DISABLED) { LeakCanary.enableDisplayLeakActivity(context); ActivityRefWatcher.install((Application) context, refWatcher); }return refWatcher;
  }
Copy the code

Since the leak analysis is performed in another process, determine whether the currently started Application is in the leak analysis process. If yes, DISABLED is returned and no subsequent initialization is performed. If it is found to be in the main process of the program, initialization is performed

LeakCanary.enableDisplayLeakActivity(context); The main function is to call PackageManager to set DisplayLeakActivity to available.

 public static void enableDisplayLeakActivity(Context context) {
    setEnabled(context, DisplayLeakActivity.class, true);
  }

  public static void setEnabled(Context context, finalClass<? > componentClass,final boolean enabled) {
    final Context appContext = context.getApplicationContext();
    executeOnFileIoThread(new Runnable() {
      @Override public void run(a) { setEnabledBlocking(appContext, componentClass, enabled); }}); }public static void setEnabledBlocking(Context appContext, Class<? > componentClass,boolean enabled) {
    ComponentName component = new ComponentName(appContext, componentClass);
    PackageManager packageManager = appContext.getPackageManager();
    int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
    // Blocks on IPC.
    packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
  }
Copy the code

The LeakCanary profile shows that these files are running in a new process. DisplayLeakActivity defaults to Enable =false so that the launch icon can be hidden at first

<application> <service android:name=".internal.HeapAnalyzerService" android:process=":leakcanary" android:enabled="false"/> <service android:name=".DisplayLeakService" android:process=":leakcanary" android:enabled="false"/> <activity android:theme="@style/leak_canary_LeakCanary.Base" android:name=".internal.DisplayLeakActivity" android:process=":leakcanary" android:enabled="false" android:label="@string/leak_canary_display_activity_label" android:icon="@drawable/leak_canary_icon" android:taskAffinity="com.squareup.leakcanary.${applicationId}"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter>  </activity> <activity android:theme="@style/leak_canary_Theme.Transparent" android:name=".internal.RequestStoragePermissionActivity" android:process=":leakcanary" android:taskAffinity="com.squareup.leakcanary.${applicationId}" android:enabled="false" android:excludeFromRecents="true" android:icon="@drawable/leak_canary_icon" android:label="@string/leak_canary_storage_permission_activity_label"/> </application>Copy the code

Then ActivityRefWatcher. Install ((Application) context, refWatcher); RefWatcher is passed in as an argument and the Activity lifecycle is monitored

  public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).watchActivities();
  }


  public void watchActivities(a) {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }
  

 public void stopWatchingActivities(a) {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
  

 private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}@Override public void onActivityStarted(Activity activity) {}@Override public void onActivityResumed(Activity activity) {}@Override public void onActivityPaused(Activity activity) {}@Override public void onActivityStopped(Activity activity) {}@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity); }};void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

Copy the code

The listener lifecycleCallbacks will listen to all Activity lifecybacks in your project. OnActivityDestroyed will be called as the Activity is destroyed. LeakCanary picks up the Activity and analyzes it to see if there is a memory leak

Analyzing memory leaks

Here the analysis object whether the memory leak isRefWatcherClass. Here is a brief introduction to the member variables of this class

  • WatchExecutor WatchExecutor: Ensures that the task is running on the main thread, and defaults to a 5s delay for the system GC
  • DebuggerControl DebuggerControl: control center
  • GcTrigger GcTrigger: An internal callRuntime.getRuntime().gc()To manually trigger GC
  • HeapDumper HeapDumper: Used to create a.hprof file that stores a snapshot of the head heap to see which programs are using most of the memory
  • HeapdumpListener: This Listener is told when the analysis result is complete
  • ExcludedRefs ExcludedRefs: indicates the whitelist for analyzing memory leaks

As you can see, whenever the Activity is destroyed, RefWatcher’s watch method is called to analyze whether it is a memory leak

 public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

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

    ensureGoneAsync(watchStartNanoTime, reference);
  }
Copy the code

This code places a random number key in the retainedKeys container to determine whether an object is reclaimed, creates a weak reference, saves the Activity object to be analyzed, and then calls the ensureGoneAsync method

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run(a) {
        returnensureGone(reference, watchStartNanoTime); }}); }Copy the code

The watchExecutor is then used to schedule the analysis task, which is mainly to ensure that it is delayed for 5 seconds on the main thread to allow the system time for GC

  @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    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);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }



  private void removeWeaklyReachableReferences(a) {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while((ref = (KeyedWeakReference) queue.poll()) ! =null) { retainedKeys.remove(ref.key); }}private boolean gone(KeyedWeakReference reference) {
    return! retainedKeys.contains(reference.key); }Copy the code

First by removeWeaklyReachableReferences () method, try to get to be analysed from a weak reference queue object, if not the system is empty that recycling, is the key value of retainedKeys removed, if the system recycling returns DONE directly, if has not been recycling system, Call gctrigger.rungc () manually; Manual trigger system gc, then call again removeWeaklyReachableReferences () method, or null, if the judge as memory leaks

 GcTrigger DEFAULT = new GcTrigger() {
    @Override public void runGc(a) {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/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 perfom a gc.
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

    private void enqueueReferences(a) {
      // 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 newAssertionError(); }}};Copy the code

The enqueueReferences method is used to suspend GC for 100ms. System.runfinalization (); This forces a call to the Finalize method of an object that has lost its reference

When a memory leak is determined, call heapDumper.Dumpheap (); Generate the.hprof file and then call back to the heapdumpListener listener, which implements the ServiceHeapDumpListener class and calls the Analyze () method


public final class ServiceHeapDumpListener implements HeapDump.Listener {...@Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump"); HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass); }}Copy the code

Heapdumps is a modle class, which is used to store some information analysis of such strong reference need HeapAnalyzerService. RunAnalysis method is to send an intent, launched HeapAnalyzerService service, This is an intentService

 public static void runAnalysis(Context context, HeapDump heapDump, Class
        listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }
Copy the code

After the service is started, the analysis will start in the onHandleIntent method to find the memory leak reference

  @Override 
  protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
Copy the code

Finding references

  • One is instantiated after the memory leak analysis service is startedHeapAnalyzerObject, and then callcheckForLeakMethod to analyze the final result,
  • checkForLeakHere’s another Square library. Haha, ha, ha, ha, ha, ha, ha, ha.Github.com/square/haha…
  • Call when you get the resultAbstractAnalysisResultService.sendResultToListener()Method that starts another service

  public static void sendResultToListener(Context context, String listenerServiceClassName, HeapDump heapDump, AnalysisResult result) { Class<? > listenerServiceClass;try {
      listenerServiceClass = Class.forName(listenerServiceClassName);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
    Intent intent = new Intent(context, listenerServiceClass);
    intent.putExtra(HEAP_DUMP_EXTRA, heapDump);
    intent.putExtra(RESULT_EXTRA, result);
    context.startService(intent);
  }
Copy the code

ListenerServiceClassName is the DisplayLeakService passed in to start LeakCanary. Install, which is itself an intentService

@Override 
protected final void onHandleIntent(Intent intent) {
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAP_DUMP_EXTRA);
    AnalysisResult result = (AnalysisResult) intent.getSerializableExtra(RESULT_EXTRA);
    try {
      onHeapAnalyzed(heapDump, result);
    } finally {
      //noinspection ResultOfMethodCallIgnoredheapDump.heapDumpFile.delete(); }}Copy the code

Then call its own onHeapAnalyzed method

protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = LeakCanary.leakInfo(this, heapDump, result, true);
    CanaryLog.d("%s".new Object[]{leakInfo});
    boolean resultSaved = false;
    booleanshouldSaveResult = result.leakFound || result.failure ! =null;
    if(shouldSaveResult) {
        heapDump = this.renameHeapdump(heapDump);
        resultSaved = this.saveResult(heapDump, result);
    }

    PendingIntent pendingIntent;
    String contentTitle;
    String contentText;

    / / set up alerts pendingIntent/contentTitle/contentText.int notificationId1 = (int)(SystemClock.uptimeMillis() / 1000L);
    LeakCanaryInternals.showNotification(this, contentTitle, contentText, pendingIntent, notificationId1);
    this.afterDefaultHandling(heapDump, result, leakInfo);
}
Copy the code

This method first determines whether you need to save the information to the local, if you need to save to your local, and then set the basic information of the notification, finally through LeakCanaryInternals. ShowNotification method call system notification bar, tell the user has a memory leak

So far, LeakCanary’s detection of memory leak source code has been analyzed

Reference: blog.csdn.net/xiaohanluo/…

Juejin. Im/post / 684490…