LeakCanaryLeakage target prediction

LeakCanary is presumably also an old friend, but how does it manage to do a memory leak analysis on our App? And that’s what we’re going to be looking at today.

So the first question we have to think about is thisAppThere’s already a leak, so how do we know he’s leaking?

🤔🤔🤔, we should know that there are two algorithms in the JVM that determine whether an instance needs to be recycled:

  1. Reference counting method
  2. Accessibility analysis

Reference counting method

forReference counting methodThere is a deadly circular reference problem, which we will examine with diagrams.Class A and class B as an instance, then the count of class A and class B0 - > 1But we’ll notice there’s another one calledInstanceEach object points to an instance of the other, i.eClass A.stance = class B,Class B.instance = class A, then the count of class A and class B is1 - > 2Even if we didClass A = null,Class B = nullFor such operations, class A and class B count only from2 - > 1, does not go to 0, which causes the memory leak problem.

Accessibility analysis

Compared with reference counting method, the reachability analysis method has a new concept called GC Root, and these GC Roots are the starting point of our reachability analysis method. This concept has been mentioned in The in-depth Understanding of Java Virtual Machine by zhou Zhiming. It can be divided into several categories:

  • An object referenced by a class static attribute in a method area, such as a Java class reference type static variable.
  • An object referenced by a constant in a method area, such as a reference in a string constant pool.
  • Objects referenced by JNI in the local method stack.
  • Objects referenced in the Java virtual machine stack, for exampleAndroidThe main entry class ofActivityThread.
  • All objects held by the synchronization lock.
  • .

We also used the above circular reference case as an analysis to see if the reachability analysis still has this memory leak problem.At this timeClass B = nullWhat happens then?

sinceClass B = nullSo ourInstanceIt should also be equal tonullAt this point, there is one less reference line, as we said above, from the starting point of the reachability analysisRootsWhen the accessibility analysis is performed and it is found that there is no more access for class B, that class B will be marked with a clearing flag and waitGCThe arrival of the.

Knowing our two leak target detection schemes, let’s see if we can achieve LeakCanary through these two schemes. If not, how does he do it?

LeakCanaryMethod of use

I have read a lot of blogs about how to use Version 2.x, but I found a problem that none of them have any function calls like LeakCanary. Install (this). Later I learned that the architecture has been reconfigured and silent loading has been implemented, so we do not need to call it manually.

Here’s the latest version I used:

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

To give a Demo that can run out of memory leaks, that is, a singleton mode, what you need to do is to jump to Activity2 in Activity1, and Activity2 instantiates the singleton so that when you return, you can see the LeakCanary problem.

public class Singleton {
    private Context context;

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

    public static class Holder{
        private static Singleton Instance;

        public static Singleton getInstance(Context context){
            if(Instance == null) {
                synchronized (Singleton.class){
                    if (Instance == null) Instance = newSingleton(context); }}returnInstance; }}}Copy the code

When a memory leak occurs, you need to check in the notification barClick on it and wait for it to pull and then we can start from something calledLeaksApp for viewing.

You can see it’s already decidedinstanceThis instance has leaked, what is the cause?

Because Activity2 has been destroyed, but the context is still held, Activity2 can’t be cleaned up and we won’t be using it anymore, and there is a memory leak. If you put the context changes into context. GetApplicationContext () also can solve this problem, this time because of the context in the singleton cycle has been modified into and Application is consistent, The Activity2 cleanup does not cause a leak because the context is no longer the same as the one stored in the singleton.

LeakCanaryHow is the task accomplished?

From the perspective of Version 1.x, the observed load would need to be called by code such as LeakCanary.install(), so let’s see how Version 2.x reconstructs it.

Using a global search, we were able to locate a class called AppWatcherInstaller, but something strange happened. It inherited the four major components of ContentProvider.

/** * Content providers are loaded before the application class is created. [AppWatcherInstaller] is * used to install [Leakcanary.AppWatcher] on Application start. * Content providers are created earlier than application to load AppWatcher */
Copy the code

AppWatcher is a big Buddha that can load AppWatcher. 🤔 🤔 🤔 🤔

AppWatcher— Originator of memory leak checks

The necessary code is captured as follows:

object AppWatcher {

    data class Config(
    // AppWatcher keeps an eye on object enablement variables
    val enabled: Boolean = InternalAppWatcher.isDebuggableBuild,
    // AppWatcher is always concerned with destroying the enable variable of the Activity instance
    val watchActivities: Boolean = true.// AppWatcher keeps an eye on the enable variable for destroying Fragment instances
    val watchFragments: Boolean = true.// AppWatcher keeps an eye on destroying the Enable variable of the Fragment View instance
    val watchFragmentViews: Boolean = true.// AppWatcher keeps an eye on destroying the enable variable of the ViewModel instance
    val watchViewModels: Boolean = true.// Set the reporting time for the resident object
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)) {var config;
  // Used to monitor objects that are still alive
  val objectWatcher
    get() = InternalAppWatcher.objectWatcher

  val isInstalled
    get() = InternalAppWatcher.isInstalled
  }
}
Copy the code

Otherwise, there’s only one object that’s doing the monitoring, and that’s the ObjectWatcher. So we assume that there must be some use of ObjectWatcher in the code, so we can now go back to the AppWatcherInstaller and see that it overwrites a method called onCreate(), And did such a thing InternalAppWatcher. Install (application), then we went in to see.

fun install(application: Application) {
    SharkLog.logger = DefaultCanaryLog()
    // Check whether the current thread is in the main thread
    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 code says that it can load and destroy the observer of the Activity and Fragment, so we will select the source of the Activity to view.

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

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      // Override the onActivityDestroyed() method in the lifecycle
      // Indicates that monitoring is invoked only at destruction time
      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

The most important thing about this class is that I have commented it out. You can see that the ObjectWatcher class is being used a second time, with a method called watch(), which is definitely worth checking out 🥴🥴🥴🥴.

ObjectWatcher— The executor of the test

class ObjectWatcher constructor(
  private val clock: Clock,
  // Check through the pool to see if there are any remaining instances
  private val checkRetainedExecutor: Executor,
  private val isEnabled: () -> Boolean = { true{})// There is no listening for the reclaimed instance
  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
  // The weak reference is associated with a specific String, the UUID
  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
  // a reference queue
  private val queue = ReferenceQueue<Any>()
  
  // The method called to do the monitoring mentioned above
  @Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {
    if(! isEnabled()) {return
    }
    // Delete weakly reachable objects
    removeWeaklyReachableObjects()
    // randomly generate ID as Key to ensure uniqueness
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    // Build weak references with activities, reference queues, and so on
    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
    // A nice design to add thread pool for asynchronous processing
    checkRetainedExecutor.execute {
      moveToRetained(key) / / 1 - >}}/ / 1 - >
  @Synchronized private fun moveToRetained(key: String) {
    // Delete weakly reachable objects a second time
    // It is used for secondary verification to ensure the correctness of data
    removeWeaklyReachableObjects()
    // At this point our data should be validated to see if it is null
    val retainedRef = watchedObjects[key]
    // If the data is not empty, a leak has occurred
    if(retainedRef ! =null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis() 
      onObjectRetainedListeners.forEach { it.onObjectRetained() } // 2!!}}// The data has been loaded into the reference queue
  // Indicates that the original strong reference instance has been empty and detected
  // And this sequence of operations will be completed before the tag and gc arrive
  private fun removeWeaklyReachableObjects(a) {
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if(ref ! =null) {
        watchedObjects.remove(ref.key)
      }
    } while(ref ! =null)}}Copy the code

We already know from the above series of process analyses whether the current instance is leaking, but there is a question, how is it reported?

Who is manipulating all this?

For LeakCanary, I have analyzed comment 2 in the code above and know that he must have done something, but what exactly did he do, send notification, generate file?? 🤕 🤕 🤕 🤕

We know from the source code of Version 1.x that there is such a class LeakCanary, do we still exist in Version 2.x? After all, we haven’t mentioned such a class before. Well, Search for it.

A search revealed that it existed, and the first line of the file said this

This is a class built on top of AppWatcher, and AppWatcher notifies it to complete the stack analysis. How does it do this?

How does downstream inform upstream

Looking at ObjectWatcher, we can see such a variable and its companion methods

private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
// Add an observer
@Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.add(listener)
  }
// Delete the observer
@Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.remove(listener)
  }
Copy the code

Add and remove methods, which is similar to what design pattern?

That’s right, observer mode! Since LeakCanary is upstream, we’ll treat him as an observer and AppWacther is our theme.

One such problem can be found by calling InternalLeakCanary, which completes an add operation in the Invoke method

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
Copy the code

Heap Dump Trigger— Report file generation trigger

As has been said before returning to our code onObjectRetainedListeners. ForEach {it. OnObjectRetained ()}, by deep call we can found that he really deep calls is the following code

override fun onObjectRetained(a) {
    if (this::heapDumpTrigger.isInitialized) { 
      heapDumpTrigger.onObjectRetained() / / 1 - >}}/ / 1 - >
private fun scheduleRetainedObjectCheck(
    reason: String,
    rescheduling: Boolean,
    delayMillis: Long = 0L
  ) {
    // Some print...
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects(reason) / / 2 - >
    }, delayMillis)
  }
Copy the code

I marked the position of comment 2, and he wanted to check the object again.

private fun checkRetainedObjects(reason: String) {
    var retainedReferenceCount = objectWatcher.retainedObjectCount
    // Another GC operation is performed
    // This is a guarantee operation to ensure that our data does not exist because GC is not coming and is forcibly evaluated
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return / / 1 - >
    //...
    dumpHeap(retainedReferenceCount, retry = true) // This is the last location of the output leak file to enter
  }
  
/ / 1 - >
private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int
  ): Boolean {
    valcountChanged = lastDisplayedRetainedObjectCount ! = retainedKeysCount lastDisplayedRetainedObjectCount = retainedKeysCount// If it is found that all instances have been cleared after GC, return directly
    if (retainedKeysCount == 0) {
      SharkLog.d { "Check for retained object found no objects remaining" }
      return true
    }
    // When the number of leaks is less than 5, a notification bar will be sent to remind you
    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        if (countChanged) {
          onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
        }
        showRetainedCountNotification() // Issue a notification
        scheduleRetainedObjectCheck() // Check again
        return true}}return false
  }
Copy the code

Of course, LeakCanary also has its own solution to force file generation. In fact, we often use this solution, that is, his leak data has not been full to the set value, but we already want to print the report, that is, directly click the notification bar to print the analysis report.

fun dumpHeap(a) = InternalLeakCanary.onDumpHeapReceived(forceDump = true) / / 1 - >

/ / 1 - >
fun onDumpHeapReceived(forceDump: Boolean) {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onDumpHeapReceived(forceDump) / / 2 - >}}fun onDumpHeapReceived(forceDump: Boolean) {
    backgroundHandler.post {
      dismissNoRetainedOnTapNotification()
      // Perform the same operation as before
      gcTrigger.runGc()
      val retainedReferenceCount = objectWatcher.retainedObjectCount
      // Do not force printing, and the number of leaks is 0
      if(! forceDump && retainedReferenceCount ==0) {
        / /...
        return@post
      }
        
      SharkLog.d { "Dumping the heap because user requested it" }
      dumpHeap(retainedReferenceCount, retry = false) // Complete the data printing of the analysis report}}Copy the code

Dump Heap— Report file generator

When it comes to report file generation, it’s not really our main focus anymore, but it’s worth mentioning.

It exists through a service to complete the printing of our data analysis report. There are a lot of interesting things inside, transparent permission requests and so on. But this timehprofI do not know whether or not to use the third party library, but to my feeling should be made by their own.

conclusion

We said a lot about the content of the source code, to comb the framework, in fact, nothing more than four content.

Pay attention to the point

  1. Observer model
  2. Object leaks are observed as weak references + reference queues