LeakCanary 2.0 principle

Background:

Android applications are based on Java (Kotlin) implementation, so it also inherits the advantages and disadvantages of Java, the most typical is the problem of memory collection, the JVM in GC to developers as far as possible not to pay attention to garbage object collection, but if the development does not pay attention to memory problems may cause the application memory leak and OOM. Incorrect use of handlers, threads, etc., can result in OOM. After applying for a certain amount of memory, the application is prone to OOM and crash if the memory is not released in time. Therefore, in the actual development process, on the one hand, errors should be avoided, and on the other hand, the third-party tool (LeakCanary) should be used to detect problems in time.

Introduction to the

LeakCanary is essentially an open source tool that automates memory leak detection for Android applications based on MAT. By integrating the tool code into our Android project, we can find and locate memory leaks during application debugging and development.

LeakCanary Example figure:

The warehouse address

usage

(1) Version 1.6.1 has introduced LeakCanary into its own project

    debugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 1.6.1'
    releaseImplementation 'com. Squareup. Leakcanary: leakcanary - android - no - op: 1.6.1'
    debugImplementation 'com. Squareup. Leakcanary: leakcanary - support - fragments: 1.6.1'
Copy the code

Added to the project’s custom Application class for initialization tasks.

    private fun initLeakCanary() {if (LeakCanary.isInAnalyzerProcess(this)) {
            return
        }
        LeakCanary.install(this)
    }
Copy the code

(2) Version 2.1 has introduced LeakCanary into our own project, but after version 2.0 it is non-invasive. Integrating LeakCanary only requires importing into the library, and initialization is done internally by implementing the ContentProvider class.

PS: Now more and more libraries use this way to complete the initialization task, which simplifies the development cost of third-party access to the library, but also makes the business side seem unfriendly once they want to do startup optimization and other optimization items.

debugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 2.1'
Copy the code

The principle of analysis

Prerequisite: Before continuing to read this article, please familiarize yourself with the fact that ReferenceQueue code in version 2.1 is implemented entirely based on Kotlin, which may not be friendly to developers who are not familiar with Kotlin.

Question: The full life cycle of an Android application consists of the life cycle of its components. If you want to analyze whether an application has a memory leak, you only need to perform the onDestory() method on an Activity or Fragment. After the onDestory is executed, other objects still hold references to the Activity. As a result, the Activity object cannot be released in time, which may cause a memory leak.

Solution:

We know that when WeakReference is created, if a ReferenceQueue object is specified, after the garbage collector detects the change of the reachability of the referenced object, The garbage collector adds the registered reference object to the ReferenceQueue queue for processing by the ReferenceQueue. However, if the reference object is not added to the ReferenceQueue after GC, there may be a memory leak.

So we can observe the memory leak of the Activity reference through WeakReference object, ReferenceQueue and GC by listening to the activity.onDestroy () callback. If the Activity object is not recovered, After finding out whether the Activity object is referenced by other objects, if it is referenced by other objects, heap dump is performed to generate the complete memory reference chain (the shortest reference chain) and display it through notification, etc.

LeakCanary start

In version 2.0, the business side is not required to complete the initialization task in the Application, and LeakCanary2.0 leverages the principle that ContentProvider is loaded before the application is created, The onCreate of the ContentProvider completes the initialization task.

(1) Configuration of ContentProvider

   <provider
            android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
            android:exported="false"
            android:authorities="com.jandroid.myapplication.leakcanary-installer" />
Copy the code

(2) initialization tasks In the onCreate method calls the InternalAppWatcher. Install initialized (application)

override fun onCreate(): Boolean { val application = context!! .applicationContext as Application InternalAppWatcher.install(application)return true
  }
Copy the code

LeakCanary initialization

LeakCanary through InternalAppWatcher. Install method completed the main initialization tasks.

(1) Check whether the thread is in the main thread by Looper, otherwise throw an exception.

(2) Use ActivityDestroyWatcher to finish listening on activity.ondestroy ().

(3) Use FragmentDestroyWatcher to listen on fragment.ondestroy ().

(4) Use InternalLeakCanary to initialize the objects needed in the process of memory leak detection.

  fun install(application: Application) {
    SharkLog.logger = DefaultCanaryLog()
    SharkLog.d { "Installing AppWatcher" }
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    InternalAppWatcher.application = application

    val configProvider = { AppWatcher.config }
    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
    onAppWatcherInstalled(application)
  }
Copy the code

The onAppWatcherInstalled variable is initialized in kotlin’s init block, which corresponds to InternalLeakCanary. Then call InternalLeakCanary class ** Override Fun Invoke (Application: Application)** to initialize the object needed to detect memory leaks.

  init {
    val internalLeakCanary = try {
      val leakCanaryListener = Class.forName("leakcanary.internal. init { val internalLeakCanary = try { val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary") leakCanaryListener.getDeclaredField("INSTANCE") .get(null) } catch (ignored: Throwable) { NoLeakCanary } @kotlin.Suppress("UNCHECKED_CAST") onAppWatcherInstalled = internalLeakCanary as (Application) -> Unit }")
      leakCanaryListener.getDeclaredField("INSTANCE")
          .get(null)
    } catch (ignored: Throwable) {
      NoLeakCanary
    }
    @kotlin.Suppress("UNCHECKED_CAST")
    onAppWatcherInstalled = internalLeakCanary as (Application) -> Unit
  }
Copy the code

ActivityDestroyWatcher.install()

ActivityDestroyWatcher class object ActivityDestroyWatcher is created in the install method

(2) The Activity lifecycle listener class is registered, and when onDestroy() is heard, the objectwatcher.watch () method is called to monitor the Activity reference.

ObjectWatcher is created in InternalAppWatcher class.


internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
          )
        }
      }
    }
  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider)
      application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}
Copy the code

FragmentDestroyWatcher.install()

(1) FragmentDestroyWatcher is a static class, and install is a static method

(2) if the SDK is android 8.0 and above, use the AndroidOFragmentDestroyWatcher listening fragments of life cycle. (3) by loading supportfragment getWatcherIfAvailable method of life cycle AndroidSupportFragmentDestroyWatcher listening class.

(4) by loading androidx getWatcherIfAvailable method of life cycle AndroidXFragmentDestroyWatcher listening class.

(5) By listening to the onCreate method of the activity and calling the Invoke method of the Fragment listener class after the activity method, complete the initialization task of the Fragment listener class. The reason for listening on an activity is because the Fragment needs to be attached to the activity, and when the activity is created, there may be a Fragment object created.

To summarize, there are three listening classes for fragments. AndroidOFragmentDestroyWatcher, AndroidSupportFragmentDestroyWatcher, AndroidXFragmentDestroyWatcher. But there is no difference in the essential implementation of the three classes.


internal object FragmentDestroyWatcher {

  private const val ANDROIDX_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"
  private const val ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME =
    "leakcanary.internal.AndroidXFragmentDestroyWatcher"

  private const val ANDROID_SUPPORT_FRAGMENT_CLASS_NAME = "android.support.v4.app.Fragment"
  private const val ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME =
    "leakcanary.internal.AndroidSupportFragmentDestroyWatcher"

  fun install(
    application: Application,
    objectWatcher: ObjectWatcher,
    configProvider: () -> AppWatcher.Config
  ) {
    val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

    if(SDK_INT >= O) { fragmentDestroyWatchers.add( AndroidOFragmentDestroyWatcher(objectWatcher, configProvider) ) } getWatcherIfAvailable( ANDROIDX_FRAGMENT_CLASS_NAME, ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME, objectWatcher, configProvider )? .let { fragmentDestroyWatchers.add(it) } getWatcherIfAvailable( ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME, objectWatcher, configProvider )? .let { fragmentDestroyWatchers.add(it) }if (fragmentDestroyWatchers.size == 0) {
      return
    }

    application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        for (watcher in fragmentDestroyWatchers) {
          watcher(activity)
        }
      }
    })
  }

  private fun getWatcherIfAvailable(
    fragmentClassName: String,
    watcherClassName: String,
    objectWatcher: ObjectWatcher,
    configProvider: () -> AppWatcher.Config
  ): ((Activity) -> Unit)? {

    return if (classAvailable(fragmentClassName) &&
        classAvailable(watcherClassName)
    ) {
      val watcherConstructor = Class.forName(watcherClassName)
          .getDeclaredConstructor(ObjectWatcher::class.java, Function0::class.java)
      @Suppress("UNCHECKED_CAST")
      watcherConstructor.newInstance(objectWatcher, configProvider) as (Activity) -> Unit

    } else {
      null
    }
  }

  private fun classAvailable(className: String): Boolean {
    return try {
      Class.forName(className)
      true} catch (e: Throwable) { // e is typically expected to be a ClassNotFoundException // Unfortunately, The prior to version 25.0.2 of the support library the / / FragmentManager FragmentLifecycleCallbacks class was a non static inner class. // Our AndroidSupportFragmentDestroyWatcher class is compiled against the static version of // the FragmentManager.FragmentLifecycleCallbacks class, leading to the // AndroidSupportFragmentDestroyWatcher class being rejected and a NoClassDefFoundError being // thrown here. So we're just covering our butts here and catching everything, and assuming // any throwable means "can't use this". See https://github.com/square/leakcanary/issues/1662 false } } }Copy the code

AndroidSupportFragmentDestroyWatcher

(1) The view of the Fragment is used to listen on the Fragment.

(2) The fragment lifecycle listener class is registered. When onDestroy() is heard, the objectwatcher.watch () method is called to monitor the fragment reference.


internal class AndroidSupportFragmentDestroyWatcher(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) : (Activity) -> Unit {

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

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if(view ! = null && configProvider().watchFragmentViews) { objectWatcher.watch( 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
    ) {
      if (configProvider().watchFragments) {
        objectWatcher.watch(
            fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
        )
      }
    }
  }

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

InternalLeakCanary

(1) Initialize some objects needed in the process of detecting memory leaks.

(2) addOnObjectRetainedListener set callback may be at risk for memory leaks.

(3) Perform heap dump after memory leak through AndroidHeapDumper.

(4) Manually call GC through GcTrigger to confirm the memory leak again.

(5) Start the thread of memory leak check.

(6) by registerVisibilityListener to monitor the visibility of the application.


  override fun invoke(application: Application) {
    this.application = application

    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)

    val gcTrigger = GcTrigger.Default

    val configProvider = { LeakCanary.config }

    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)

    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
        configProvider
    )
    application.registerVisibilityListener { applicationVisible ->
      this.applicationVisible = applicationVisible
      heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
    }
    addDynamicShortcut(application)

    disableDumpHeapInTests()
  }

Copy the code

Listening ObjectWatcher

Both activity and frgment use ObjectWatcher’s Watch method to perform listening tasks.

(1) removeWeaklyReachableObjects removed if can reach the object (object) to be GC, also there is not the object of a memory leak.

(2) create a weak reference object referenced by watchedObject and associate it with the ReferenceQueue ReferenceQueue

(3) watchedObjects is the LinkedHashMap object, which is used to store the WeakReference object. This usually refers to an object reference that has not yet been determined whether there is a memory leak.

(4) Implement moveToRetained method in mainHandler. (Check whether the object is reachable after 5s).

  @Synchronized fun watch(
    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)
    }
  }

 private val checkRetainedExecutor = Executor {
    mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)
  }
Copy the code

moveToRetained

(1) removeWeaklyReachableObjects method is mainly to remove watchedObjects by GC reference queue.

(2) If the current reference retainedRef is not removed and is still in the watchedObjects queue, it is not GC yet, indicating that there may be a memory leak, which needs to be determined later.

(3) in InternalLeakCanary class we call addOnObjectRetainedListener introduced into OnObjectRetainedListener class object. We are back to the InternalLeakCanary class by calling the onObjectre谴 责 method.

(4) removeWeaklyReachableObjects method, removed the watchedObjects in the queue will be GC reference objects, the other is the memory leak.

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if(retainedRef ! = null) { retainedRef.retainedUptimeMillis = clock.uptimeMillis() onObjectRetainedListeners.forEach { it.onObjectRetained() } } } private funremoveWeaklyReachableObjects() {
    // 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

onObjectRetained

Based on the above analysis, we call back to the OnObjecTre谴 责 method of the InternalLeakCanary class.

(1) Determine whether the initialization of heapDumpTrigger is complete. The onObjectreorganism method of HeapDumpTrigger is called.

(2) in HeapDumpTrigger onObjectRetained scheduleRetainedObjectCheck method call.

  override fun onObjectRetained() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onObjectRetained()
    }
  }
Copy the code

scheduleRetainedObjectCheck

(1) The handle of the backgroundHandler corresponds to the following sub-thread name: private const val LEAK_CANARY_THREAD_NAME = “LeakCanary- heap-dump”

(2) By calling checkRetainedObjects, the detection task of objects that may have memory leaks is completed.


  private fun scheduleRetainedObjectCheck(
    reason: String,
    rescheduling: Boolean,
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      val scheduledIn = checkCurrentlyScheduledAt - SystemClock.uptimeMillis()
      SharkLog.d { "Ignoring request to check for retained objects ($reason), already scheduled in ${scheduledIn}ms" }
      return
    } else {
      val verb = if (rescheduling) "Rescheduling" else "Scheduling"
      val delay = if (delayMillis > 0) " in ${delayMillis}ms" else ""
      SharkLog.d { "$verb check for retained objects${delay} because $reason" }
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects(reason)
    }, delayMillis)
  }
Copy the code

checkRetainedObjects

(1) retainedReferenceCount obtains the number of objects in objectWatcher that may have memory leaks

(2) If the number of retainedReferenceCount is greater than 0, GC will be manually executed to update the value of retainedReferenceCount again. (3) Heap dump is not performed when the number of current retainedReferenceCount leaked instances is less than 5.

(4) if the heap dump two intervals of less than 60 s, heapdumps is not, and only do showRetainedCountNotification notification bar and in scheduleRetainedObjectCheck detection time by notice.

(5) Start the ForegroundService HeapAnalyzerService to process heap dump files.


  private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if(! config.dumpHeap) { SharkLog.d {"Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    if(! config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) { showRetainedCountNotification( objectCount = retainedReferenceCount, contentText = application.getString( R.string.leak_canary_notification_retained_debugger_attached ) ) scheduleRetainedObjectCheck( reason ="debugger is attached",
          rescheduling = true,
          delayMillis = WAIT_FOR_DEBUG_MILLIS
      )
      return
    }

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if(elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { showRetainedCountNotification( objectCount = retainedReferenceCount, contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) ) scheduleRetainedObjectCheck(  reason ="previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
          rescheduling = true,
          delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
    dismissRetainedCountNotification()
    dumpHeap(retainedReferenceCount, retry = true)}Copy the code

GcTrigger

(1) There may be observed references that are about to become weakly reachable, but are not yet queued for references. This should be an active GC call to possibly avoid a heap dump.

Lai source: blog.csdn.net/u012551350/…

object Default : GcTrigger {
    override fun runGc() {
      // 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 perform a gc. Runtime.getRuntime() .gc() enqueueReferences() System.runFinalization() }Copy the code

checkRetainedCount

(1) If there is no memory leaking object, heap dump is not performed

(2) if there is a memory leak object number less than val retainedVisibleThreshold: Int = 5, and is not to heap dump

private fun checkRetainedCount( retainedKeysCount: Int, retainedVisibleThreshold: Int ): Boolean { val countChanged = lastDisplayedRetainedObjectCount ! = retainedKeysCount lastDisplayedRetainedObjectCount = retainedKeysCountif (retainedKeysCount == 0) {
      SharkLog.d { "Check for retained object found no objects remaining" }
      if (countChanged) {
        showNoMoreRetainedObjectNotification()
      }
      return true
    }

    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        showRetainedCountNotification(
            objectCount = retainedKeysCount,
            contentText = application.getString(
                R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
            )
        )
        scheduleRetainedObjectCheck(
            reason = "found only $retainedKeysCount retained objects (< $retainedVisibleThreshold while app visible)",
            rescheduling = true,
            delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true}}return false
  }

Copy the code

dumpHeap

(1) showRetainedCountNotification shows that leakage number notification bar

(2) Start a foreground service HeapAnalyzerService to analyze heap dump files.


  private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
      if (retry) {
        SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
        scheduleRetainedObjectCheck(
            reason = "failed to dump heap",
            rescheduling = true,
            delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
        )
      } else {
        SharkLog.d { "Failed to dump heap, will not automatically retry" }
      }
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
              R.string.leak_canary_notification_retained_dump_failed
          )
      )
      return
    }
    lastDisplayedRetainedObjectCount = 0
    lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

Copy the code

dump

To be updated