LeakCanary source code analysis and ContentProvider optimization plan

1. Use

To use LeakCancary 2.0, you only need to configure the following code to use it. I don’t know where it is higher than LeakCancary 1.0~

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

2. Source code analysis

After reading the source code, you can see the Androidmanifest file of the leakcancary-leaksentry module, you can see the following:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary.leaksentry"
    >

  <application>
    <provider
        android:name="leakcanary.internal.LeakSentryInstaller"
        android:authorities="${applicationId}.leak-sentry-installer"
        android:exported="false"/>
  </application>
</manifest>

Then we can see what the LeakSentryInstaller class does

internal class LeakSentryInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    CanaryLog.logger = DefaultCanaryLog()
    val application = context!!.applicationContext as Application
    //利用系统自动调用ContentProvider的onCreate来进行安装
    InternalLeakSentry.install(application)
    return true
  }
  ...

As for why the system calls the onCreate method of ContentProvider, we can look at the source code, which can be seen in the handleMessage in H in ActivityThread

public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
    switch (msg.what) {
        case BIND_APPLICATION:
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
            AppBindData data = (AppBindData)msg.obj;
            //关键方法
            handleBindApplication(data);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            break;

Then you can see in handleBindApplication

// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
    if (!ArrayUtils.isEmpty(data.providers)) {
        //contentprovider初始化,里面会调用onCreate方法
        installContentProviders(app, data.providers);
    }
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
    mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
    throw new RuntimeException(
        "Exception thrown in onCreate() of "
        + data.instrumentationName + ": " + e.toString(), e);
}
try {
    //app的onCreate方法调用
    mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {

The specific onCreate code logic of calling contentprovider is as follows

@UnsupportedAppUsage
private void installContentProviders(
        Context context, List<ProviderInfo> providers) {
    final ArrayList<ContentProviderHolder> results = new ArrayList<>();

    for (ProviderInfo cpi : providers) {
        ···
        //installProvider方法
        ContentProviderHolder cph = installProvider(context, null, cpi,
                false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
        if (cph != null) {
            cph.noReleaseNeeded = true;
            results.add(cph);
        }
    }
  //installProvider方法,然后一步步跟进
  //1
  //XXX Need to create the correct context for this provider.
  localProvider.attachInfo(c, info);
  //2
  public void attachInfo(Context context, ProviderInfo info) {
        attachInfo(context, info, false);
   }
  //3
  private void attachInfo(Context context, ProviderInfo info, boolean testing) {
        mNoPerms = testing;
        mCallingPackage = new ThreadLocal<>();
        if (mContext == null) {
            ···
            ContentProvider.this.onCreate();
        }
    }

Through the above analysis, we can know that after we introduce the dependency, the AndroidMainfest.xml file in the dependency package will be actively merged into the main AndroidManifest.xml file, and then the ContentProvider will be automatically created during the program startup process, and then InternalLeakSentry.install (application), then perform a series of monitoring and dump operations.

2.1 InternalLeakSentry.install(application)

Let’s analyze what is done in InternalLeakSentry.install(application), you can see

fun install(application: Application) {
    CanaryLog.d("Installing LeakSentry")
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    InternalLeakSentry.application = application

    val configProvider = { LeakSentry.config }
    // 1.监听 Activity.onDestroy()
    ActivityDestroyWatcher.install(
        application, refWatcher, configProvider
    )
    // 2.监听 Fragment.onDestroy()
    FragmentDestroyWatcher.install(
        application, refWatcher, configProvider
    )
    // 3.监听完成后进行一些初始化工作
    listener.onLeakSentryInstalled(application)
  }

From the naming, you can see that the watch is performed when the Activity and Fragment are destroyed

1. ActivityDestroyWatcher

internal class ActivityDestroyWatcher private constructor(
  private val refWatcher: RefWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
    override fun onActivityDestroyed(activity: Activity) {
      if (configProvider().watchActivities) {
        // 监听到 onDestroy() 之后,通过 refWatcher 监测 Activity
        refWatcher.watch(activity)
      }
    }
  }

  companion object {
    fun install(
      application: Application,
      refWatcher: RefWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(refWatcher, configProvider)
      application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

2. FragmentDestroyWatcher

internal interface FragmentDestroyWatcher {

  fun watchFragments(activity: Activity)

  companion object {

    private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"

    fun install(
      application: Application,
      refWatcher: RefWatcher,
      configProvider: () -> LeakSentry.Config
    ) {
      val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()

      //大于等于android O  
      if (SDK_INT >= O) {
        fragmentDestroyWatchers.add(
            AndroidOFragmentDestroyWatcher(refWatcher, configProvider)
        )
      }

      if (classAvailable(
              SUPPORT_FRAGMENT_CLASS_NAME
          )
      ) {
        // androidx 使用 SupportFragmentDestroyWatcher
        fragmentDestroyWatchers.add(
            SupportFragmentDestroyWatcher(refWatcher, configProvider)
        )
      }

      if (fragmentDestroyWatchers.size == 0) {
        return
      }

      application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
        override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
        ) {
          for (watcher in fragmentDestroyWatchers) {
            watcher.watchFragments(activity)
          }
        }
      })
    }

    private fun classAvailable(className: String): Boolean {
      return try {
        Class.forName(className)
        true
      } catch (e: ClassNotFoundException) {
        false
      }
    }
  }
}

Android O and later, androidx has the monitoring function of Fragment life cycle. Why not listen to the ones before Android O? ? ? (To be resolved) Fragment memory leak monitoring was not supported before version 1.5.4, and it was added in later versions.

3. listener.onLeakSentryInstalled(application)

The final implementation class of the listener is the InternalLeakCanary class in leakcanary-android-core

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

    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)
    //用于发现可能的内存泄漏之后手动调用 GC 确认是否真的为内存泄露
    val gcTrigger = GcTrigger.Default

    val configProvider = { LeakCanary.config }

    val handlerThread = HandlerThread(HeapDumpTrigger.LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)
    //用于确认内存泄漏之后进行 heap dump 工作。
    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
    )
    application.registerVisibilityListener { applicationVisible ->
      this.applicationVisible = applicationVisible
      heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
    }
    addDynamicShortcut(application)
  }

Here is a knowledge point about GC recycling, we can see how excellent third-party frameworks are written

interface GcTrigger {
  fun runGc()
  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()
    }
    private fun enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100)
      } catch (e: InterruptedException) {
        throw AssertionError()
      }
    }
  }
}

As you can see, it uses Runtime.getRuntime().gc() instead of System.gc(), enter the System.gc source code to have a look

public static void gc() {
    boolean shouldRunGC;
    synchronized (LOCK) {
        shouldRunGC = justRanFinalization;
        if (shouldRunGC) {
            justRanFinalization = false;
        } else {
            runGC = true;
        }
    }
    if (shouldRunGC) {
        Runtime.getRuntime().gc();
    }
}

You can see that the source code of System.gc or the final implementation is Runtime.getRuntime().gc(); but a series of judgment conditions are required. We manually call System.runFinalization() to make the justRanFinalizationw in the gc method true, thereby ensuring Runtime.getRuntime().gc() will be executed.

3. How to judge that the object may leak: the meaning and function of ReferenceQueue

After the Activity/Fragment is destroyed, a series of object recycling will be carried out. We associate these objects with the reference queue respectively. When an object is recycled, **(Once the weak reference becomes weakly reachable (reachability algorithm) Analysis), the reference will be added to the reference queue, and then reclaimed) **The reference of our object will be added to the reference queue. Perform a series of operations according to this principle, and finally determine whether the memory leaks.

3.1 Reference queue

Usually we translate its ReferenceQueue into a reference queue, in other words the queue that stores the reference, and the reference object is stored. Its function is that when the object referenced by the Reference object is recycled by the GC, the Reference object will be added to the end of the reference queue (ReferenceQueue).

Commonly used methods of ReferenceQueue :

public Reference poll() : Take out an element from the queue, and return null if the queue is empty;

public Reference remove() : Remove an element from the queue, if not, block until there is an element that can be dequeued;

public Reference remove(long timeout): remove an element from the queue, if not, block until there is a pair of elements or block for more than timeout milliseconds;

1. Strong reference
2, soft reference
3, weak reference
4. Phantom reference Phantom reference is
equivalent to no reference, which means that it may be recycled by GC at any time. The purpose of setting a virtual reference is to be associated by a virtual reference When the object is recycled by the garbage collector, it can receive a system notification. (It is used to track the activities of objects being recycled by GC) The difference between virtual references and weak references is: virtual references must be used in conjunction with the reference queue (ReferenceQueue), and their activities during GC recycling are as follows:

ReferenceQueue queue=new ReferenceQueue();

PhantomReference pr=new PhantomReference(object,queue);

That is, when the GC reclaims an object, if it finds that the object has a phantom reference, it will first add the phantom reference of the object to the reference queue associated with it before the collection. The program can know whether the referenced object has been reclaimed by the GC by judging whether a virtual reference has been added to the reference queue.

3.2 GC Root Object

3.3 Whether the memory leaks

After knowing the principle of the reference queue, firstly describe how to judge whether it is leaking, first create three queues

/**
   * References passed to [watch] that haven't made it to [retainedReferences] yet.
   * watch() 方法传进来的引用,尚未判定为泄露
   */
  private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
  /**
   * References passed to [watch] that we have determined to be retained longer than they should
   * have been.
   * watch() 方法传进来的引用,已经被判定为泄露
   */
  private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
  private val queue = ReferenceQueue<Any>() // 引用队列,配合弱引用使用
    
//KeyedWeakReference,对象和引用队列进行弱引用关联,所以这个对象一定会被回收    
class KeyedWeakReference(
  referent: Any,
  val key: String,
  val name: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
    referent, referenceQueue
) {
  @Volatile
  var retainedUptimeMillis = -1L

  companion object {
    @Volatile
    @JvmStatic var heapDumpUptimeMillis = 0L
  }

}    

If an obj object is associated with a weak reference to the queue queue, during garbage collection, it is found that the object has a weak reference, and the reference will be added to the reference queue. If we get a reference in the queue, it means the object be recovered, if not get, it indicates that the object is still strong / soft reference is not released, then that object has not been recovered, a memory leak, and then dump memory snapshots, using third-party libraries referenced chain analysis .

It is emphasized here that an object may be held by multiple references, such as strong references, soft references, and weak references. As long as the object has strong references/soft references, any reference queue associated with this object will not be able to get references, references A queue is equivalent to a notification. Multiple reference queues are associated with an object. When the object is recycled, multiple queues will be notified .

3.4 watch()

@Synchronized fun watch(
  watchedReference: Any,
  referenceName: String
) {
  if (!isEnabled()) {
    return
  }
  //移除队列中将要被 GC 的引用
  removeWeaklyReachableReferences()
  val key = UUID.randomUUID()
      .toString()
  val watchUptimeMillis = clock.uptimeMillis()
  val reference = // 构建当前引用的弱引用对象,并关联引用队列 queue
    KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
  if (referenceName != "") {
    CanaryLog.d(
        "Watching instance of %s named %s with key %s", reference.className,
        referenceName, key
    )
  } else {
    CanaryLog.d(
        "Watching instance of %s with key %s", reference.className, key
    )
  }

  watchedReferences[key] = reference
  checkRetainedExecutor.execute {
    //如果引用未被移除,则可能存在内存泄漏
    moveToRetained(key)
  }
}

removeWeaklyReachableReferences()

private fun removeWeaklyReachableReferences() {
    // 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.
    // 弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference? // 队列 queue 中的对象都是会被 GC 的
      if (ref != null) {
        val removedRef = watchedReferences.remove(ref.key)
        if (removedRef == null) {
          retainedReferences.remove(ref.key)
        }
        // 移除 watchedReferences 队列中的会被 GC 的 ref 对象,剩下的就是可能泄露的对象
      }
    } while (ref != null)
  }

moveToRetained()

@Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableReferences() // 再次调用,防止遗漏
    val retainedRef = watchedReferences.remove(key)
    if (retainedRef != null) {
      retainedReferences[key] = retainedRef
      onReferenceRetained()
    }
  }

Finally, it will call back to the onReferenceRetained() method of InternalLeakCanary

override fun onReferenceRetained() {
  if (this::heapDumpTrigger.isInitialized) {
    heapDumpTrigger.onReferenceRetained()
  }
}

//1.HeapDumpTrigger 的 onReferenceRetained()
fun onReferenceRetained() {
  scheduleRetainedInstanceCheck("found new instance retained")
}

//2.scheduleRetainedInstanceCheck
private fun scheduleRetainedInstanceCheck(reason: String) {
  backgroundHandler.post {
    checkRetainedInstances(reason)
  }
}
  
//3.checkRetainedInstances
private fun checkRetainedInstances(reason: String) {
  CanaryLog.d("Checking retained instances because %s", reason)
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if (!config.dumpHeap) {
      return
    }

  var retainedKeys = refWatcher.retainedKeys
    //当前泄露实例个数小于 5 个,不进行 heap dump
    if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

      if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
        showRetainedCountWithDebuggerAttached(retainedKeys.size)
          scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
          CanaryLog.d(
          "Not checking for leaks while the debugger is attached, will retry in %d ms",
          WAIT_FOR_DEBUG_MILLIS
        )
          return
      }
    // 可能存在被观察的引用将要变得弱可达,但是还未入队引用队列。
    // 这时候应该主动调用一次 GC,可能可以避免一次 heap dump
  gcTrigger.runGc()

    retainedKeys = refWatcher.retainedKeys

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

      HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)

      CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
      HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
      dismissNotification()
      val heapDumpFile = heapDumper.dumpHeap()
      if (heapDumpFile == null) {
        CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
          scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
          showRetainedCountWithHeapDumpFailed(retainedKeys.size)
          return
      }

  refWatcher.removeRetainedKeys(retainedKeys)

    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
}

For some details, you can look at the code comments. If checkRetainedCount meets the number, a head dump must be initiated. The specific logic is in AndroidHeapDumper.dumpHeap():

 override fun dumpHeap(): File? {
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
    ···
    return try {
     //Dump出文件
      Debug.dumpHprofData(heapDumpFile.absolutePath)
      heapDumpFile
    } catch (e: Exception) {
      CanaryLog.d(e, "Could not dump heap")
      // Abort heap dump
      null
    } finally {
      cancelToast(toast)
      notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
    }
  }

Finally, start a foreground service  HeapAnalyzerService to analyze the heap dump file. In the old version, Square’s own haha ​​library was used for parsing. This library has been deprecated. Square has completely rewritten the parsing library. The main logic is in the moudle  leakcanary-analyzer . I haven't read this part, so I won't analyze it here. For the new parser, the official website introduces this:

Uses 90% less memory and 6 times faster than the prior heap parser.

Reduced 90% of the memory footprint, and 6 times faster than the original. Later, I have time to analyze this analysis library separately.

The following process will not be repeated, find the shortest GC Roots reference path through the parsing library, and then show it to the user.

4. Manual write memory leak detection

The following is an example of memory leak detection written with reference to Zero's Demo, the idea is the same as LeakCanary


fun main() {

    class MyKeyedWeakReference(
            referent: Any,
            val key: String,
            val name: String,
            referenceQueue: ReferenceQueue<Any>
    ) : WeakReference<Any>(
            referent, referenceQueue
    ) {
        val className: String = referent.javaClass.name
        override fun toString(): String {
            return "{key=$key,className=$className}"
        }
    }
    //需要观察的对象
    val watchedReferences = mutableMapOf<String,MyKeyedWeakReference>()
    //如果最后retainedReferences还存在引用,说明泄漏了
    val retainedReferences = mutableMapOf<String,MyKeyedWeakReference>()
    //当与之关联的弱引用中的实例被回收,则会加入到queue
    val gcQueue = ReferenceQueue<Any>()

    fun sleep(mills: Long){
        try {
            Thread.sleep(mills)
        }catch (e: Exception){
            e.printStackTrace()
        }
    }

    fun gc(){
        println("执行gc...")
        Runtime.getRuntime().gc()
        sleep(100)
        System.runFinalization()
    }

    fun removeWeaklyReachableReferences(){
        println("removeWeaklyReachableReferences")
        var ref: MyKeyedWeakReference?
        do {
            ref = gcQueue.poll() as MyKeyedWeakReference? //队列queue中的对象都是会被GC的
            println("ref=$ref,如果ref为null,说明对象还有强引用")
            if (ref != null){ //说明被释放了
                println("ref=$ref, 对象被释放了,key=${ref.key}")
                val removedRef = watchedReferences.remove(ref.key)
                println("removedRef=$removedRef, 如果removedRef为null,说明已经不在watchedReferences了,key=${ref.key}")
                if (removedRef == null){
                    //不在watchedReferences则说明在retainedReferences
                    retainedReferences.remove(ref.key)
                }
            }
        }while (ref != null)
    }

    @Synchronized
    fun moveToRetained(key: String){
        println("5.moveToRetained,key=$key")
        removeWeaklyReachableReferences()
        val retainedRef = watchedReferences.remove(key)
        println("retainedRef =$retainedRef 如果还有值说明没有被释放")
        if (retainedRef != null){ //添加到retainedReferences
            retainedReferences[key] = retainedRef
        }

    }

    fun watch(
            obj: Any,
            referenceName: String = ""){
        println("2.watch...")
        removeWeaklyReachableReferences()
        val key = UUID.randomUUID().toString()
        println("3.key=$key")
        val reference = MyKeyedWeakReference(obj,key,referenceName,gcQueue)
        println("4.reference=$reference")
        //加入观察列表
        watchedReferences[key] = reference
        //过段时间查看是否释放
        thread(start = true){
            sleep(5000)
            moveToRetained(key)
        }

    }

    var obj : Any? = Object()
    println("1.创建一个对象obj=$obj")
    watch(obj!!,"")
    sleep(2000)
    obj = null
    if (obj == null){
        println("obj=$obj 释放了")
    }
    gc()
    sleep(5000)
    println("watchedReferences=$watchedReferences")
    println("retainedReferences=$retainedReferences")
    println("执行完毕")

5. Optimization of ContentProvider

5.1 Content initialization sequence

Initialization through ContentProvider can indeed bring convenience to users, but it will affect the startup speed. If there are multiple ContentProviders, how to control the order of these ContentProvider initializations, please refer to the following article https://sivanliu.github.io /2017/12/16/provider%E5%88%9D%E5%A7%8B%E5%8C%96/, if some third-party values ​​only provide the initialization method of ContentProvider, we don’t want to affect the startup time of our APP, How to deal with it?

5.2

If some third-party libraries only provide the initialization method of ContentProvider and we don't want to affect the startup time of our APP, how should we deal with it?

We can use the AOP method for instrumentation, modify the onCreate method of ContentProvider through Gradle+Transform+ASM, return in advance, and then manually call the initialization code . If these initialization codes are private or only restricted to the package, you can also pass ASM modifies the access permissions, and then initializes where we want to initialize. This may involve a sequential problem. You need to modify it first and then initialize it somewhere. Here is just an idea.

If a library takes a long time to initialize, and it is initialized in ContentProvider, the initialization code in ContentProvider is stinky and long, and no other initialization method is provided. Why do you want such a garbage library!

6. Summary

  1. Use ContentProvider to initialize automatically, no need for users to initialize manually

  2. GC recycling, reference queue

  3. Support fragment after 1.5.4, support androidx

  4. Heap dump will be initiated when there are 5 leaked references

  5. The new heap parser reduces memory usage by 90% and speeds up 6 times

  6. The pros and cons of ContentProvider, and optimization solutions

Reference article

https://sivanliu.github.io/2017/12/16/provider%E5%88%9D%E5%A7%8B%E5%8C%96/

https://juejin.im/post/5d1225546fb9a07ecd3d6b71

Guess you like

Origin blog.csdn.net/EnjoyEDU/article/details/108682301