Hi, everybody friends, stupid bird meets you again. Over the next month or two, Goofbird will publish a series of articles focusing on Android performance optimization. As an advanced knowledge of Android, performance optimization is useful knowledge both in social interviews and in daily work, and it is a touchstone for differentiating intermediate and advanced programmers. Small stupid bird will be different topics to explain, I hope you like, if you want to know more, welcome to pay attention to my study together.

Today we first learn a small knowledge of memory optimization, is the detection and solution of memory leaks. Of course, how to solve the problem is something that most intermediate engineers have to learn, and there are plenty of materials available online, so I won’t go into detail here. Instead, the focus is on memory leak detection. Square’s LeakCanary is the best known memory leak detection tool in the industry.

Reading this passage, which is expected to take about 20 minutes, you will learn:

  • LeakCanary Detects the principle of memory leak
  • Use ContentProvider to initialize a tripartite library

The principle of overview

The website has a detailed explanation of how LeakCanary works. 1.LeakCanary uses ObjectWatcher to monitor the Android life cycle. When the Activity and Fragment are destroyed, those references are passed to the ObjectWatcher as weakReferences. If these references are not cleared after 5 seconds of gc, then there is a memory leak. 2. When the number of leaked objects reaches a threshold, LeakCanary dumps the Java stack information to an.hprof file. 3.LeakCanary uses the Shark library to parse the.hprof file, find the reference stack of references that cannot be cleaned, and then determine which instance is causing the leak based on our knowledge of the Android system. 4. By leaking information, LeakCanary will reduce a complete reference chain to a smaller one, and the rest of the leak chain caused by this small reference chain will be aggregated.

LeakCanary is a library that we can easily learn from:

  • How does LeakCanary use ObjectWatcher to monitor the life cycle?
  • LeakCanary How to dump and analyze.hprofThe file?

Look at the official principle always feel not satisfied, let’s analyze from the code level. This article is based on LeakCanary 2.0 Beta.

The basic use

LeakCanary is fairly simple to use. Just add a dependency line to build.gradle on the Module for less code intrusion.

dependencies {  // debugImplementation because LeakCanary should only run in debug builds.DebugImplementation 'com. Squareup. Leakcanary: leakcanary - android: 2.0 - beta - 3'}Copy the code

As such, the application simply plugs into LeakCanary memory detection. Of course, there are some more advanced uses, such as changing custom Config, adding monitoring items, etc., you can refer to the official website

Source code analysis

1. Initialization

Compared to the previous 1.x release, 2.0 doesn’t even need to add install code to the Application. For those of you wondering, how does LeakCanary insert its own initialization code? LeakCanary is initialized using a ContentProvider. I was introducing the Android plug-in series 3: The technical schools and the support of the four major components have introduced the characteristics of ContentProvider, that is, during the packaging process, contentProviders from different modules will be merged into a file, and the ContentProvider is automatically installed when the app is started. And the installation will be earlier than Application’s onCreate. LeakCanary has been designed on this basis. Are your Android libraries still being initialized in your Application?

We can check the LeakCanary source code and find that it has a ContentProvider in the Androidmanifest.xml of LeakCanary – object-watcher-Android.

<provider        android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"        android:authorities="${applicationId}.leakcanary-installer"        android:exported="false"/>Copy the code

Then we look at the code for the AppWatcherInstaller and see that internal install is using InternalAppWatcher.

  // AppWatcherInstalleroverride fun onCreate(): Boolean {val application = context!! .applicationContext as Application    InternalAppWatcher.install(application)    return true}  // InternalAppWatcherfun install(application: Application) {// Omit some code    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

You can see that the main thing here is to separate activities and fragments and register them separately. The Activity of the life cycle is listening through the Application. ActivityLifecycleCallbacks.

private val lifecycleCallbacks =    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {      override fun onActivityDestroyed(activity: Activity) {        if (configProvider().watchActivities) {          objectWatcher.watch(activity)        }      }    }application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)Copy the code

Listening is with the aid of an Activity and fragments of life cycle of ActivityLifecycleCallbacks lifecycle callback, When the Activity is created to invoke FragmentManager. RegisterFragmentLifecycleCallbacks registered fragments of life cycle method to monitor.

    override fun onFragmentViewDestroyed(      fm: FragmentManager,      fragment: Fragment    ) {      val view = fragment.viewif (view ! = null && configProvider().watchFragmentViews) {        objectWatcher.watch(view)      }    }    override fun onFragmentDestroyed(      fm: FragmentManager,      fragment: Fragment    ) {      if (configProvider().watchFragments) {        objectWatcher.watch(fragment)      }    }  }Copy the code

Eventually, both activities and fragments pass their own references to objectwatcher.watch () for monitoring. This is where you enter the reference monitoring logic for LeakCanary.

Side note: LeakCanary 2.0 has added a Fragment lifecycle monitor compared to 1.0, and the responsibilities of each class are clearer. However, I personally feel that using (Activty)->Unit as a lambda expression as a class is not very elegant, but rather interface oriented programming. It is perfectly possible to design both ActivityWatcher and FragmentWatcher to inherit from an interface, making it easy for subsequent extensions.

2. Reference monitoring

2.1 References and GC

  1. Quote First let’s talk a little bit about preparation. As you all know, there are four types of references in Java:
  • Strong references: The garbage collector will never collect it, and the Java VIRTUAL machine will throw OOM if it runs out of memory
  • Soft references: Only when memory is low does the JVM reclaim the space occupied by objects that only soft references point to
  • Weak references: When a JVM does garbage collection, objects associated only with weak references are reclaimed, regardless of whether memory is sufficient.
  • Virtual references: As with no references, they can be garbage collected at any time.

If an object has a soft (or weak, or virtual) reference to it when it is being gc, it is added to its associated ReferenceQueue (ReferenceQueue) before the object is reclaimed. If a soft-reference (or weak-reference, or vref) object is itself in the reference queue, the object to which the reference object points has been reclaimed.

When soft references (or a weak reference, or virtual reference object is the object points to be recycled, so the reference object itself has no value, if the program in the presence of large amounts of this type of object (note that we create a soft reference and a weak reference, virtual reference object itself is a strong reference, does not automatically by gc recycling), would waste memory. So we can now manually reclaim the reference object itself in the reference queue.

For example, we often see this usage

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);Copy the code

There is also a way to use it

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list, new ReferenceQueue<WeakReference<ArrayList>>());Copy the code

In this way, the object can be associated with the ReferenceQueue to determine whether the object is GC or not. In addition, we can see from the characteristics of weak references, weak references do not affect whether the object is gc, which is a good way to monitor the object’s GC status.

2. There are two ways to manually invoke GC in GCjava.

System.gc();/ / orRuntime.getRuntime().gc();Copy the code

2.2 monitor

As we mentioned in section 1, both activities and fragments rely on the response LifecycleCallback to call back and forth for destruction messages, and then call objectwatcher.watch to add post-destruction monitoring. Now let’s look at what objectwatcher.watch does.

  @Synchronized fun watch(    watchedObject: Any,    name: String  ) {    removeWeaklyReachableObjects()    val key = UUID.randomUUID().toString()    val watchUptimeMillis = clock.uptimeMillis()    val reference =      KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)    watchedObjects[key] = reference    checkRetainedExecutor.execute {      moveToRetained(key)    }  }  private fun removeWeaklyReachableObjects() {    // 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)  }  @Synchronized private fun moveToRetained(key: String) {    removeWeaklyReachableObjects()    val retainedRef = watchedObjects[key]if (retainedRef ! = null) {      retainedRef.retainedUptimeMillis = clock.uptimeMillis()      onObjectRetainedListeners.forEach { it.onObjectRetained() }    }  }Copy the code

Here we see that there is a ReferenceQueue object that stores KeyedWeakReference. Every time a Watch object is added, the object in the ReferenceQueue will be removed from the watchObjects map, because these objects have been reclaimed. Then generate a KeyedWeakReference, which is a WeakReference object holding a key and monitoring the start time. MoveToRetained, which records and calls back to the monitor when the object was monitored.

So now that we have the object we need to monitor, how do we determine that the object has leaked memory? And that’s where you keep going. OnAppWatcherInstalled (Application) onAppWatcherInstalled(application) onAppWatcherInstalled(Application) The code shows that this method is the Invoke method of InternalLeakCanary.

  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    )  }  override fun onObjectRetained() {    if (this::heapDumpTrigger.isInitialized) {      heapDumpTrigger.onObjectRetained()    }  }Copy the code

HeapDumper, gcTrigger, heapDumpTrigger, etc are initialized for GC and heapDump. OnObjectRetainedListener is also implemented. And added to the above onObjectRetainedListeners themselves, so that each object moveToRetained, InternalLeakCanary can get onObjectRetained () callback, Just back in the callback heapDumpTrigger. OnObjectRetained () method. It all seems to depend on the class HeapDumpTrigger.

The main processing logic for HeapDumpTrigger is in the checkRetainedObjects method.

  private fun checkRetainedObjects(reason: String) {    val config = configProvider()    var retainedReferenceCount = objectWatcher.retainedObjectCount    if (retainedReferenceCount > 0) {Gctrigger.rungc () // Triggers a GC operation, reserving only objects that cannot be reclaimed      retainedReferenceCount = objectWatcher.retainedObjectCount    }    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) returnif (! config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {      showRetainedCountWithDebuggerAttached(retainedReferenceCount)      scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)      return    }    val heapDumpUptimeMillis = SystemClock.uptimeMillis()    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis    dismissRetainedCountNotification()    val heapDumpFile = heapDumper.dumpHeap()    if (heapDumpFile == null) {      scheduleRetainedObjectCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)      showRetainedCountWithHeapDumpFailed(retainedReferenceCount)      return    }    lastDisplayedRetainedObjectCount = 0    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)    HeapAnalyzerService.runAnalysis(application, heapDumpFile)  }Copy the code

So what exactly does HeapDumpTrigger do? I have arranged mainly the following functions:

  • Background threads poll the objects that are currently alive
  • If the surviving objects are greater than zero, a GC operation is triggered to reclaim the unleaked objects
  • After GC, the number of surviving objects is compared with the predetermined number of objects. If there are more objects, the heapDumper.dumpheap () method is called to dump the objects into a file and hand it to The HeapAnalyzerService for analysis
  • Display notifications based on survivability

2.3 summarize

So if we look at this, we should have an idea. Activities and fragments register their own references to ObjectWatcher when onDestroy is being monitored by the HeapDumpTrigger class. AndroidHeapDumper is called to dump the file and then relies on HeapAnalyzerService for analysis. In the next section, we will focus on the object dump operation and the HeapAnalyzerService analysis process.

3. Dump objects and analyze them

3.1 the dump object

Hprof is a JVM TI Agent native tool provided by the JDK. JVM TI is a standard C/C++ programming interface provided by the JVM. It is the unified basis for Debugger, Profiler, Monitor, Thread Analyser and other tools. It has been implemented in the mainstream Java virtual machine. The Hprof tool actually implements this interface and can be considered a simple set of Profiler Agent tools. We also mentioned it in new Knowledge Week: 10.8-10.14 (launch), you can refer to meituan’s article.

Hprof files are familiar to anyone who has used the Android Studio Profiler tool. When you use the Memory Profiler tool to Dump the Java Heap icon, the Profiler tool captures your Memory allocation. However, once captured, we can only view the Memory Profiler while it is running, so how can we save the Memory usage at that time, or if I want to use another tool to analyze the heap allocation, the hprof file will come in handy. Android Studio can export these objects to hprof files.

LeakCanary also uses the Hprof file for object storage. The Hprof file is relatively simple and is organized in the format of pre-information + record sheet. But the variety of records is considerable. See HPROF Agent for details.

Android also provides a simple method debug. dumpHprofData(filePath) to dump objects to the hprof file in the specified path. LeakCanary uses the Shark library to parse various records in the Hprof file. The Shark HprofReader and HprofWriter are used to read and write the information we need. You can focus on some important ones, such as:

  • Class Dump
  • Instance Dump
  • Object Array Dump
  • Primitive Array Dump

Dump specific code in the AndroidHeapDumper class. HprofReader and HprofWriter are too complex. If you are interested, check the source code directly

  override fun dumpHeap(): File? {val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ? : return null    return try {      Debug.dumpHprofData(heapDumpFile.absolutePath)      if (heapDumpFile.length() == 0L) {        null      } else {        heapDumpFile      }    } catch (e: Exception) {      null    } finally {    }  }Copy the code

3.2 Object Analysis

As we have already analyzed, HeapDumpTrigger mainly relies on HeapAnalyzerService for analysis. So what’s the big deal with HeapAnalyzerService? Let’s move on. HeapAnalyzerService is a ForegroundService. The Analyze method of HeapAnalyzer is called upon receiving the analyze Intent. So the final place for analysis is the Analyze method of HeapAnalyzer.

The core code is as follows

    try {      listener.onAnalysisProgress(PARSING_HEAP_DUMP)      Hprof.open(heapDumpFile)          .use { hprof ->// 1. Generate graph            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)// 2. Look for Leak            val findLeakInput = FindLeakInput(                graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors            )            val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks()            listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)            return HeapAnalysisSuccess(                heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),                applicationLeaks, libraryLeaks            )          }    } catch (exception: Throwable) {      listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)      return HeapAnalysisFailure(          heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),          HeapAnalysisException(exception)      )    }  }Copy the code

This code covers the use of the Shark library designed for LeakCanary, which is not explained here. Here’s an overview of what each step does:

  • First call HprofHeapGraph indexHprof method, this method will dump out all kinds of instance instance, Class Class object and an Array of objects, etc are set up the index of the query, in order to record the id as the key, All required information is stored in the Map for easy access
  • Call the findLeakInput.findleak method, which starts with the GC Root, finds the shortest chain of references that caused the leak, and builds LeakTrace from that chain.
  • Show the searched LeakTrace to the public

conclusion

This article analyzes the LeakCanary memory leak detection and some code design ideas, but there is no space to cover everything. Let’s answer the question posed at the beginning of this article.

1. How does LeakCanary use ObjectWatcher to monitor the life cycle? LeakCanary using the Application ActivityLifecycleCallbacks and FragmentManager FragmentLifecycleCallbacks method to carry on the Activity and fragments of life cycle Check, when an Activity or Fragment is called back to onDestroy, the ObjectWatcher generates a KeyedReference to check, and then uses HeapDumpTrigger polling and gc triggering to find the time to send an alert.

2.LeakCanary How to dump and analyze the. Hprof file? You have obtained the hprof file using the debug. dumpHprofData method of the Android platform, parsed the self-built Shark library, and obtained the LeakTrace file

reference

LeakCanary website

HPROF Agent.

Are your Android libraries still initialized in Application?

I am Android stupid bird journey, stupid birds also want to have the heart to fly up, I am here to accompany you slowly become stronger.