查看原文
其他

LeakCanary 你真的了解么?看看这些高级用法

鸿洋
2024-08-30

The following article is from Rethink Android Author madroid

1前言


众所周知,Square 出品的内存泄漏检测工具 LeakCanary 可以很方便的检测出 App 中存在的内存泄漏问题。当我们决定要不要在项目中引入 LeakCanary 的时候,经常也会听到声音:


• “LeakCanary 接入简单,无需手动初始化。”

• “LeakCanary 虽好,但就是太卡。”

• “LeakCanary 虽好,但无法线上使用。”


一度我也是这么认为的,直到我认真研究了下才发现,事实可能并没有那么简单。本文就是尝试从 LeakCanary 的一些高级用法,来重新论证上述的观点。文末会附上完整代码,可直接使用。


想要使用 LeakCanary 的一些高级用法,首先就是需要我们主动掌握 LeakCanary 的初始化时机,添加一些自定义的配置,下面就看一下如何手动初始化 LeakCanary ?


2如何手动初始化 LeakCanary ?


正常情况下,我们只要添加下面一行代码,就可以在 App 中使用 LeakCanary 了。


dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}


自动初始化


这是怎么做到的?是采用了 ContentProvider 的加载机制来做的。简单讲大致流程如下:



1. 先执行 Application中的attachBaseContext 函数;

2. 然后会执行 ContentProvider 中的 onCreate 函数;

3. 最后才会走到 Application 中的 onCreate 函数中;


那下面就看一下 LeakCanary 是怎么自动初始化的,首先是在 AndroidManifest.xml  文件中声明:


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

    </application>

有一个需要关注的点就是,provider 的 enabled 状态是通过资源文件中的值来决定的,这就是禁用自动初始化的关键。MainProcessAppWatcherInstaller 定义如下:


internal class MainProcessAppWatcherInstaller : ContentProvider() {

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

}


可见,初始化的主要逻辑就是 AppWatcher.manualInstall(application) 函数。其定义大致如下:


@JvmOverloads
  fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5)
,
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {


appDefaultWatchers 中是默认配置关注内存泄漏的类型,支持的有 Activity、Fragment、RootView 和 Service。


手动初始化


想要对 LeakCanary 添加一些自定义的配置,就需要禁用自动初始化的逻辑,上面也有提到在资源文件中添加 leak_canary_watcher_auto_install 值即可,如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
  <bool name="leak_canary_watcher_auto_install">false</bool>
</resources>


手动初始化的时候,我们就可以根据自己的需要添加想要检测的类型,如果我们不想检测 RootView 的类型,则可以如下定义:


val watchersToInstall = AppWatcher.appDefaultWatchers(application)
  .filter { it !is RootViewWatcher }
AppWatcher.manualInstall(
  application = application,
  watchersToInstall = watchersToInstall
)

初始化的时候的确是可以做到开箱即用,对于想要延迟初始化以及自定义配置的话,也可以很方便的支持。


下面就会开始探索如何解决 LeakCanary 卡顿相关的问题。


3如何解决卡顿?


LeakCanary 造成卡顿的原因就是在主进程中 dump hprof 文件,.hprof 通常会有上百兆,整个过程至少会持续 20 秒(中位数)以上。所以在这个过程中,用户有任何繁琐的操作都会使 App 不堪重负表现卡顿,如果是性能差的老机器,什么都不操作都可能出现 ANR 的问题。


针对上述问题通过用的解决方案就是把整个 dump hprof 文件的过程放到一个单独的进程中做,这样就会尽可能少的影响主进程的操作。快手开源的 KOOM 库采用的也是这种方式,当然 LeakCanary 本身也提供了多进程的方式。


使用 leakcanary-android-process


使用时需要引入 leakcanary-android-process 模块,如下:


dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.9.1'
}


此依赖包中使用 WorkManager 来处理跨进程通讯,处理的方式也是非常巧妙,只要添加依赖就可以做到跨进程。大致思路如下:


1. 在 leakcanary-android-process 包中定义 RemoteLeakCanaryWorkerService 并在 AndroidManifest 文件中声明为单独的进程;

2. leakcanary-android-core 包中会判断 RemoteLeakCanaryWorkerService 类是否存在,如存在则使用 WorkManager 启动子进程进行 Dump 操作,否则在子线程中处理。


其中 RemoteLeakCanaryWorkerService 定义如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary">


  <application>
    <service
      android:name="leakcanary.internal.RemoteLeakCanaryWorkerService"
      android:exported="false"
      android:process=":leakcanary" />

  </application>

</manifest>


使用 WorkManager dump 内存的逻辑如下:


// EventListener 是 LeakCanary 的事件回调,这里仅仅处理了 Dump 内存的事件
object RemoteWorkManagerHeapAnalyzer : EventListener {

  private const val REMOTE_SERVICE_CLASS_NAME = "leakcanary.internal.RemoteLeakCanaryWorkerService"

  override fun onEvent(event: Event) {
    if (event is HeapDump) {
      val application = InternalLeakCanary.application
      val heapAnalysisRequest =
        OneTimeWorkRequest.Builder(RemoteHeapAnalyzerWorker::class.java).apply {
          val dataBuilder = Data.Builder()
            .putString(ARGUMENT_PACKAGE_NAME, application.packageName)
            .putString(ARGUMENT_CLASS_NAME, REMOTE_SERVICE_CLASS_NAME)
          setInputData(event.asWorkerInputData(dataBuilder))
          with(WorkManagerHeapAnalyzer) {
            addExpeditedFlag()
          }
        }.build()
      SharkLog.d { "Enqueuing heap analysis for ${event.file} on WorkManager remote worker" }
      val workManager = WorkManager.getInstance(application)
      workManager.enqueue(heapAnalysisRequest)
    }
  }
}

最终效果如下,在 dump 事件前后,打印日志的进程由 25405 变成 25426。



使用 KOOM


除了使用 LeakCanary 自带的跨进程方案之外,还可以使用  KOOM 库中的一个包 koom-fast-dump ,在 LeakCanary 的配置方式如下:


LeakCanary.config = LeakCanary.config.copy(
  heapDumper = HeapDumper {
    // 核心代码就这一行,注意此方法会等待子进程返回采集结果,不要在UI线程调用!
    ForkJvmHeapDumper.getInstance().dump(it.absolutePath)
  })

LeakCanary 默认的 dump 使用的是 Debug.dumpHprofData() ,代码如下:


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

使用 koom-fast-dump 与 LeakCanary 自带的包 leakcanary-android-process 效果是一样的,都会切换到子进程,日志如下:



小结


无论是使用 koom-fast-dump 还是 leakcanary-android-process ,都可以解决 LeakCanary dump 内存时卡顿的问题。默认情况下,使用 leakcanary-android-process 更加方便,如果是想要想要自定义 HeapDump 相关逻辑话,使用 koom-fast-dump 会相对简单一点。


通过上面的介绍可知,LeakCanary 可以通过配置 Config 来自定义 HeapDump 逻辑,除此之外还可以监听 LeakCanary 的主要事件,然后做一些我们想要的事情,比如把相关问题上传到 Crash 平台或者是质量平台上,方便从宏观的角度治理内存泄漏问题。


4如何在线上使用?


解决了卡顿问题之后,在线上使用 LeakCanary 似乎也不是那么遥不可及了,下面我们看一下如何在线上使用 LeakCanary。


想要在线上使用 LeakCanary 首要要确定以下问题:


1. 如何获取 LeakCanary 分析内存泄漏的结果?

2. 内存泄漏的结果以何种形式上报到质量平台上?

3. 如何确定合理的监控采集时机,做到尽可能小的影响用户?

监听 LeakCanary 事件


监听 LeakCanary dump 以及内存分析事件可以通过 LeakCanary.Config 进行配置,SDK 内部内置了一下监听器,如下:


object LeakCanary {
  data class Config(
        // ...

    val eventListeners: List<EventListener> = listOf(
      LogcatEventListener,
      ToastEventListener,
      LazyForwardingEventListener {
        if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
      },
      when {
          RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
            RemoteWorkManagerHeapAnalyzer
          WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
          else -> BackgroundThreadHeapAnalyzer
      }
    ),
  ) {
    }
}


可以看出,我们在控制台看到的日志打印(LogcatEventListener)、App中的通知提醒(NotificationEventListener)等逻辑都是在此处配置的。包括上面提到使用子进程 dump 内存的逻辑就是在 RemoteWorkManagerHeapAnalyzer 内部实现的。


我们想要获得对应的分析结果也需要通过此方式。我们通过实现 EventListener 接口即可获取对接的结果,实现大致如下:


private class RecordToService : EventListener {

  /**
   * SDK 内部事件回调,可以在此处过滤出内存泄漏的结果
   */

  override fun onEvent(event: EventListener.Event) {
    if (event !is EventListener.Event.HeapAnalysisDone<*>) {
      return
    }

    if (event is EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded) {
      record(event.heapAnalysis)
    }
  }

  /**
   * 处理内存泄漏的结果
   */

  private fun record(heapAnalysis: HeapAnalysisSuccess) {
    val allLeaks = heapAnalysis.allLeaks
    // 处理结果
  }
}

事件定义好之后通过以下配置进行初始化:


class LeakCanaryConfig {
  // 初始化配置
  fun init(app: Application) {

    val eventListeners = LeakCanary.config.eventListeners.toMutableList().apply {
      // 将我们自定义的事件添加到事件列表中,也可以根据自己的需求删除一些线上不需要的事件
            add(RecordToService())
    }
    LeakCanary.config = LeakCanary.config.copy(
      eventListeners = eventListeners
    )
  }
}


到这了我们就已经能够拿到 LeakCanary 分析的内存泄漏结果了。但是这里的结果,跟我们平时使用的 Crash 上报信息并不能直接匹配,因为这里并没有直接可以使用的堆栈信息,需要我们自己进行拼接。


下面就看一下如何通过 LeakCanary 中的信息构造对应的 Throwable。


构建 Throwable


这部分基本没有什么难点,直接按照 LeakTrace 对象中的字段进行拼接即可,下面是完整的代码。


internal class LeakCanaryThrowable(private val leakTrace: LeakTrace) : Throwable() {

    override val message: String
        get() = leakTrace.leakingObject.message()

    override fun getStackTrace(): Array<StackTraceElement> {
        val stackTrace = mutableListOf<StackTraceElement>()
        stackTrace.add(StackTraceElement("GcRoot", leakTrace.gcRootType.name, "GcRoot.kt"42))
        for (cause in leakTrace.referencePath) {
            stackTrace.add(buildStackTraceElement(cause))
        }
        return stackTrace.toTypedArray()
    }

    private fun buildStackTraceElement(reference: LeakTraceReference): StackTraceElement {
        val file = reference.owningClassName.substringAfterLast(".") + ".kt"
        return StackTraceElement(reference.owningClassName, reference.referenceDisplayName, file, 0)
    }

    private fun LeakTraceObject.message(): String {
        return buildString {
            append("发现内存泄漏问题,")
            append(
                if (retainedHeapByteSize != null) {
                    val humanReadableRetainedHeapSize = humanReadableByteCount(retainedHeapByteSize!!.toLong())
                    "$className, Retaining $humanReadableRetainedHeapSize in $retainedObjectCount objects."
                } else {
                    className
                }
            )
        }
    }

    private fun humanReadableByteCount(bytes: Long): String {
        val unit = 1000
        if (bytes < unit) return "$bytes B"
        val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
        val pre = "kMGTPE"[exp - 1]
        return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
    }
}


将堆栈打印出来的效果如下:



LeakCanaryThrowable 构建后之后就可以根据自己团队使用的 Crash 上报 SDK 进行上传了。


调整监控策略


到目前为止 LeakCanary 虽然可以在子进程 dump内存并且分析结果了,但是在线上版本运行多少对性能还是有些影响的。为了尽可能减少这些影响,就需要调整 LeakCanary 监控的时机了,尽量是在用户不使用当前 App 的时候进行处理。


可能的场景就是 App 切到后台或者是手机息屏时才开始处理相关的任务,LeakCanary 也提供了应该的工具包,首先需要引入 leakcanary-android-release 包,如下:


dependencies {
  // LeakCanary for releases
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-release:${leakCanaryVersion}'
}

下面就需要对之前的 LeakCanaryConfig 类进行改造了,需要添加 BackgroundTrigger 以及 ScreenOffTrigger ,这两个触发器的逻辑大致如下:


class LeakCanaryConfig {

    fun init(app: Application) {

        // App 进入后台触发器
        BackgroundTrigger(
            application = app,
            analysisClient = analysisClient,
            analysisExecutor = analysisExecutor,
            analysisCallback = analysisCallback
        ).start()

        // 手机息屏触发器
        ScreenOffTrigger(
            application = app,
            analysisClient = analysisClient,
            analysisExecutor = analysisExecutor,
            analysisCallback = analysisCallback
        ).start()
    }
}

可能会觉得就算是这样配置,也会觉得不是那么放心,其实也可以通过云端下发配置的方式来动态控制是否开启 LeakCanary 的监控功能。如下,通过 HeapAnalysisClient 自定义拦截器。


private val analysisClient by lazy {
    HeapAnalysisClient(
        heapDumpDirectoryProvider = {
            File("")
        },
        // stripHeapDump: remove all user data from hprof before analysis.
        config = HeapAnalysisConfig(stripHeapDump = true),
        // Default interceptors may cancel analysis for several other reasons.
        interceptors = listOf(flagInterceptor) + HeapAnalysisClient.defaultInterceptors(app)
    )
}

private val flagInterceptor = object : HeapAnalysisInterceptor {

    override fun intercept(chain: HeapAnalysisInterceptor.Chain): HeapAnalysisJob.Result {
        // 通过开关控制任务是否进行
        if(enable) {
            chain.job.cancel("cancel reason")
        }
        return chain.proceed()
    }
}


除了我们上面自定义的拦截器之外,SDK内部还预制了一些极端情况的场景,如下:


fun defaultInterceptors(application: Application): List<HeapAnalysisInterceptor> {
  return listOf(
    // 仅支持特定 Android 版本
    GoodAndroidVersionInterceptor(),
    // 存储空间太小也不支持
    MinimumDiskSpaceInterceptor(application),
    // 可用内存太小也不支持
    MinimumMemoryInterceptor(application),
    MinimumElapsedSinceStartInterceptor(),
    OncePerPeriodInterceptor(application),
    SaveResourceIdsInterceptor(application.resources)
  )
}

有了上述逻辑的综合加持,在线上版本中使用 LeakCanary 的影响范围可能并没有现象中的大。当然 LeakCanary 官方对这部分内容还是持谨慎态度的,leakcanary-android-release 本身还是处于试验阶段。


当然如果有内测渠道,可以先在内测的版本中跑起来。


小结


其实 leakcanary-androidleakcanary-android-release 两个包的依赖图大致如下:


+--- project :leakcanary-android-release
|    +--- project :shark-android
|
    |    \--- project :shark
|
    |         \--- project :shark-graph
|
    |              \--- project :shark-hprof
|
    |                   \--- project :shark-log
|
    \--- project :leakcanary-android-utils
+--- project :leakcanary-android
|    +--- project :leakcanary-android-core (*)
|
    +--- project :leakcanary-object-watcher-android
|    \--- org.jetbrains.kotlin:kotlin-stdlib
+--- project :leakcanary-android-core
|
    +--- project :shark-android
|    +--- project :leakcanary-object-watcher-android-core
|
    +--- project :leakcanary-object-watcher-android-androidx
|    \--- project :leakcanary-object-watcher-android-support-fragments


可见,:leakcanary-android-release 模块并没有依赖 :leakcanary-android ,仅有 :shark-android:leakcanary-android-utils 模块是通用的。


分析源码可以知,:leakcanary-android-release:leakcanary-android两个包在 HeapDump 以及结果处理上都有差异,leakcanary-android-release 模块也无法使用 leakcanary-android 中的多进程逻辑,因为其内部写死是使用 Debug.dumpHprofData 的。好在其触发条件比较苛刻,小范围使用影响可控。


使用 LeakCanary 采集内存泄漏的建议方式如下:


• Debug 环境


        ▪ 添加 leakcanary-android 依赖,使用默认的一些事件监听器(日志、通知),方便定位排除问题;

        ▪ 添加 leakcanary-android-process 依赖,在子进程中处理耗时任务,优化开发体验;

        ▪ 自定义事件监听器,上报对应的结果;

• Release 环境

            ▪ leakcanary-android-release 依赖,仅在一些特定的情况下触发任务,减少对用户使用的影响;

            ▪ 自定义事件监听器,上报对应的结果;


以上逻辑的代码已上传至 gist ,感兴趣的同学可以自取。

https://gist.github.com/madroidmaq/9207e24820838ccc0b1a2558d83df7ef


5总结


首先,正常在 Debug 环境中使用 LeakCanary 的确是添加一行依赖就能搞定了,包括对多进程的开启也是如此,真的算是开箱即用了。由此可见其设计功底了。


在 Release 环境使用,也有对应的方案。但是整体方案还处于实验阶段,建议控制好使用范围。一种是云端开启采样方式开启,另一种就是在内测版本中使用控制好使用范围。


回过头再来看我们之前对 LeakCanary 留下的刻板印象:


• “LeakCanary 虽好,但就是太卡。”

• “LeakCanary 虽好,但无法线上使用。”


读到这里我相信你对上面的问题已经有了自己的看法了。古云说:“士别三日,当刮目相待”,对于这些在持续更新的技术也应如此,要时刻保持开放、学习的心态。唯有如此,才有突破。




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

详解Android14 Activity 启动过程
不同版本上 Bitmap 内存分配与回收对比
别滥用FileProvider了,Android中FileProvider的各种场景应用


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存