LeakCanary use

LeakCanary is a memory leak detection library for Android. This paper analyzes the source code from the following four points

  • Check for memory leaks
  • Check the timing of memory leaks
  • How do I determine a memory leak
  • How to analyze memory leaks (a little, maybe none)
  • Memory leak false positives

1. Check for memory leaks

AppWatcherInstaller inherits from ContentProvider and is called between ApplicationattachBaseContext(Context)andonCreate()Init in this way.

Method 2 manualinstall watchersToInstall implements the default parameters, in this way we see the Activity, FragmentAndViewModel, RootView, Service four observer

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}
Copy the code

2. Check the timing of memory leaks

2.1 ActivityWatcher

The activity triggers OnDestory to check whether the activity instance is recycled

private val lifecycleCallbacks =
  object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
    override fun onActivityDestroyed(activity: Activity) {
      reachabilityWatcher.expectWeaklyReachable(
        activity, "${activity::class.java.name} received Activity#onDestroy() callback")}}Copy the code

2.2 FragmentAndViewModelWatcher

Fragment triggers onFragmentDestroyed or onFragmentViewDestroyed to check whether the Fragment instance can be reclaimed. ViewModel Triggers onClear to check whether the viewModel instance can be reclaimed

2.2.1 Checking Fragments

. Because Android now has three fragments androidx fragments. The Android app. The app. The fragments. Android support. The v4. App. The fragments LeakCanary checks to see if any of the above three fragments are introduced via reflection, and if so, creates the corresponding Watcher to add to fragmentDestroyWatchers

private fun getWatcherIfAvailable(
  fragmentClassName: String,
  watcherClassName: String,
  reachabilityWatcher: ReachabilityWatcher
): ((Activity) -> Unit)? {

  return if (classAvailable(fragmentClassName) &&
    classAvailable(watcherClassName)
  ) {
    val watcherConstructor =
      Class.forName(watcherClassName).getDeclaredConstructor(ReachabilityWatcher::class.java)
    @Suppress("UNCHECKED_CAST")
    watcherConstructor.newInstance(reachabilityWatcher) as (Activity) -> Unit
  } else {
    null}}Copy the code

2.2.2 Fragment Memory Leak Check Time

(1) The application registers the activity lifecycle callback. (2) When listening to the Ctivity being created, get the corresponding fragmentManager of that activity to create the fragment’s lifecycle observer (3) when the onFragmentViewDestroyed/onFragmentDestroyed trigger, traverse a collection and then check whether can be recycled fragments instance

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

  override fun onFragmentViewDestroyed(
    fm: FragmentManager,
    fragment: Fragment
  ) {
    val view = fragment.view
    if(view ! =null) {
      reachabilityWatcher.expectWeaklyReachable(
        view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
        "(references to its views should be cleared to prevent leaks)")}}override fun onFragmentDestroyed(
    fm: FragmentManager,
    fragment: Fragment
  ) {
    reachabilityWatcher.expectWeaklyReachable(
      fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback")}}Copy the code

2.2.3 Check which ViewModel memory leaks

Since the Fragment/Activity is destroyed and the Fragment/Activity object is reclaimed, all viewModel instances bound to the Fragment/Activity should also be destroyed, so leakCanary adds memory checks to the ViewModel (1) Listen When the activity is created, bind a spy viewModel instance

//AndroidXFragmentDestroyWatcher
override fun invoke(activity: Activity) {
  if (activity is FragmentActivity) {
    val supportFragmentManager = activity.supportFragmentManager
    supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    ViewModelClearedWatcher.install(activity, reachabilityWatcher)
  }
}
Copy the code

When the fragment is created, bind a spy viewModel instance

//AndroidXFragmentDestroyWatcher##fragmentLifecycleCallbacks
override fun onFragmentCreated(
  fm: FragmentManager,
  fragment: Fragment,
  savedInstanceState: Bundle?). {
  ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
}
Copy the code

2.2.4 ViewModel Memory leak check time

(1) Use reflection to get the Fragment /activity bound viewModel set (2) When the ViewModel life cycle of leakCanary binding reaches onCleared, check to see if all viewModel instances are recoverable.

//ViewModelClearedWatcher
override fun onCleared(a){ viewModelMap? .values? .forEach { viewModel -> reachabilityWatcher.expectWeaklyReachable( viewModel,"${viewModel::class.java.name} received ViewModel#onCleared() callback")}}Copy the code

2.3 RootViewWatcher

View triggers onViewDetachedFromWindow to check whether the View instance is recycled Using Curtains, a view is to check all is added to the phoneWindow above, windowLayoutParams. The title for Toast or a Tooltip, or all except PopupWindow view.

//RootViewWatcher
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

  val watchDetachedView = Runnable {
    reachabilityWatcher.expectWeaklyReachable(
      rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback")}override fun onViewAttachedToWindow(v: View) {
    WindowManager.LayoutParams.TYPE_PHONE
    mainHandler.removeCallbacks(watchDetachedView)
  }

  override fun onViewDetachedFromWindow(v: View) {
    mainHandler.post(watchDetachedView)
  }
})
Copy the code

2.4 ServiceWatcher

Service triggers onDestroy to check whether the service instance is reclaimed

private fun onServiceDestroyed(token: IBinder){ servicesToBeDestroyed.remove(token)? .also { serviceWeakReference -> serviceWeakReference.get()? .let { service -> reachabilityWatcher.expectWeaklyReachable( service,"${service::class.java.name} received Service#onDestroy() callback")}}}Copy the code

3. How to identify memory leaks

ReferenceQueue: ReferenceQueue to which the garbage collector adds registered reference objects after detecting appropriate reachability changes

(1) Add the object to be checked to weakReference and watchedObjects

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  if(! isEnabled()) {return
  }
  removeWeaklyReachableObjects()
  val key = UUID.randomUUID()
    .toString()
  val watchUptimeMillis = clock.uptimeMillis()
  val reference =
    KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
  SharkLog.d {
    "Watching " +
      (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
      (if (description.isNotEmpty()) "($description)" else "") +
      " with key $key"
  }

  watchedObjects[key] = reference
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}
Copy the code

(6) After GC is performed, the ReferenceQueue is traversed and the objects saved in the watchedObjects collection are deleted

private fun removeWeaklyReachableObjects(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.
  var ref: KeyedWeakReference?
  do {
    ref = queue.poll() as KeyedWeakReference?
    if(ref ! =null) {
      watchedObjects.remove(ref.key)
    }
  } while(ref ! =null)}Copy the code

(3) Determine whether the length of watchedObjects is changed. If it is, memory leakage is considered

private fun checkRetainedCount(
  retainedKeysCount: Int,
  retainedVisibleThreshold: Int,
  nopeReason: String? = null
): Boolean {
  valcountChanged = lastDisplayedRetainedObjectCount ! = retainedKeysCount ...if (retainedKeysCount < retainedVisibleThreshold) {
    if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
      if (countChanged) {
        onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
      }
      showRetainedCountNotification(
        objectCount = retainedKeysCount,
        contentText = application.getString(
          R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
        )
      )
      scheduleRetainedObjectCheck(
        delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
      )
      return true}}return false
}
Copy the code

(10) Hprof file will be generated when 5 memory leaks are detected

override fun dumpHeap(a): DumpHeapResult {
...
val durationMillis = measureDurationMillis {
  Debug.dumpHprofData(heapDumpFile.absolutePath)
}
...
}
Copy the code

4. How to analyze memory leaks

Analyze hprof files using Shark analysis tool

(8) Here, heapAnalysis objects are generated by parsing hprof files. SharkLog is printed and stored in the database

override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
  SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }

  val db = LeaksDbHelper(application).writableDatabase
  val id = HeapAnalysisTable.insert(db, heapAnalysis)
  db.releaseReference()
...
}
Copy the code

5. False alarms are generated for memory leaks

The mainstream garbage collector of Java virtual machine adopts the reachable analysis algorithm. The reachable algorithm is traversed from the GC root node. If the node cannot be traversed from the root node, the corresponding object of the node is in the recyclable state. Otherwise it will not be recycled.

public class MainActivity2 extends FragmentActivity {
    Fragment mFragmentA;
    Fragment mFragmentB;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        mFragmentA = new FragmentA();
        mFragmentB = new FragmentB();
        findViewById(R.id.buttona).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { replaceFragment(mFragmentA); }}); findViewById(R.id.buttonb).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { replaceFragment(mFragmentB); }}); }private void replaceFragment(Fragment fragment) { getSupportFragmentManager().beginTransaction() .replace(R.id.container, fragment).commit(); }}Copy the code

In the fragment case, Leakcanary believes that the fragment should be released once it has gone onDestory. But is this really a memory leak?

├ ─ com. Example. MainActivity2 instance │ Leaking: MFragmentA │ ~~~~~~~~~~ ╰→ com.example.FragmentA instance Leaking: YES (ObjectWatcher was watching this because com.example.FragmentA received Fragment#onDestroy() callback and Fragment#mFragmentManager is null) key = 216c8cf8-2cdb-4509-84e9-8404afefffeb watchDurationMillis = 3804 retainedDurationMillis = -1 key = eaa41c88-bccb-47ac-8fb7-46b27dec0356 watchDurationMillis = 6113 retainedDurationMillis  = 1112 key = 77d5f271-382b-42ec-904b-1e8a6d4ab097 watchDurationMillis = 7423 retainedDurationMillis = 2423 key = 8d79952f-a300-4830-b513-62e40cda8bba watchDurationMillis = 15771 retainedDurationMillis = 10765 13858 bytes retained by leaking objects Signature: f1d17d3f6aa4713d4de15a4f465f97003aa7Copy the code

Based on the stack information, LeakCanary thinks fragmentA has gone and onDestory should reclaim the fragmentA object, but finds that it is still held by MainActivity2 and cannot be recycled, and determines a memory leak. In our logic, it’s okay for fragments not to be released. It’s just that this implementation isn’t memory optimal.