LeakCanary source code analysis

Posted by spiceydog on Thu, 27 Jan 2022 07:47:13 +0100

Memory leak

Memory leak caused by single case

public class SingletonActivityContext {
    private static SingletonActivityContext instance;
    private Context context;

    private SingletonActivityContext(Context context) {
        this.context = context;
    }

    public static SingletonActivityContext getInstance(Context context) {
        if (instance == null) {
            instance = new SingletonActivityContext(context);
        }
        return instance;
    }
}
  • Problem: if the context of the Activity is passed in, the memory will not be recycled when the Activity exits, because the singleton object holds the reference of the Activity.

  • Solution: pass in the context of the Application to make the life cycle of the single instance as long as that of the Application.

Memory leak caused by creating static instance of non static inner class

public class StaticLeakActivity extends Activity {
    private static noneStaticClass mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (mResource == null) {
            mResource = new noneStaticClass();
        }

    }

    private class noneStaticClass {

    }
}
  • Non static internal classes hold external class references. The life cycle of static instance mResource is as long as that of the application. As a result, static instances always hold references to StaticLeakActivity, which makes StaticLeakActivity unable to be recycled by memory.

  • Solution: change the non static inner class noneStaticClass into a static inner class (with static modification).

Memory leak caused by Handler

public class HandlerLeakActivity extends Activity {
    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
            }
        }, 1000 * 60 * 10);

        finish();
    }
    /*
    * terms of settlement:
    * 1.Declare Handler static
    * 2.Introduce Activity through weak reference
    *
    * */
}
  • Message Queue -> message -> Handler -> Activity.

  • solve:

    1. Declare the Handler as static, so the life cycle of the Handler has nothing to do with the Activity.
    2. Introduce Activity through weak reference.

    perhaps

    1. Handler is called when onDestroy removeCallbacksAndMessages();

Memory leak caused by Webview

public class WebviewLeakActivity extends AppCompatActivity {
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWebView = (WebView) findViewById(R.id.wv_show);
        mWebView.loadUrl("http://www.baidu.com");
    }


    @Override
    protected void onDestroy() {
        destroyWebView();
        android.os.Process.killProcess(android.os.Process.myPid());
        super.onDestroy();
    }

    private void destroyWebView() {
        if (mWebView != null) {
            mWebView.pauseTimers();
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }
    }

}

Solution: separate process

<activity
   android:name=".WebviewActivity"
   android:process="webview"/>

ReferenceQueue

  • Soft reference weak reference

  • Object is garbage collected, and the Java virtual machine will add this reference to the reference queue associated with it.

Source code analysis

Looking for memory leaks

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Fresco.initialize(this);
        (1)Follow in
        LeakCanary.install(this);
    }
}
public static RefWatcher install(Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      (1)Follow in
      .buildAndInstall();
}
public RefWatcher buildAndInstall() {
  (1)establish RefWatcher object
  RefWatcher refWatcher = build();
  if (refWatcher != DISABLED) {
    (2)To open the memory leak pop-up window Activity.  
    LeakCanary.enableDisplayLeakActivity(context);
    (3)adopt ActivityRefWatcher monitor Activity Memory leak.
    (4)Follow in  
    ActivityRefWatcher.install((Application) context, refWatcher);
  }
  return refWatcher;
}

(2)To open the memory leak pop-up window Activity.  The underlying call.
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);
}
public final class ActivityRefWatcher {

  /** @deprecated Use {@link #install(Application, RefWatcher)}. */
  @Deprecated
  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    install(application, refWatcher);
  }

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

  (1)activity Lifecycle Callback 
  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) {
          (4)important
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  /**
   * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
   * after they have been destroyed.
   */
  public ActivityRefWatcher(Application application, RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
  }

  void onActivityDestroyed(Activity activity) {
    (6)Follow in
    refWatcher.watch(activity);
  }
  (2)Register listening
  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }
  (3)Deregistration
  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
}
public final class RefWatcher {

  public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build();
  (1)To perform memory leak detection
  private final WatchExecutor watchExecutor;
 (2)Query whether it is in debug During debugging, memory leak detection will not be performed during re debugging
  private final DebuggerControl debuggerControl;
 (3)trigger GC garbage collection
  private final GcTrigger gcTrigger;
 (4)dump Heap file in memory
  private final HeapDumper heapDumper;
 (5)Holding objects that are to be detected and have memory leaks key
  private final Set<String> retainedKeys;
 (6)establish WeakReference The reference queue passed in during is mainly used to judge whether the objects held by weak references are deleted gc Recycling, gc After recycling, weak references are added to the queue
  private final ReferenceQueue<Object> queue;
 (7)Monitor generation heap File callback
  private final HeapDump.Listener heapdumpListener;
 (8) Some system memory leaks that need to be eliminated
  private final ExcludedRefs excludedRefs;
}
public void watch(Object watchedReference) {
  watch(watchedReference, "");
}

/**
 * Watches the provided references and checks if it can be GCed. This method is non blocking,
 * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
 * with.
 *
 * @param referenceName An logical identifier for the watched object.
 */
public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  final long watchStartNanoTime = System.nanoTime();
 (1)Add unique key value
  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
 (2)Create weak reference object
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);
 (3)Check whether the object is recycled
  ensureGoneAsync(watchStartNanoTime, reference);
}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
 (1)look down watchExecutor realization
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
     (2)Follow in
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}

@Override public void execute(Retryable retryable) {
  (3)adopt idleHandler Perform detection
  if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    waitForIdle(retryable, 0);
  } else {
    postWaitForIdle(retryable, 0);
  }
}

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

void waitForIdle(final Retryable retryable, final int failedAttempts) {
  // This needs to be called from the main thread.
  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override public boolean queueIdle() {
      postToBackgroundWithDelay(retryable, failedAttempts);
      return false;
    }
  });
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  long gcStartNanoTime = System.nanoTime();
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
  (1)Delete weak references that have been recycled 
  removeWeaklyReachableReferences();

  (2)debug Return during debugging
  if (debuggerControl.isDebuggerAttached()) {
    // The debugger can create false leaks.
    return RETRY;
  }
 (3)Return after recycling
  if (gone(reference)) {
    return DONE;
  }
 (4)Active trigger GC
  gcTrigger.runGc();
 (5)Delete weak references that have been recycled
  removeWeaklyReachableReferences();
 (6)Analysis started before it was recycled
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    (7)obtain dump snapshot
    File heapDumpFile = heapDumper.dumpHeap();
    if (heapDumpFile == RETRY_LATER) {
      // Could not dump the heap.
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
   (8)Start analysis
    heapdumpListener.analyze(
        new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
            gcDurationMs, heapDumpDurationMs));
  }
  return DONE;
}

Summary:

  1. Through application Activitylifecyclecallbacks listens to the onDestroy method of an Activity.

  2. When the Activity calls the onDestroy method, a weak reference is created to associate with the Activity. And assign a unique Key.

  3. When the Activity is GC, the weak reference will be added to ReferenceQueue.

  4. If it is not added to the queue, trigger GC manually and check whether it is added to the queue again.

  5. If not, a memory leak occurs.

Analyze memory leaks

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
  long analysisStartNanoTime = System.nanoTime();

  if (!heapDumpFile.exists()) {
    Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
  }

  try {
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    HprofParser parser = new HprofParser(buffer);
    (1)Continue Hprof Convert file to Snapshot snapshot
    Snapshot snapshot = parser.parse();
    (2)Remove duplicate memory leaks
    deduplicateGcRoots(snapshot);
    
    (3)according to key Query whether the parsing result has the object we need
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);

    // False alarm, weak reference was cleared in between key check and heap dump.
    if (leakingRef == null) {
      (4)stay heap dump References are recycled in the process
      return noLeak(since(analysisStartNanoTime));
    }
    (5)Leak path
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}
private Instance findLeakingReference(String key, Snapshot snapshot) {
  (1)Find the first leaked weak reference object from the memory snapshot
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  List<String> keysFound = new ArrayList<>();
  (2)Traverse all instances of this object
  for (Instance instance : refClass.getInstancesList()) {
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    String keyCandidate = asString(fieldValue(values, "key"));
   (3) If key Value and first defined key If the value is the same, the leaked object is returned
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    keysFound.add(keyCandidate);
  }
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
    Instance leakingRef) {

  ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
  ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

  // False alarm, no strong reference path to GC Roots.
  if (result.leakingNode == null) {
    return noLeak(since(analysisStartNanoTime));
  }

  LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

  String className = leakingRef.getClassObj().getClassName();

  // Side effect: computes retained size.
  snapshot.computeDominators();

  Instance leakingInstance = result.leakingNode.instance;

  long retainedSize = leakingInstance.getTotalRetainedSize();

  // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
  if (SDK_INT <= N_MR1) {
    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
  }

  return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
      since(analysisStartNanoTime));
}

Summary:

  1. Weak reference found in Snapshot

  2. Traverse all instances of the KeyedWeakReference class

  3. If the key value is the same as the key value defined at the beginning, the leaked object is returned

reference resources:

Topics: Java Android source code analysis Singleton pattern