preface

LeakCanary is a simple and convenient memory leak detection framework that many students have already used. It is very convenient to use and has the following features: 1. No manual initialization is required. 2. Memory leaks can be detected automatically and alarms can be sent through notification 3. Cannot be used online

LeakCanary how does LeakCanary detect memory leaks and how does LeakCanary initialize? 3. How does LeakCanary find a memory leak? 4. Why does LeakCanary not work online?

This article reviews the main process for LeakCanary memory leak detection and answers the above questions

1. LeakCanaryPrinciples and basic flow of memory leak detection

1.1 Principles of Memory Leakage

Causes of memory leaks: Objects that are no longer needed are still referenced, so that the memory allocated for the object cannot be reclaimed.

For example: aActivityThe instance object is being calledonDestoryMethod is no longer needed if a reference is storedActivityThe static field of the object will result inActivityCannot be collected by garbage collector.

The reference chain comes from the garbage collector’s reachability analysis algorithm: when an object arrivesGC RootsWhen no reference chain is attached, the object is proved to be unavailable. As shown in figure:



objectobject5,object6,object7They’re related to each other, but they go toGC RootsAre unreachable, so they will be judged to be recyclable objects.

JavaLanguage, can be asGC RootsObjects include the following:

  • The object referenced in the virtual machine stack (the local variable table in the stack frame).
  • Objects referenced by static properties in the method area.
  • The object referenced by the constant in the method area.
  • Objects referenced by JNI (commonly referred to as Native methods) in the Native method stack.

1.2 LeakCanaryBasic process for detecting memory leaks

Knowing how memory leaks work, we can guess what the basic flow of LeakCanary looks like. Trigger detection (objects that are no longer needed) after the page is closed. 2. Trigger GC and get objects that still exist, which are likely to leak. 3. Store the results and use notifications to alert users to leaks

The overall flow chart is as follows:

  • 1.ObjectWatcherCreated aKeyedWeakReferenceTo monitor objects.
  • 2. Later, in the background thread, the delay checks to see if the reference has been cleared and fires if it has notGC
  • 3. If the reference has not been cleared, it willdumps the heapTo a.hprofFile, and then will.hprofThe file is stored to the file system.
  • 4. The analysis process is mainly inHeapAnalyzerServiceIn,Leakcanary2.0The use ofSharkTo resolvehprofFile.
  • 5.HeapAnalyzerTo obtainhprofAll of theKeyedWeakReferenceAnd getobjectId
  • 6.HeapAnalyzerTo calculateobjectIdtoGC RootIs the shortest strong reference link path to determine whether there is leakage, and then build the reference chain causing leakage.
  • 7. Store the analysis results in the database and display the leak notification.

Here is just a general introduction, the detailed process can be read below

2. LeakCanaryHow is it installed automatically?

LeakCanary is easy to use and can be automatically initialized just by adding dependencies. How does this work? Let’s take a look at the source code, in fact, mainly through ContentProvider implementation

internal sealed class AppWatcherInstaller : ContentProvider() {

  /** * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process. */
  internal class MainProcess : AppWatcherInstaller(a)/** * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`, * [LeakCanaryProcess] automatically sets up the LeakCanary code */
  internal class LeakCanaryProcess : AppWatcherInstaller(a)override fun onCreate(a): Boolean {
    valapplication = context!! .applicationContextas Application
    AppWatcher.manualInstall(application)
    return true}}Copy the code

When we start the App, the general startup sequence is: Application->attachBaseContext =====>ContentProvider->onCreate =====>Application->onCreate The ContentProvider is initialized before application.oncreate, which calls the initialization method of LeakCanary for manual initialization

2.1 Cross-process Initialization

Note that The AppWatcherInstaller has two subclasses,MainProcess and LeakCanaryProcess. MainProcess is used by default and will be used when the App process is initialized. When the leakcanary- Android-Process module needs to be initialized by a separate process, it will be deactivated in a new process

2.2 LeakCanary2.0Manual initialization method

LeakCanary takes time to detect a memory leak and interrupts the App operation, which is not a great experience when detection is not required, so although LeakCanary can be automatically initialized, we actually need to manually initialize it sometimes

LeakCanary automatic initialization can be turned off manually

 
      
 <resources>
      <bool name="leak_canary_watcher_auto_install">false</bool>
 </resources>
Copy the code

1. Then call AppWatcher. ManualInstall when you need to initialize 2. Config = LeakCanary. Config. copy(dumpHeap = false) 3. Begin the desktop icon: rewrite r. ool. Leak_canary_add_launcher_icon LeakCanary. Or call showLeakDisplayActivityLauncherIcon (false)

2.3 summary

LeakCanary is initialized with ContentProvier. ContentProvier is normally loaded before application.oncreate, and LeakCanary has called AppWatcher. ManualInstall in its onCreate() method to initialize it. This eliminates the initialization step, but can cause problems with StartUp time. Users have no control over the timing of initialization, which is why Google has launched StartUp. However, for LeakCanary this problem is not serious, as it is only relied on during the Debug phase

3.LeakCanaryHow do I detect memory leaks?

3.1 First let’s look at what is done during initialization.

When we initialize, we call AppWatcher. ManualInstall. Let’s look at this method

  @JvmOverloads
  fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {
    ....
    watchersToInstall.forEach {
      it.install()
    }
  }

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

As you can see, the initialization is installed some Watcher, namely, by default, we can only observe the Activity, fragments, RootView, Service whether these objects are leaking If need to see other objects, the need to manually add and processing

3.2 LeakCanaryHow do I trigger detection?

As mentioned above, some Watcher is installed during initialization, using ActivityWatcher as an example

class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

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

  override fun uninstall(a) {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}
Copy the code

As you can see, detection for memory leaks is triggered when Activity. OnDestory is triggered

3.3 LeakCanaryHow do I detect objects that might leak?

Can be seen from the above, the Activity will call after being closed to ObjectWatcher. ExpectWeaklyReachable

@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)
    }
  }

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

1. All incoming observations are stored in watchedObjects 2. A KeyedWeakReference weakreference is generated for each watchedObject and associated with a queue. When the object is reclaimed, the weakReference goes to queue 3. In the process of testing, we will call removeWeaklyReachableObjects for many times, has recycled object removed from the watchedObjects 4. If watchedObjects does not remove the object, proving that it was not reclaimed, moveToRetained is called

3.4 LeakCanaryA heap snapshot is generatedhproffile

MoveToRetained later, you can call to HeapDumpTrigger. CheckRetainedInstances checkRetainedInstances method () method is to determine the leakage of the last method. This verifies that the reference is really leaked, and if so, heap dump is initiated, the dump file is analyzed, and the chain of references is found

private fun checkRetainedObjects(a) {
    var retainedReferenceCount = objectWatcher.retainedObjectCount

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

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

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if(elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { onRetainInstanceListener.onEvent(DumpHappenedRecently) .return
    }

    dismissRetainedCountNotification()
    val visibility = if (applicationVisible) "visible" else "not visible"
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility")}private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ){... heapDumper.dumpHeap() .... lastDisplayedRetainedObjectCount =0
     lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
     objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
     HeapAnalyzerService.runAnalysis(
       context = application,
       heapDumpFile = heapDumpResult.file,
       heapDumpDurationMillis = heapDumpResult.durationMillis,
       heapDumpReason = reason
     )
 }
}
Copy the code

1. If the retainedObjectCount is greater than 0, do a GC to avoid additional Dump 2. By default, if retainedReferenceCount<5, no Dump is performed, saving resource 3. If the time between two dumps is less than 60 seconds, the system returns directly to avoid frequent Dump 4. Call heapDumper.dumpheap () for the actual Dump operation 5. after Dump, delete the already processed reference 6. Call HeapAnalyzerService. RunAnalysis analyze the results

3.5 LeakCanaryHow to analyzehproffile

Analysis of thehprofDocumentation works mainly inHeapAnalyzerServiceClass

aboutHprofThe parsing details of the file need to be involvedHprofBinary protocol, by reading the protocol documentation,hprofThe binary file structure is as follows:

The analysis process is as follows:



A brief description of the process:

1. Parse the file header to obtain the start position for parsing

2. Create the vm based on the header informationHprofThe file object

3. Create a memory index

Use 4.hprofObject and index buildingGraphobject

5. Find the objects that may be leaked andGCRootTo determine whether there is leakage (using the breadth-first algorithm inGraphFind the)

Leakcanary2.0 is one of the biggest changes to Leakcanary2.0 from the previous version. The main idea behind Leakcanary2.0 is to parse the contents of the hprof file into a graph data structure according to the binary protocol of the hprof file, and then breadth through the graph to find the shortest path. The path starts with the GCRoot object and ends with the leaked object

The implementation principle of Android Memory Leak Detection LeakCanary2.0 (Kotlin version) is described

3.6 Storage and notification of leakage results

The results of the storage and mainly done in DefaultOnHeapAnalyzedListener notice

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()
    ...

    if (InternalLeakCanary.formFactor == TV) {
      showToast(heapAnalysis)
      printIntentInfo()
    } else {
      showNotification(screenToShow, contentTitle)
    }
  }
Copy the code

Two main things are done: 1. Store leak analysis results in the database; 2. Display notifications to remind users to look for memory leaks

4. WhyLeakCanaryCannot be used online?

After understanding the work that has been done to LeakCanary determination objects, it is not difficult to see that applying LeakCanary directly online has some of the following problems: 1. After each memory leak, a.hprof file is generated, parsed, and the result is written to.hprof.result. Increase the burden of mobile phones, causing problems such as mobile phone congestion. 2. Multiple calls to GC may affect online performance. 3. The same leakage problem will cause repeated generation of.hprof files, repeated analysis and writing to disk. 4.. Hprof files are large and information retrieval is a problem.

Knowing these problems, we can try to put forward some solutions: 1. We can set a memory threshold M according to the mobile phone information. When the used memory is less than M, if there is a memory leak at this time, only the information of the leaked object is stored in the memory, and the.hprof file is not generated. If the number of used links is greater than M, a. Hprof file is generated. 2. 3. Instead of retrieving the. Hprof file directly, you can retrieve the analysis result. You can try to store leaked objects in the database and detect the same leak only once for each user to reduce the impact on users

The above ideas have not been tested, just for readers’ reference

conclusion

When we introduce LeakCanary, it will automatically install and start analyzing memory leaks and alarm by following steps 1. Automatic installation 2. detection of possible leaking objects 3. heap snapshot, generate hprof file 4. Analyze the Hprof file. 5. Classify and notify leaks

This article looks at the main process of LeakCanary and some of the questions raised at the beginning of this article

The resources

LeakCanary2.0 (Kotlin version) is the latest LeakCanary detection solution for Android. Completely based on Kotlin refactoring and upgrading! Why use LeakCanary to detect memory leaks?