前语

众所周知,Square 出品的内存走漏检测工具 LeakCanary 能够很便利的检测出 App 中存在的内存走漏问题。当咱们决议要不要在项目中引入 LeakCanary 的时分,常常也会听到声音:

  • “LeakCanary 接入简略,无需手动初始化。”
  • “LeakCanary 虽好,但便是太卡。”
  • “LeakCanary 虽好,但无法线上运用。”

一度我也是这么以为的,直到我认真研究了下才发现,现实或许并没有那么简略。本文便是测验从 LeakCanary 的一些高档用法,来从头论证上述的观念。 文末会附上完好代码,可直接运用。

想要运用 LeakCanary 的一些高档用法,首要便是需求咱们主动把握 LeakCanary 的初始化机遇,增加一些自界说的装备,下面就看一下怎么手动初始化 LeakCanary ?

怎么手动初始化 LeakCanary ?

正常情况下,咱们只需增加下面一行代码,就能够在 App 中运用 LeakCanary 了。

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

主动初始化

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

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

  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 中是默许装备重视内存走漏的类型,支撑的有 ActivityFragmentRootViewService

手动初始化

想要对 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 卡顿相关的问题。

怎么处理卡顿?

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

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

运用 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 效果是相同的,都会切换到子进程,日志如下:

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

小结

无论是运用koom-fast-dump 还是leakcanary-android-process,都能够处理 LeakCanary 卡顿的问题。

koom-fast-dump 是将 dump 内存(生成 .hprof 文件)的逻辑放到了子进程,而 leakcanary-android-process 是将剖析 .hprof 文件放到了子进程。两者能够配合运用。

感谢 @彭旭锐 指出不足地方。以下是原文: 无论是运用koom-fast-dump 还是leakcanary-android-process,都能够处理 LeakCanary dump 内存时卡顿的问题。默许情况下,运用leakcanary-android-process更加便利,假如是想要想要自界说HeapDump相关逻辑话,运用koom-fast-dump会相对简略一点。

经过上面的介绍可知,LeakCanary 能够经过装备 Config 来自界说 HeapDump 逻辑,除此之外还能够监听 LeakCanary 的首要工作,然后做一些咱们想要的工作,比如把相关问题上传到 Crash 渠道或者是质量渠道上,便利从宏观的角度治理内存走漏问题。

怎么在线上运用?

处理了卡顿问题之后,在线上运用 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)
    }
}

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

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

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 ,感兴趣的同学能够自取。

总结

首要,正常在 Debug 环境中运用 LeakCanary 的确是增加一行依靠就能搞定了,包含对多进程的敞开也是如此,真的算是开箱即用了。由此可见其设计功底了。

在 Release 环境运用,也有对应的计划。但是全体计划还处于试验阶段,建议操控好运用规模。一种是云端敞开采样方法敞开,另一种便是在内测版别中运用操控好运用规模。

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

  • “LeakCanary 虽好,但便是太卡。”
  • “LeakCanary 虽好,但无法线上运用。”

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