This article is based on LeakCanary – Android :2.5

I’ve created a body of Android knowledge that has been collated on GitHub. To create a series of senior junior high school engineers can understand the quality of the article, welcome to star~

Mind mapping

1. The background

In Android development, memory leaks happen all the time, either by yourself or by three libraries. The dynamically allocated heap memory in the program is not released or cannot be released due to some special reasons, resulting in a waste of system memory, resulting in program running speed slowing down and even program crash and other serious consequences. Originally Android memory is tight, but also memory leaks, the consequences are unimaginable. Therefore, we should try our best to avoid memory leaks. On the one hand, we need to learn which common scenarios will cause memory leaks. On the other hand, we introduce LeakCanary to help us automatically detect where there are memory leaks.

LeakCanary is an open source library from Square (yes, this company again,OkHttp and Retrofit are both open source from this company), through which we can detect memory leaks during App running. It also analyzes the reference chain of object memory leaks to the developers, and we go to fix this memory leak Dew very aspect.

LeakCanary literally translates to LeakCanary. There is a short story about this name. Canaries, beautiful birds. In the 17th century, British miners discovered that canaries were sensitive to gas. The smallest amount of gas in the air will stop the canaries singing; When the gas level exceeds a certain limit, the canaries are already dead from the poison, although the dull human is unaware of it. With relatively poor mining equipment, workers carried a canary with them each time they went down the well as a “gas indicator” to evacuate in case of a dangerous situation. Similarly,LeakCanary is sensitive enough to help us detect memory leaks and avoid OOM risks.

2. The initialization

When importing LeakCanary, just add the following configuration to built. gradle:

// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 2.5'
Copy the code

That’s it, there is no code change needed! We don’t need to change any code, and just like that,LeakCanary has been introduced. So I have a question? We usually introduce a library that is initialized in the Application’s onCreate, it doesn’t have to be initialized in code, how does it work?

The only way I can think of to do this is if it defines a ContentProvider internally and initializes it inside the ContentProvider.

Let’s verify: After importing LeakCanary, run the project, then check the AndroidManifest file in the DEBUG APK and search for provider definition. Sure enough, I found it:

<provider
    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
    android:enabled="@ref/0x7f040007"
    android:exported="false"
    android:authorities="com.xfhy.allinone.leakcanary-installer" />
<! -- Here @ref/ 0x7F040007 corresponds to @bool/ Leak_canary_watcher_auto_install -->
Copy the code
class AppWatcherInstaller : ContentProvider() {
    override fun onCreate(a): Boolean {
        valapplication = context!! .applicationContextas Application
        AppWatcher.manualInstall(application)
        return true}}Copy the code

The initialization is done inside the ContentProvider. The ContentProvider is automatically initialized when the App starts, which is automatically initialized by calling appwatcher.manualInstall (). At first, I thought it was nice and elegant, but then I found out that many three-party libraries did it. A ContentProvider per library is initialized, a bit redundant. Jetpack solved this problem with App Startup, which encapsulates this principle.

It is important to note that the ContentProvider’s onCreate is executed earlier than the Application’s onCreate. If you want to initialize at another time to optimize the startup time, that’s fine. Just rewrite @bool/ Leak_canary_watcher_auto_install in the app to false. Then manually call appwatcher.manualInstall (Application) where appropriate. However, LeakCanary is originally used for debug, so it doesn’t feel necessary to optimize the startup time.

3. Timing of leaks

LeakCanary automatically detects leaks from:

  • destroyed Activity instances
  • destroyed Fragment instances
  • destroyed fragment View instances
  • cleared ViewModel instances

As you can see, it’s all about detecting things that are vulnerable to leaks in Android development. So how does it detect, let’s analyze that

3.1 the Activity

Through application # registerActivityLifecycleCallbacks () to register the Activity life cycle listening, then in onActivityDestroyed () in objectWatcher. Watch (activi ty,….) Check whether the object is leaked. The detection of whether the object leaks is analyzed separately later.

3.2 Fragment and Fragment View

Also, detection of this two is also a need to monitor cycle, but this time is to monitor the fragments of life cycle, using fragmentManager. RegisterFragmentLifecycleCallbacks can achieve. The Fragment checks for leaks in onFragmentDestroy() and the FragmentView checks for leaks in onFragmentViewDestroyed().

However, the process of getting this fragmentManager was a bit tortuous.

  • Android O above, through activity# getFragmentManager (). (AndroidOFragmentDestroyWatcher)
  • AndroidX, through activity# getSupportFragmentManager (). (AndroidXFragmentDestroyWatcher)
  • The support package, through activity# getSupportFragmentManager (). (AndroidSupportFragmentDestroyWatcher)

As you can see, the way to fetch the FragmentManager is different in different scenarios. The implementation of the FragmentManager, the registration of the Fragment lifecycle, and the detection of leakage in onFragmentDestroyed and onFragmentViewDestroyed are implemented differently in different environments. So encapsulating them into different policies (corresponding to the above three policies) is the application of the policy pattern.

Since we need an instance of the FragmentManager, we also need to listen to the Activity lifecycle. We need to get the instance of the Activity in onActivityCreated() to get the FragmentManager to listen to the Fragment Life cycle.

//AndroidOFragmentDestroyWatcher.kt

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")}}Copy the code

3.3 the ViewModel

In previously AndroidXFragmentDestroyWatcher will monitor onFragmentCreated alone ()

override fun onFragmentCreated(
  fm: FragmentManager,
  fragment: Fragment,
  savedInstanceState: Bundle?). {
  ViewModelClearedWatcher.install(fragment, objectWatcher, configProvider)
}
Copy the code

Install actually generates a ViewModelClearedWatcher from the Fragment and ViewModelProvider, which is a new ViewModel, and then checks the Frag in the onCleared() of the ViewModel Whether there is a leak in each ViewModel in MENT

//ViewModelClearedWatcher.kt

init {
    // We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,
    // However that was added in 2.1.0 and we support AndroidX first stable release. Viewmodel -2.0.0
    // does not have ViewModelStore#keys. All versions currently have the mMap field.
    // Retrieve all viewModels of the fragment by reflection
    viewModelMap = try {
      val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
      mMapField.isAccessible = true
      @Suppress("UNCHECKED_CAST")
      mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) {
      null}}override fun onCleared(a) {
    if(viewModelMap ! =null && configProvider().watchViewModels) {
      viewModelMap.values.forEach { viewModel ->
        objectWatcher.watch(
            viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback")}}}Copy the code

4. Whether the monitored object leaks

Before talking about this, we must first review a knowledge point, WeakReference in Java is a WeakReference type, whenever GC occurs, if the object it holds is not held by other strong references, then the object it refers to will be reclaimed, at the same time or later the WeakReference will be added to the queue to Refer EnceQueue. LeakCanary detects memory leaks based on this principle.

/** * Weak reference objects, which do not prevent their referents from being * made finalizable, finalized, and then reclaimed. Weak references are most * often used to implement canonicalizing mappings. * * <p> Suppose that the  garbage collector determines at a certain point in time * that an object is <a href="package-summary.html#reachability">weakly * reachable</a>. At that time it will atomically clear all weak references to * that object and all weak references to any other weakly-reachable objects * from which that object is reachable through a chain of strong and soft * references. At the same time it will declare all of the formerly * weakly-reachable objects to be finalizable. At the same time or at some * later time it will enqueue those newly-cleared  weak references that are * registered with reference queues. * *@author   Mark Reinhold
 * @since1.2 * /

public class WeakReference<T> extends Reference<T> {

    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q); }}Copy the code

Key points to achieve:

  1. When an object needs to be recycled, a unique key is generated, encapsulated into KeyedWeakReference, and passed in the custom ReferenceQueue
  2. Put the key and KeyedWeakReference into a map
  3. After a while (the default is 5 seconds) actively trigger GC, remove all the KeyedWeakReference in the user-defined ReferenceQueue (the objects referenced by them have been reclaimed), and at the same time according to the key of these KeyedWeakReference, the KeyedWe in the map AkReference is also removed.
  4. At this time, if there is still KeyedWeakReference remaining in the map, it is not entered the team, that is to say, the object corresponding to these KeyedWeakReference has not been reclaimed. This is not reasonable, and there is a memory leak.
  5. Analyze the reference chain of these memory leaking objects and save the data

Here’s the code:

//ObjectWatcher.kt /** * Watches the provided [watchedObject]. * * @param description Describes why the object is watched. */ @Synchronized fun watch( watchedObject: Any, description: String ) { ...... / / remove all KeyedWeakReference in reference queue, at the same time, it will be removed from the map removeWeaklyReachableObjects (val) key = UUID. RandomUUID (). The toString () val watchUptimeMillis = clock.uptimeMillis() val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, WatchedObjects [key] = reference // moveToRetained() check is executed after 5 seconds checkRetainedExecutor.execute { moveToRetained(key) } } @Synchronized private fun moveToRetained(key: String) {/ / remove those who have already been recovered removeWeaklyReachableObjects () / / determine whether the corresponding to the key lock KeyedWeakReference been removed val retainedRef = If (retainedRef! = null) { retainedRef.retainedUptimeMillis = clock.uptimeMillis() onObjectRetainedListeners.forEach { it.onObjectRetained() } } }Copy the code

The Activity or Fragment that needs to be recycled will use the watch() method to check for memory leaks. The code above corresponds to steps 1-4 to implement the key points. Next specific analysis memory leak is how to go

//InternalLeakCanary#onObjectRetained()
//InternalLeakCanary#scheduleRetainedObjectCheck()
//HeapDumpTrigger#scheduleRetainedObjectCheck()
//HeapDumpTrigger#checkRetainedObjects()

private fun checkRetainedObjects(a) {
    // For example, if you are debugging, do not dump the heap and wait 20 seconds to determine the state

    val config = configProvider()
    
    ......
    // How many objects are left uncollected
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      // Manually trigger the GC, which is also triggered with a delay of 100ms to allow a bit of time for reclaimed objects to be queued by reference to make the results more accurate.
      gcTrigger.runGc()
      // Let's see how many objects are left
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    
    //checkRetainedCount returns true in 2 cases.
    //1. If the number of unreclaimed objects is 0, a leak free notification is displayed
    //2. When retainedReferenceCount is less than 5, display the leak notification (app visible or not visible for more than 5 seconds), delay 2 seconds before checking checkRetainedObjects()
    //app visibility is determined in visibilitytracker. kt by recording the number of Activity#onStart and onStop
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      // Dump in 1 minute, come back lateronRetainInstanceListener.onEvent(DumpHappenedRecently) showRetainedCountNotification( objectCount = retainedReferenceCount, contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) ) scheduleRetainedObjectCheck(  delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis )return
    }

    / / the dump
    // Dump heap with debug.dumphProfData (filePath)
    . / / dump heap before have to objectWatcher clearObjectsWatchedBefore (heapDumpUptimeMillis) to remove it before the start of the dump all references
    HeapAnalyzerService#runAnalysis() HeapAnalyzerService#runAnalysis()
    dumpHeap(retainedReferenceCount, retry = true)}Copy the code

HeapAnalyzerService invocation is Shark library to analyze heap, back to DefaultOnHeapAnalyzedListener. The analysis results of onHeapAnalyzed analysis results into the Treasury, send a notification message.

Shark 🦈 : Shark is the heap Analyzer that powers LeakCanary 2. It’s a Kotlin standalone heap analysis library that runs at “high Speed “with a” low memory footprint “.

5. To summarize

LeakCanary is an elegant canary that helps us detect memory leaks. This article mainly analyzes the initialization of LeakCanary, the timing of monitoring the leak, and the process of monitoring the leak of an object. The source code is very elegant, not fully shown in this article, the source code is too much to stick up too elegant. Reading source code not only allows us to learn new things, but also gives us something to emulate in future code, and even a handy job interview.