前语
众所周知,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 的加载机制来做的。简略讲大致流程如下:
- 先执行
Application
中的attachBaseContext
函数; - 然后会执行
ContentProvider
中的onCreate
函数; - 最后才会走到
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
卡顿相关的问题。
怎么处理卡顿?
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
来处理跨进程通讯,处理的方法也是非常奇妙,只需增加依靠就能够做到跨进程。大致思路如下:
- 在
leakcanary-android-process
包中界说RemoteLeakCanaryWorkerService
并在 AndroidManifest 文件中声明为单独的进程; -
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 卡顿的问题。
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 首要要确定以下问题:
- 怎么获取 LeakCanary 剖析内存走漏的成果?
- 内存走漏的成果以何种形式上签到质量渠道上?
- 怎么确定合理的监控收集机遇,做到尽或许小的影响用户?
监听 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-android
与 leakcanary-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 虽好,但无法线上运用。”
读到这儿我相信你对上面的问题已经有了自己的观点了。古云说:“士别三日,当刮目相待”,关于这些在继续更新的技能也应如此,要时间保持开放学习的心态,唯有如此,才有突破。