LeakCanaray is Square’s open-source Memory leak detection library for the Android App.

Project address: github.com/square/leak…

The official instructions: square. Dead simple. IO/leakcanary /

A, use,

1.1 Project Introduction

For versions after 2.0, we do not need to configure LeakCanary. Install (this) in our application, just import the library from build.gradle:

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

LeakCanaray has been installed and is running properly if the following log is displayed:

D LeakCanary: LeakCanary is running and ready to detect leaks
Copy the code

1.2 Triggering Scenario

The detection is automatically triggered when the Activity, Fragment, or Fragment View instances are destroyed and the ViewModel is cleaned.

1.3 Automatic detection and reporting workflow

Monitor object recycling -> Dump heap -> Analyze heap -> output results if there are leaks.

1.4 Limitations: Root activities and services cannot be detected.

Because of the low cost of access and testing, it is recommended to do an initial screening for memory leaks in a normal business.

For use before version 2.0, see previous article: Performance Tuning Tool (9) -LeakCanary

Second, source code analysis

2.1 the initialization

LeakCanary. Install (this) is gone and the class name has changed, so the framework initialization is a bit hard to find. (Note: leakCanaray V2.5 is the Kotlin code)

internal sealed class AppWatcherInstaller : ContentProvider() { override fun onCreate(): Boolean { val application = context!! .applicationContext as Application AppWatcher.manualInstall(application) return true } }Copy the code

Here the initialization is in ContentProvider.oncreate, which executes before application.oncreate, thus omits the step of application install on the client side. Then see: AppWatcher manualInstall – > InternalAppWatcher. Install

leakcanary/internal/InternalAppWatcher.kt

fun install(application: Application) {
  checkMainThread()
  if (this::application.isInitialized) {
   return
  }
  InternalAppWatcher.application = application
  if (isDebuggableBuild) {
   SharkLog.logger = DefaultCanaryLog()
  }

  val configProvider = { AppWatcher.config }
  ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
  FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
  onAppWatcherInstalled(application)
}
Copy the code

Here you install the Activity and Fragment respectively.

2.2 Memory leak monitoring

Here to ActivityDestroyWatcher. Install as an example analysis

ActivityDestroyWatcher.kt

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {
  private val lifecycleCallbacks =
   object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
     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

Initialize ActivityDestroyWatcher, register life cycle callbacks with the application, listen for the Activity onDestroy callback, and monitor memory leaks through objectwatcher.watch.

leakcanary/ObjectWatcher.kt private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>() private val  watchedObjects = mutableMapOf<String, KeyedWeakReference>() private val queue = ReferenceQueue<Any>() @Synchronized fun watch( watchedObject: Any, description: String ) { if (! IsEnabled () {return} / / 1. The gc first before ReferenceQueue references in clear removeWeaklyReachableObjects (val) key = UUID. RandomUUID () .toString() val watchUptimeMillis = clock.uptimeMillis() //2. Wrap the activity cause as a weak reference, 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 / / 3 \. 5 s after departure detection (gc) 5 s time checkRetainedExecutor. Execute {moveToRetained (key)}}Copy the code

Here checkRetainedExecutor is passed in externally and has a 5s delay.

leakcanary/internal/InternalAppWatcher.kt

private val checkRetainedExecutor = Executor {
  mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)//5s
}
Copy the code

Read on:

private fun moveToRetained(key: String) { removeWeaklyReachableObjects() val retainedRef = watchedObjects[key] if (retainedRef ! = null) { retainedRef.retainedUptimeMillis = clock.uptimeMillis() onObjectRetainedListeners.forEach { it.onObjectRetained() } } }Copy the code

RetainedRef will be null if GC is successfully collected within 5s delay time, otherwise gc will be triggered, so the subsequent memory leak will be determined by gc again.

leakcanary/internal/InternalLeakCanary.kt override fun onObjectRetained() = scheduleRetainedObjectCheck() fun scheduleRetainedObjectCheck() { if (this::heapDumpTrigger.isInitialized) { heapDumpTrigger.scheduleRetainedObjectCheck() }}Copy the code

If there is a memory leak, then dumpHeap will be executed:

2.3 the dump to confirm

leakcanary/internal/HeapDumpTrigger.kt private fun dumpHeap( retainedReferenceCount: Int, retry: Boolean ) { saveResourceIdNamesToMemory() val heapDumpUptimeMillis = SystemClock.uptimeMillis() KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis //1.dump heap when (val heapDumpResult = heapDumper.dumpHeap()) { ... Is HeapDump -> {... //2.analysis heap HeapAnalyzerService.runAnalysis( context = application, heapDumpFile = heapDumpResult.file, heapDumpDurationMillis = heapDumpResult.durationMillis ) } } }Copy the code

Dump hprof files and then create a service to analyze dump heap files.

2.4 heap dump

leakcanary/internal/AndroidHeapDumper.kt override fun dumpHeap(): DumpHeapResult { val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ? : return NoHeapDump val waitingForToast = FutureResult<Toast? >() showToast(waitingForToast) if (! waitingForToast.wait(5, SECONDS)) { SharkLog.d { "Did not dump heap, too much time waiting for Toast." } return NoHeapDump } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Notifications.canShowNotification) { val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping) val builder = Notification.Builder(context) .setContentTitle(dumpingHeap) val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW) notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification) } val toast = waitingForToast.get() return try { val durationMillis = measureDurationMillis { Debug.dumpHprofData(heapDumpFile.absolutePath) } if (heapDumpFile.length() == 0L) { SharkLog.d { "Dumped heap file is 0 byte length" } NoHeapDump } else { HeapDump(file = heapDumpFile, durationMillis = durationMillis) } } catch (e: Exception) { SharkLog.d(e) { "Could not dump heap" } // Abort heap dump NoHeapDump } finally { cancelToast(toast) notificationManager.cancel(R.id.leak_canary_notification_dumping_heap) } }Copy the code

The dump process sends Notification and then dumps the hprof file using debug. dumpHprofData.

cepheus:/data/data/com.example.leakcanary/files/leakcanary # ls -al
-rw------- 1 u0_a260 u0_a260 22944796 2020-12-07 11:30 2020-12-07_11-30-37_701.hprof
-rw------- 1 u0_a260 u0_a260 21910520 2020-12-07 14:52 2020-12-07_14-52-40_703.hprof
Copy the code

Next, look at the analysis of the service

2.5 Hprof memory leak analysis

leakcanary/internal/HeapAnalyzerService.kt override fun onHandleIntentInForeground(intent: Intent?) { if (intent == null || ! intent.hasExtra(HEAPDUMP_FILE_EXTRA)) { SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." } return } // Since we're running in the main process we should be careful not to impact it. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS, -1) val config = LeakCanary.config val heapAnalysis = if (heapDumpFile.exists()) { analyzeHeap(heapDumpFile, config) } else { missingFileFailure(heapDumpFile) } val fullHeapAnalysis = when (heapAnalysis) { is HeapAnalysisSuccess -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis) is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis) } onAnalysisProgress(REPORTING_HEAP_ANALYSIS) config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis) }Copy the code

First, the service is handled by a new process

<service
   android:name="leakcanary.internal.HeapAnalyzerService"
   android:exported="false"
   android:process=":leakcanary" />
Copy the code

Here the core method should be in analyzeHeap

private fun analyzeHeap(
  heapDumpFile: File,
  config: Config
): HeapAnalysis {
  val heapAnalyzer = HeapAnalyzer(this)
  val proguardMappingReader = try {
    ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
  } catch (e: IOException) {
    null
  }
  return heapAnalyzer.analyze(
      heapDumpFile = heapDumpFile,
     leakingObjectFinder = config.leakingObjectFinder,
     referenceMatchers = config.referenceMatchers,
     computeRetainedHeapSize = config.computeRetainedHeapSize,
     objectInspectors = config.objectInspectors,
     metadataExtractor = config.metadataExtractor,
     proguardMapping = proguardMappingReader?.readProguardMapping()
  )
}
Copy the code

The final job of analyzing the Heap dumps to find leaks is left to HeapAnalyzer

shark/HeapAnalyzer.kt fun analyze( heapDumpFile: File, leakingObjectFinder: LeakingObjectFinder, referenceMatchers: List<ReferenceMatcher> = emptyList(), computeRetainedHeapSize: Boolean = false, objectInspectors: List<ObjectInspector> = emptyList(), metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP, proguardMapping: ProguardMapping? = null ): HeapAnalysis { val analysisStartNanoTime = System.nanoTime() if (! heapDumpFile.exists()) { val exception = IllegalArgumentException("File does not exist: $heapDumpFile") return HeapAnalysisFailure( heapDumpFile = heapDumpFile, createdAtTimeMillis = System.currentTimeMillis(), analysisDurationMillis = since(analysisStartNanoTime), exception = HeapAnalysisException(exception) ) } return try { listener.onAnalysisProgress(PARSING_HEAP_DUMP) val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)) sourceProvider.openHeapGraph(proguardMapping).use { graph -> val helpers = FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors) val result = helpers.analyzeGraph( metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime ) val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats() val randomAccessStats = "RandomAccess[" + "bytes=${sourceProvider.randomAccessByteReads}," + "reads=${sourceProvider.randomAccessReadCount}," + "travel=${sourceProvider.randomAccessByteTravel}," + "range=${sourceProvider.byteTravelRange}," + "size=${heapDumpFile.length()}" + "]" val stats = "$lruCacheStats $randomAccessStats" result.copy(metadata = result.metadata + ("Stats" to stats)) } } catch (exception: Throwable) { HeapAnalysisFailure( heapDumpFile = heapDumpFile, createdAtTimeMillis = System.currentTimeMillis(), analysisDurationMillis = since(analysisStartNanoTime), exception = HeapAnalysisException(exception) ) } }Copy the code

Here by ConstantMemoryMetricsDualSourceProvider reading hprof files, then by FindLeakInput for analysis.

private fun FindLeakInput.analyzeGraph( metadataExtractor: MetadataExtractor, leakingObjectFinder: LeakingObjectFinder, heapDumpFile: File, analysisStartNanoTime: Long ): HeapAnalysisSuccess { listener.onAnalysisProgress(EXTRACTING_METADATA) val metadata = metadataExtractor.extractMetadata(graph) listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS) //1. Get a collection of leaked object ids from hprof, where weak references are collected that have not been reclaimed. val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph) //2. For these suspected leaking objects, the shortest reference path to gcroot is calculated to determine whether a leak has occurred. val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds) return HeapAnalysisSuccess( heapDumpFile = heapDumpFile, createdAtTimeMillis = System.currentTimeMillis(), analysisDurationMillis = since(analysisStartNanoTime), metadata = metadata, applicationLeaks = applicationLeaks, libraryLeaks = libraryLeaks ) }Copy the code

Here leakingObjectFinder findLeakingObjectIds is actually KeyedWeakReferenceFinder, first to get through it leak object id of the collection. FindLeaks then calculates the shortest reference path to Gcroot for those suspected leaks to determine if a leak has occurred.

Finally, build LeakTrace, pass the chain of references, and present the results of the analysis.

val leakTrace = LeakTrace(
    gcRootType = GcRootType.fromGcRoot(shortestPath.root.gcRoot),
   referencePath = referencePath,
   leakingObject = leakTraceObjects.last()
)
Copy the code

Third, the framework changes

Official note:

Since version 1.6.3, there have been major changes, which can be summarized as follows:

  • Cut to the kotlin Java

  • The Heap analysis library has changed from Haha to Shark, which is itself an open source library for Square: github.com/square/haha, Shark does not exist as a standalone third-party open source library but is a component of leakCanaray, so the total kotlin code volume for the new project has increased.

  • The memory leak workflow has been optimized.

Fourth, workflow summary

LeackCanaray also provides FragmentDestroyWatcher. The principle is the same as that of the leakCanary core class.

  • The application process section focuses on Activity/Fragment life cycle monitoring, watcher cites them.

  • Memory leak prediction detection mechanism: Judge whether the object is recovered by system GC through WeakReference +ReferenceQueue, and the Activity/Fragment reference is packaged as WeakReference and passed into ReferenceQueue at the same time. When the wrapped Activity/Fragment object is detected by the GC at the end of its life cycle, it is added to the ReferenceQueue and processed by the ReferenceQueue. When an object is not added to the ReferenceQueue after GC, it may have a memory leak.

  • Dump or not: Gc is actively triggered to see if there are any weak references that have not been reclaimed. When applied in the foreground, dump operation can only be triggered if 5 or more leak objects are met, and only one leak object is met in the background. However, there is also a nonpReason restriction in both the front and background. This reason is equivalent to a unified fault tolerance. Save to determine whether leakCanary is installed, configured correctly, whether the previous Noify notification has been sent, etc.

  • Dump hprof files through the Debug. DumpHprofData (filePath) to implement, in data/data/package/files/leakcanary directory and file size 10 several M to M range. This process should be time-consuming.

  • Hprof file analysis is handled by HeapAnalyserService, which itself is in a separate process. The core functions are completed by Shark, and the main work of memory leakage is: The collection of leaked object ids is obtained from hprof, where weak references that have not been recovered are mainly collected. For these suspected leaked objects, the shortest reference path to Gcroot is calculated to confirm whether leakage occurs. If a memory leak is confirmed, statistical report output is generated.

Reference: Leakcanary official documentation