美文网首页
LeakCanary解析

LeakCanary解析

作者: 就叫汉堡吧 | 来源:发表于2023-03-29 14:35 被阅读0次
    • 概述

      LeakCanary is a memory leak detection library for Android.

      LeakCanary官网

      当我们需要排查应用的内存泄露问题时,通常的做法是通过AndroidStudio的Profiler或者其他图形化分析工具查找当某个实例销毁时gc后内存曲线没有显著的场景,然后dump它的hprof来分析。

      LeakCanary则是通过代码“hook”的方式来实现疑似内存泄露的监听,通过它你可以不用再盯着内存曲线变化,当有疑似内存泄露发生时,LeakCanary会自动提示并进行dump,同时它会在Launcher上创建一个应用图标,点开后能看见解析后的hprof信息,也就是说还有类似MAT的作用。

      下面我们结合使用来分析它是怎么做的。

    • 使用

      使用LeakCanary非常简单,添加一个依赖即可(有debug和release两种方式可选):

      dependencies {
        // debugImplementation because LeakCanary should only run in debug builds.
        debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
        // LeakCanary for releases
        releaseImplementation 'com.squareup.leakcanary:leakcanary-android-release:2.10'
      }
      
    • 源码分析

      首先看一下leakcanary怎么做到在桌面生成一个图标来查看泄露分析界面的,在leakcanary-android-core jar包下的Manifest中:

      <activity-alias
          android:name="leakcanary.internal.activity.LeakLauncherActivity"
          android:banner="@drawable/leak_canary_tv_icon"
          android:enabled="@bool/leak_canary_add_launcher_icon"
          android:exported="true"
          android:icon="@mipmap/leak_canary_icon"
          android:label="@string/leak_canary_display_activity_label"
          android:targetActivity="leakcanary.internal.activity.LeakActivity"
          android:taskAffinity="com.squareup.leakcanary.${applicationId}"
          android:theme="@style/leak_canary_LeakCanary.Base" >
          <intent-filter>
              <action android:name="android.intent.action.MAIN" />
              <category android:name="android.intent.category.LAUNCHER" />
              <!-- Android TV launcher intent -->
              <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
          </intent-filter>
      </activity-alias>
      

      可以看到,是通过activity-alias标签来实现LeakActivity的便捷入口的。

      在leakcanary-object-watcher-android jar包下的Manifest配置了:

      <application>
          <provider
              android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
              android:authorities="${applicationId}.leakcanary-installer"
              android:enabled="@bool/leak_canary_watcher_auto_install"
              android:exported="false" />
      </application>
      

      MainProcessAppWatcherInstaller是一个ContenProvider:

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

      AppWatcher.manualInstall如下:

      fun manualInstall(
        application: Application,
        retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
        watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
      ) {
        ...
        // Requires AppWatcher.objectWatcher to be set
        LeakCanaryDelegate.loadLeakCanary(application)
      
        watchersToInstall.forEach {
          it.install()
        }
        ...
      }
      

      先看LeakCanaryDelegate.loadLeakCanary:

      val loadLeakCanary by lazy {
        try {
          val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
          leakCanaryListener.getDeclaredField("INSTANCE")
            .get(null) as (Application) -> Unit
        } catch (ignored: Throwable) {
          NoLeakCanary
        }
      }
      

      这里是一个函数属性,InternalLeakCanary使用了一个特别的kotlin语法,使用invoke实现函数体,实际上继承的是Function1接口:

      internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {
        ...
        override fun invoke(application: Application) {
          _application = application
            ...
          AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
      
          val gcTrigger = GcTrigger.Default
      
          val configProvider = { LeakCanary.config }
            ...
      
          heapDumpTrigger = HeapDumpTrigger(
            application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
            configProvider
          )
          application.registerVisibilityListener { applicationVisible ->
            this.applicationVisible = applicationVisible
            heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
          }
          registerResumedActivityListener(application)
          addDynamicShortcut(application)
            ...   
        }
        ...
      }
      

      这里看这些代码还看不出用处,先暂时一放,再看watchersToInstall里是什么,它来自于appDefaultWatchers(application)方法:

      fun appDefaultWatchers(
        application: Application,
        reachabilityWatcher: ReachabilityWatcher = objectWatcher
      ): List<InstallableWatcher> {
        return listOf(
          ActivityWatcher(application, reachabilityWatcher),
          FragmentAndViewModelWatcher(application, reachabilityWatcher),
          RootViewWatcher(reachabilityWatcher),
          ServiceWatcher(reachabilityWatcher)
        )
      }
      

      以检测Activity泄露为例,看下ActivityWatcher的install方法:

      private val lifecycleCallbacks =
          object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
            override fun onActivityDestroyed(activity: Activity) {
              reachabilityWatcher.expectWeaklyReachable(
                activity, "${activity::class.java.name} received Activity#onDestroy() callback"
              )
            }
          }
      override fun install() {
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
      }
      

      可以看到,这里会给Application注册一个关于Activity的生命周期监听,在onDestroy时会调用 方法,reachabilityWatcher是前面默认方法参数赋值的objectWatcher:

      val objectWatcher = ObjectWatcher(
        clock = { SystemClock.uptimeMillis() },
        checkRetainedExecutor = {
          check(isInstalled) {
            "AppWatcher not installed"
          }
          mainHandler.postDelayed(it, retainedDelayMillis)
        },
        isEnabled = { true }
      )
      

      它的expectWeaklyReachable方法如下:

      @Synchronized override fun expectWeaklyReachable(
        watchedObject: Any,
        description: String
      ) {
        if (!isEnabled()) {
          return
        }
        removeWeaklyReachableObjects()
        val key = UUID.randomUUID()
          .toString()
        val watchUptimeMillis = clock.uptimeMillis()
        val reference =
          KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
        ...
      
        watchedObjects[key] = reference
        checkRetainedExecutor.execute {
          moveToRetained(key)
        }
      }
      

      这里的removeWeaklyReachableObjects方法如下:

      private fun removeWeaklyReachableObjects() {
        var ref: KeyedWeakReference?
        do {
          ref = queue.poll() as KeyedWeakReference?
          if (ref != null) {
            watchedObjects.remove(ref.key)
          }
        } while (ref != null)
      }
      

      queue是一个ReferenceQueue,这里有一个基础知识:WeakReference构造的时候可以设置一个ReferenceQueue,如果WeakReference持有的对象被gc了就会把这个对象放入这个queue中。因此,这个方法的含义就是会先把已回收内存的对象从watchedObjects中移除。

      然后会把这个当前的对象(在此就是当前执行了onDestroy方法的Activity实例)放入watchedObjects中,随后会执行checkRetainedExecutor的execute,找到前面设置的checkRetainedExecutor,其实execute就是执行了:

      mainHandler.postDelayed(it, retainedDelayMillis)
      

      retainedDelayMillis是在调用上面的manualInstall方法时初始化的,初始值是5秒,因此,最终是延迟5秒后(主要是给系统留出尽可能充足的gc回收时间)在主线程执行moveToRetained(key)方法:

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

      onObjectRetainedListeners里的是什么呢?这时我们可以回到上面的InternalLeakCanary的invoke中找到一句AppWatcher.objectWatcher.addOnObjectRetainedListener(this),因此默认的有一个it就是InternalLeakCanary,它的onObjectRetained如下:

      override fun onObjectRetained() = scheduleRetainedObjectCheck()
      
      fun scheduleRetainedObjectCheck() {
        if (this::heapDumpTrigger.isInitialized) {
          heapDumpTrigger.scheduleRetainedObjectCheck()
        }
      }
      

      HeapDumpTrigger的scheduleRetainedObjectCheck方法如下:

      fun scheduleRetainedObjectCheck(
        delayMillis: Long = 0L
      ) {
        val checkCurrentlyScheduledAt = checkScheduledAt
        if (checkCurrentlyScheduledAt > 0) {
          return
        }
        checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
        backgroundHandler.postDelayed({
          checkScheduledAt = 0
          checkRetainedObjects()
        }, delayMillis)
      }
      

      可见会在后台线程执行checkRetainedObjects方法,简化如下:

      private fun checkRetainedObjects() {
        ...
        val config = configProvider()
        ...
        var retainedReferenceCount = objectWatcher.retainedObjectCount
      
        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()
          retainedReferenceCount = objectWatcher.retainedObjectCount
        }
      
        //如果返回true则说明没有泄露或者没达到需要dump的数量,若是false则继续往下执行dumpHeap
        if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
        ...
        val visibility = if (applicationVisible) "visible" else "not visible"
        dumpHeap(
          retainedReferenceCount = retainedReferenceCount,
          retry = true,
          reason = "$retainedReferenceCount retained objects, app is $visibility"
        )
      }
      

      objectWatcher.retainedObjectCount得到的是watchedObjects中尚未被gc回收的对象,如果还有则再次调用gc:

      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()
          }
        }
      }
      

      这里线程sleep是为了给gc操作留出时间。

      gc操作完后重新获取watchedObjects中尚未被gc回收的对象,调用checkRetainedCount方法再次检查,如果返回true则说明没有泄露或者没达到需要dump的数量,不再往下执行,即不打印log,若是false则继续往下执行dumpHeap。

      checkRetainedCount方法如下:

      private fun checkRetainedCount(
        retainedKeysCount: Int,
        retainedVisibleThreshold: Int,
        nopeReason: String? = null
      ): Boolean {
        val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
        lastDisplayedRetainedObjectCount = retainedKeysCount
        //前面的gc后无泄露对象,则不需要打印
        if (retainedKeysCount == 0) {
          if (countChanged) {
            SharkLog.d { "All retained objects have been garbage collected" }
            onRetainInstanceListener.onEvent(NoMoreObjects)
            showNoMoreRetainedObjectNotification()
          }
          return true
        }
        ...
        //这里会判断泄露对象是否达到了需要打印hprof的数量
        if (retainedKeysCount < retainedVisibleThreshold) {
          if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
            if (countChanged) {
              onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
            }
            showRetainedCountNotification(
              objectCount = retainedKeysCount,
              contentText = application.getString(
                R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
              )
            )
            scheduleRetainedObjectCheck(
              delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
            )
            return true
          }
        }
        return false
      }
      

      可见,除了无泄露时不打印日志之外,这里还会判断泄露对象是否达到了需要打印hprof的数量,如果没达到还是不会打印,但是会在通知栏显示通知,点击后会立即开启dump,这个数量限额默认是5个,即达到5个后自动开启dump。

      在应用可见或者设置了applicationInvisibleLessThanWatchPeriod为true时,未达到dump阈值但是又存在泄露的情况下会在等待2秒(WAIT_FOR_OBJECT_THRESHOLD_MILLIS)后再次调用scheduleRetainedObjectCheck方法重复以上的流程。

      最后来看dumpHeap方法:

      private fun dumpHeap(
        retainedReferenceCount: Int,
        retry: Boolean,
        reason: String
      ) {
        //创建leakcanary保存dump日志的文件夹和hprof文件
        val directoryProvider =
          InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
        val heapDumpFile = directoryProvider.newHeapDumpFile()
        ...
        try {
          ...
          val heapDumpUptimeMillis = SystemClock.uptimeMillis()
          KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
          durationMillis = measureDurationMillis {
            //进行文件输出
            configProvider().heapDumper.dumpHeap(heapDumpFile)
          }
          ...
          //把已打印完的泄露对象清空
          objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
          //监听器通知(LeakCanary.config.eventListeners)
          InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
        } catch (throwable: Throwable) {
          ...
          //通知栏通知dump失败
          showRetainedCountNotification(
            objectCount = retainedReferenceCount,
            contentText = application.getString(
              R.string.leak_canary_notification_retained_dump_failed
            )
          )
          return
        }
      }
      

      可以看到,其实调用了configProvider().heapDumper的dumpHeap方法,heapDumper是:

      val heapDumper: HeapDumper = AndroidDebugHeapDumper
      object AndroidDebugHeapDumper : HeapDumper {
        override fun dumpHeap(heapDumpFile: File) {
          Debug.dumpHprofData(heapDumpFile.absolutePath)
        }
      }
      

      使用Debug来进行日志文件输出。

    • 自定义设置

      可以通过LeakCanary.Config来进行自定义设置,比如:

      LeakCanary.Config config = new LeakCanary.Config.Builder(LeakCanary.getConfig()).retainedVisibleThreshold(1).build();
      LeakCanary.setConfig(config);
      

      以上设置了retainedVisibleThreshold,也就是上面checkRetainedCount方法中判断的dump数量阈值。

    • hprof的文件保存位置

      上面的newHeapDumpFile方法中设置保存hprof文件的路径如下:

      private fun externalStorageDirectory(): File {
        val downloadsDirectory = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
        return File(downloadsDirectory, "leakcanary-" + context.packageName)
      }
      

      如果需要该文件来进行MAT等工具处理的话可以在这个路径下找到。

    • 其他的默认监听的关键代码

      Fragment在onFragmentViewDestroyed时监听Fragment的View,在onFragmentDestroyed时监听fragment:

      internal class AndroidOFragmentDestroyWatcher(
        private val reachabilityWatcher: ReachabilityWatcher
      ) : (Activity) -> Unit {
        private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
      
          override fun onFragmentViewDestroyed(
            fm: FragmentManager,
            fragment: Fragment
          ) {
            val view = fragment.view
            if (view != null) {
              reachabilityWatcher.expectWeaklyReachable(
                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
          ) {
            reachabilityWatcher.expectWeaklyReachable(
              fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
            )
          }
        }
      
        override fun invoke(activity: Activity) {
          val fragmentManager = activity.fragmentManager
          fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
        }
      }
      

      View在onViewDetachedFromWindow时监听:

      rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
      
        val watchDetachedView = Runnable {
          reachabilityWatcher.expectWeaklyReachable(
            rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
          )
        }
      
        override fun onViewAttachedToWindow(v: View) {
          mainHandler.removeCallbacks(watchDetachedView)
        }
      
        override fun onViewDetachedFromWindow(v: View) {
          mainHandler.post(watchDetachedView)
        }
      })
      

      androidX时,ViewModel在onCleared时监听:

      internal class ViewModelClearedWatcher(
        storeOwner: ViewModelStoreOwner,
        private val reachabilityWatcher: ReachabilityWatcher
      ) : ViewModel() {
      
        // 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.
        private val viewModelMap: Map<String, ViewModel>? = 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() {
          viewModelMap?.values?.forEach { viewModel ->
            reachabilityWatcher.expectWeaklyReachable(
              viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
            )
          }
        }
      
        companion object {
          fun install(
            storeOwner: ViewModelStoreOwner,
            reachabilityWatcher: ReachabilityWatcher
          ) {
            val provider = ViewModelProvider(storeOwner, object : Factory {
              @Suppress("UNCHECKED_CAST")
              override fun <T : ViewModel?> create(modelClass: Class<T>): T =
                ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
            })
            provider.get(ViewModelClearedWatcher::class.java)
          }
        }
      }
      

      Service在onDestroy时监听:

      private fun onServiceDestroyed(token: IBinder) {
        servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
          serviceWeakReference.get()?.let { service ->
            reachabilityWatcher.expectWeaklyReachable(
              service, "${service::class.java.name} received Service#onDestroy() callback"
            )
          }
        }
      }
      
    • 其他对象的泄露监听

      除了Android中的Activity、Fragemtn、ViewModel、View等实例,你还可以自己设置其他可疑的自定义对象的监听。

      //watch已被废弃
      AppWatcher.objectWatcher.watch(obj)
      //改用
      AppWatcher.objectWatcher.expectWeaklyReachable(obj,description)
      

      不过注意,按照我们的分析,这样设置的话,从调用expectWeaklyReachable开始,就会被当作泄露对象来排查,因此这个操作应该根据具体的业务场景放在合适的位置,即必须在确认该对象应该被回收了时调用监听方法。否则在正在合理使用时也会把该对象当作泄露对象报告。

    • 总结

      通过以上分析,我们可以总结出LeakCanary的工作原理:

      1. LeakCanary是通过在实例的生命周期销毁方法里把该对象封装成弱引用对象保存到集合,此时这些对象被认为是可能会发生泄露的对象;

      2. 保存完之后等待一段时间(默认5秒)来给系统留出清理的时间;

      3. 之前保存成的弱引用都会指定同一个ReferenceQueue,所以这里通过检索ReferenceQueue中的对象,如果存在则说明已经被清理(JVM会把清理完的弱引用对象放在它指定的ReferenceQueue中),则把他们从监听集合watchedObjects中移除,留下的就是尚未被清理的;

      4. 因为我们在认为相关对象应该被销毁的时机放入监听集合watchedObjects中,在经过了上面的处理后如果仍然在watchedObjects中存在,则被认为是已经发生内存泄露的对象;

      5. 可以配置一旦检测到就dump报告,也可以配置达到一定阈值(默认是5)后dump。

    相关文章

      网友评论

          本文标题:LeakCanary解析

          本文链接:https://www.haomeiwen.com/subject/qpufddtx.html