带着问题学关于LeakCanary2.0的四个问题
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带着问题学关于LeakCanary2.0的四个问题相关的知识,希望对你有一定的参考价值。
好文推荐:
作者:RicardoMJiang
前言
LeakCanary
是一个简单方便的内存泄漏检测框架,相信很多同学都用过,使用起来非常方便,它有以下几个特点
1.不需要手动初始化
2.可自动检测内存泄漏并通过通知报警
3.不能用于线上
那我们自然可以提出以下几个问题
1.说一下LeakCanary
检测内存泄漏的原理与基本流程
2.LeakCanary
是如何初始化的?
3.说一下LeakCanary
是如何查找内存泄露的?
4.为什么LeakCanary
不能用于线上?
本文主要梳理LeakCanary
内存泄漏检测的主要流程并回答以上几个问题
1. LeakCanary
检测内存泄漏的原理与基本流程
1.1 内存泄漏的原理
内存泄漏的原因:不再需要的对象依然被引用,导致对象被分配的内存无法被回收。
例如:一个Activity
实例对象在调用了onDestory
方法后是不再被需要的,如果存储了一个引用Activity
对象的静态域,将导致Activity
无法被垃圾回收器回收。
引用链来自于垃圾回收器的可达性分析算法:当一个对象到GC Roots
没有任何引用链相连时,则证明此对象是不可用的。如图:
对象object5
、object6
、object7
虽然互相有关联,但是它们到 GC Roots
是不可达的,所以它们将会被判定为是可回收的对象。
在Java
语言中,可作为GC Roots
的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
1.2 LeakCanary
检测内存泄漏的基本流程
知道了内存泄漏的原理,我们可以推测到LeakCanary
的基本流程大概是怎样的
1.在页面关闭后触发检测(不再需要的对象)
2.触发GC
,然后获取仍然存在的对象,这些是可能泄漏的
3.dump heap
然后分析hprof
文件,构建可能泄漏的对象与GCRoot
间的引用链,如果存在则证明泄漏
4.存储结果并使用通知提醒用户存在泄漏
总体流程图如下所示:
- 1.
ObjectWatcher
创建了一个KeyedWeakReference
来监视对象. - 2.稍后,在后台线程中,延时检查引用是否已被清除,如果没有则触发
GC
- 3.如果引用一直没有被清除,它会
dumps the heap
到一个.hprof
文件中,然后将.hprof
文件存储到文件系统。 - 4.分析过程主要在
HeapAnalyzerService
中进行,Leakcanary2.0
中使用Shark
来解析hprof
文件。 - 5.
HeapAnalyzer
获取hprof
中的所有KeyedWeakReference
,并获取objectId
- 6.
HeapAnalyzer
计算objectId
到GC Root
的最短强引用链路径来确定是否有泄漏,然后构建导致泄漏的引用链。 - 7.将分析结果存储在数据库中,并显示泄漏通知。
这里只做一个总体的介绍,具体流程可以阅读下文
2. LeakCanary
是如何自动安装的?
LeakCanary
的使用非常方便,只需要添加依赖便可以自动初始化,这是如何实现的呢?
我们看一下源码,其实主要是通过ContentProvider
实现的
internal sealed class AppWatcherInstaller : ContentProvider() {
/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()
/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
}
复制代码
当我们启动App
时,一般启动顺序为:Application
->attachBaseContext
=====>ContentProvider
->onCreate
=====>Application
->onCreate
ContentProvider
会在Application.onCreate
前初始化,这样就调用到了LeakCanary
的初始化方法
实现了免手动初始化
2.1 跨进程初始化
注意,AppWatcherInstaller
有两个子类,MainProcess
与LeakCanaryProcess
其中默认使用MainProcess
,会在App
进程初始化
有时我们考虑到LeakCanary
比较耗内存,需要在独立进程初始化
使用leakcanary-android-process
模块的时候,会在一个新的进程中去开启LeakCanary
2.2 LeakCanary2.0
手动初始化的方法
LeakCanary
在检测内存泄漏时比较耗时,同时会打断App
操作,在不需要检测时的体验并不太好
所以虽然LeakCanary
可以自动初始化,但我们有时其实还是需要手动初始化
LeakCanary
的自动初始化可以手动关闭
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="leak_canary_watcher_auto_install">false</bool>
</resources>
复制代码
1.然后在需要初始化的时候,调用AppWatcher.manualInstall
即可
2.是否开始dump
与分析开头:LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)
3.桌面图标开头:重写R.bool.leak_canary_add_launcher_icon
或者调用LeakCanary.showLeakDisplayActivityLauncherIcon(false)
2.3 小结
LeakCanary
利用ContentProvier
进行了初始化。
ContentProvier
一般会在Application.onCreate
之前被加载,LeakCanary
在其onCreate()
方法中调用了AppWatcher.manualInstall
进行初始化
这种写法虽然方便,免去了初始化的步骤,但是可能会带来启动耗时的问题,用户不能控制初始化的时机,这也是谷歌推出StartUp
的原因
不过对于LeakCanary
这个问题并不严重,因为它只在Debug
阶段被依赖
3.LeakCanary
如何检测内存泄漏?
3.1 首先我们来看下初始化时做了什么?
当我们初始化时,调用了AppWatcher.manualInstall
,下面来看看这个方法,都安装了什么东西
@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
....
watchersToInstall.forEach {
it.install()
}
}
fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
可以看出,初始化时即安装了一些Watcher
,即在默认情况下,我们只会观察Activity
,Fragment
,RootView
,Service
这些对象是否泄漏
如果需要观察其他对象,需要手动添加并处理
3.2 LeakCanary
如何触发检测?
如上文所述,在初始化时会安装一些Watcher
,我们以ActivityWatcher
为例
class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {
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)
}
override fun uninstall() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
}
可以看到在Activity.onDestory
时,就会触发检测内存泄漏
3.3 LeakCanary
如何检测可能泄漏的对象?
从上面可以看出,Activity
关闭后会调用到ObjectWatcher.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)
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
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
可以看出
1.传入的观察对象都会被存储在watchedObjects
中
2.会为每个watchedObject
生成一个KeyedWeakReference
弱引用对象并与一个queue
关联,当对象被回收时,该弱引用对象将进入queue
当中
3.在检测过程中,我们会调用多次removeWeaklyReachableObjects
,将已回收对象从watchedObjects
中移除
4.如果watchedObjects
中没有移除对象,证明它没有被回收,那么就会调用moveToRetained
3.4 LeakCanary
触发堆快照,生成hprof
文件
moveToRetained
之后会调用到HeapDumpTrigger.checkRetainedInstances
方法
checkRetainedInstances()
方法是确定泄露的最后一个方法了。
这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump
,分析 dump
文件,找到引用链
private fun checkRetainedObjects() {
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
onRetainInstanceListener.onEvent(DumpHappenedRecently)
....
return
}
dismissRetainedCountNotification()
val visibility = if (applicationVisible) "visible" else "not visible"
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
....
heapDumper.dumpHeap()
....
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
HeapAnalyzerService.runAnalysis(
context = application,
heapDumpFile = heapDumpResult.file,
heapDumpDurationMillis = heapDumpResult.durationMillis,
heapDumpReason = reason
)
}
}
1.如果retainedObjectCount
数量大于0,则进行一次GC
,避免额外的Dump
2.默认情况下,如果retainedReferenceCount<5
,不会进行Dump
,节省资源
3.如果两次Dump
之间时间少于60s,也会直接返回,避免频繁Dump
4.调用heapDumper.dumpHeap()
进行真正的Dump
操作
5.Dump
之后,要删除已经处理过了的引用
6.调用HeapAnalyzerService.runAnalysis
对结果进行分析
3.5 LeakCanary
如何分析hprof
文件
分析hprof
文件的工作主要是在HeapAnalyzerService
类中完成的
关于Hprof
文件的解析细节,就需要牵扯到Hprof
二进制文件协议,通过阅读协议文档,hprof
的二进制文件结构大概如下:
解析流程如下所示:
简要说下流程:
1.解析文件头信息,得到解析开始位置
2.根据头信息创建Hprof
文件对象
3.构建内存索引
4.使用hprof
对象和索引构建Graph
对象
5.查找可能泄漏的对象与GCRoot
间的引用链来判断是否存在泄漏(使用广度优先算法在Graph
中查找)
Leakcanary2.0
较之前的版本最大变化是改由kotlin
实现以及开源了自己实现的hprof
解析的代码,总体的思路是根据hprof
文件的二进制协议将文件的内容解析成一个图的数据结构,然后广度遍历这个图找到最短路径,路径的起始就是GCRoot
对象,结束就是泄漏的对象
具体分析可见:Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理
3.6 泄漏结果存储与通知
结果的存储与通知主要在DefaultOnHeapAnalyzedListener
中完成
override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
SharkLog.d { "\\u200B\\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }
val db = LeaksDbHelper(application).writableDatabase
val id = HeapAnalysisTable.insert(db, heapAnalysis)
db.releaseReference()
...
if (InternalLeakCanary.formFactor == TV) {
showToast(heapAnalysis)
printIntentInfo()
} else {
showNotification(screenToShow, contentTitle)
}
}
主要做了两件事
1.存储泄漏分析结果到数据库中
2.展示通知,提醒用户去查看内存泄漏情况
4.为什么LeakCanary
不能用于线上?
理解了LeakCanary
判定对象泄漏后所做的工作后就不难知道,直接将LeakCanary
应用于线上会有如下一些问题:
1.每次内存泄漏以后,都会生成一个.hprof
文件,然后解析,并将结果写入.hprof.result
。增加手机负担,引起手机卡顿等问题。
2.多次调用GC
,可能会对线上性能产生影响
3.同样的泄漏问题,会重复生成 .hprof
文件,重复分析并写入磁盘。
4..hprof
文件较大,信息回捞成问题。
了解了这些问题,我们可以尝试提出一些解决方案:
1.可以根据手机信息来设定一个内存阈值 M
,当已使用内存小于 M
时,如果此时有内存泄漏,只将泄漏对象的信息放入内存当中保存,不生成.hprof
文件。当已使用大于 M
时,生成.hprof
文件
2.当引用链路相同时,可根据实际情况去重。
3.不直接回捞.hprof
文件,可以选择回捞分析的结果
4.可以尝试将已泄漏对象存储在数据库中,一个用户同一个泄漏只检测一次,减少对用户的影响
以上想法并没有经过实际验证,仅供读者参考
总结
当我们引入LeakCanary
后,它就会自动安装并且开始分析内存泄漏并报警
主要分为以下几步
1.自动安装
2.检测可能泄漏的对象
3.堆快照,生成hprof
文件
4.分析hprof
文件
5.对泄漏进行分类并通知
本文主要梳理了LeakCanary
的主要流程与文章开始提出的几个问题,如果对您有所帮助,欢迎点赞~
最后
笔者在面试前,从网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,进行了复习,在此分享给大家,希望能帮助到大家学习提升,如有需要参考的可以直接去我 GitHub地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。
以上是关于带着问题学关于LeakCanary2.0的四个问题的主要内容,如果未能解决你的问题,请参考以下文章