锦囊篇|一文摸懂LeakCanary

微信扫一扫,分享到朋友圈

锦囊篇|一文摸懂LeakCanary

LeakCanary 想来也是我们的一个老朋友了,但是它是如何做到对我们的 App 进行内存泄漏分析的呢?这也是我们今天要去研究的主题了。

我们要先思考的第一个问题也就是 App 中已经存在泄漏了,那我们该怎么知道他泄漏了呢?

,我么应该知道在 JVM 中存在这样的两种关于实例是否需要回收的算法:

  1. 引用计数法
  2. 可达性分析法

引用计数法

对于 引用计数法 而言,存在一个非常致命的循环引用问题,下面我们将用图分析一下。

类A和类B作为一个实例,那么类A和类B的计数
0 -> 1 ,不过我们能够注意到里面还有一个叫做
Instance 的对象分别指向了对方的实例,即
类A.Instance = 类B
类B.Instance = 类A ,那么这个时候类A和类B的计数为
1 -> 2 了,即使我们做了
类A = null
类B = null 这样的操作,类A和类B的计数也只是从
2 -> 1

,并未变成0,也就导致了内存泄漏的问题。

可达性分析法

和引用计数法比较,可达性分析法多了一个叫做 GC Root 的概念,而这些 GC Roots 就是我们可达性分析法的起点,在周志明前辈的《深入理解Java虚拟机》中就已经提到过了这个概念,它主要分为几类:

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用。
  • 在本地方法栈中JNI引用的对象。
  • 在Java虚拟机栈中引用的对象,譬如 Android 的主入口类 ActivityThread
  • 所有被同步锁持有的对象。
  • 。。。。。

我们同样用上述循环引用的案例作为分析,看看可达性分析是否还会出现这样的内存泄漏问题。

这个时候
类B = null

了,那会发生什么样的状况呢?

既然
类B = null ,那么我们的
Instance 也应该等于
null ,这个时候也就少掉一根引用线,我们在上面说过了,可达性分析法的起点,这时候再从
Roots 进行可达性分析时发现类B不再存在通路到达,那类B就会被加上清理标志,等待
GC

的到来。

知道了我们的两种泄漏目标检查的方案,我们就看看在 LeakCanary 中到底是不是通过这两种方案实现?如果不是,那他的实现方式又是什么呢?

LeakCanary 使用方法

看了很多使用介绍的博客,但是我用 Version 2.X 时,发现一个问题,全都没有 LeakCanary.install(this) 这样的函数调用,后来才知道是架构重构过,实现了静默加载,不需要我们手动再去调用了。

下面是我使用的最新版本:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
复制代码

给出一个可以跑出内存泄漏的 Demo ,也就是一个单例模式,你要做的是在 Activity1 中实现往 Activity2 的跳转功能, Activity2 实例化单例,这样再进行返回后就能查看到 LeakCanary 给我们放出的内存泄漏问题了。

public class Singleton {
private Context context;
private Singleton(Context context){
this.context = context;
}
public static class Holder{
private static Singleton Instance;
public static Singleton getInstance(Context context){
if(Instance == null) {
synchronized (Singleton.class){
if (Instance == null) Instance = new Singleton(context);
}
}
return Instance;
}
}
}
复制代码

发生内存泄漏时,你要去通知栏中进行查看

点击,并等待他拉取完成之后我们就可以开始从一个叫做
Leaks

的App中进行查看了。

能看到已经判定了
instance

这个实例已经发生了泄漏,原因是什么?

因为 Activity2 已经被销毁了,但是 context 依旧被持有,导致 Activity2 无法被清理,而我们又不会再使用到,也就发生了内存泄漏。如果你把 context 修改成 context.getApplicationContext() 也就能解决这个问题了,因为单例中的 context 的周期这个时候已经修改成和 Application 一致,那么 Activity2 的清理时因为 context 不再和单例中所保存的一致,所以不会导致泄漏的发生。

LeakCanary 是如何完成任务的?

Version 1.X 来看的话,观测的加载是需要 LeakCanary.install() 这样的代码去进行调用的,那我们就从这个角度进行切入,看看 Version 2.X 将他做了怎样的重构。

通过全局搜索,我们能够定位到一个类叫做 AppWatcherInstaller ,但是有一点奇怪的事情发生,它继承了 ContentProvider 这个四大组件。

/**
* Content providers are loaded before the application class is created. [AppWatcherInstaller] is
* used to install [leakcanary.AppWatcher] on application start.
* Content providers比Application更早进行创建,这个类的存在是为了加载AppWatcher
*/
复制代码

既然时为了去加载 AppWatcher ,那我们就先去看看 AppWatcher 这尊大佛到底能干些什么事情呢?

AppWatcher —— 内存泄漏检查的发起人

截取必要代码如下:

object AppWatcher {
data class Config(
// AppWatcher时刻关注对象的使能变量
val enabled: Boolean = InternalAppWatcher.isDebuggableBuild,
// AppWatcher时刻关注销毁Activity实例的使能变量
val watchActivities: Boolean = true,
// AppWatcher时刻关注销毁Fragment实例的使能变量
val watchFragments: Boolean = true,
// AppWatcher时刻关注销毁Fragment View实例的使能变量
val watchFragmentViews: Boolean = true,
// AppWatcher时刻关注销毁ViewModel实例的使能变量
val watchViewModels: Boolean = true,
// 对于驻留对象做出汇报时间的设置
val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
) {
var config;
// 用于监测依旧存活的对象
val objectWatcher
get() = InternalAppWatcher.objectWatcher
val isInstalled
get() = InternalAppWatcher.isInstalled
}
}
复制代码

不然发现这里面唯一作出监测动作的对象也仅仅只有一个,也就是 ObjectWatcher 。那我们猜测在代码中肯定会有对 ObjectWatcher 的使用,那我们现回归到 AppWatcherInstaller 中,能够发现他重写了一个叫做 onCreate() 的方法,并且做了这样的一件事 InternalAppWatcher.install(application) ,那我们就进到里面去看看。

fun install(application: Application) {
SharkLog.logger = DefaultCanaryLog()
// 检查当前是否在主线程
checkMainThread()
if (this::application.isInitialized) {
return
}
InternalAppWatcher.application = application
val configProvider = { AppWatcher.config }
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
复制代码

代码中说到会加载销毁时 ActivityFragment 的观察者,那我们就挑选 Activity 的源码来进行查看。

internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
// 重写生命周期中的onActivityDestroyed()方法
// 说明在销毁时才开始调用监察
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
objectWatcher.watch(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
}
companion object {
fun install(
application: Application,
objectWatcher: ObjectWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
复制代码

这一个类中最关键的地方我已经写了注释,你能够看到 ObjectWatcher 这个类被第二次使用了,而且使用了一个叫做 watch() 的方法,那肯定有必要去看看 。

ObjectWatcher —— 检测的执行者

class ObjectWatcher constructor(
private val clock: Clock,
// 通过池来检查是否还有被保留的实例
private val checkRetainedExecutor: Executor,
private val isEnabled: () -> Boolean = { true }
) {
// 没有被回收的实例的监听
private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
// 通过特定的String,也就是UUID,与弱引用关联
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
// 一个引用队列
private val queue = ReferenceQueue<Any>()
// 上文中提到的被调用用来做监测的方法
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
// 删去弱可达的对象
removeWeaklyReachableObjects()
// 随机生成ID作为Key,保证了唯一性
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
// 通过activity、引用队列等构建弱引用
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) // 1-->
}
}
// 1-->
@Synchronized private fun moveToRetained(key: String) {
// 进行了第二次的删去弱可达的对象
// 用于二次确认,保证数据的正确性
removeWeaklyReachableObjects()
// 这个时候我们的数据应该需要验证是否已经为空
val retainedRef = watchedObjects[key]
// 如果数据不为空说明发生了泄漏
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() } // 2!!!
}
}
// 数据已经被加载到引用队列中
// 说明这个原有的强引用实例已经置空,并且被监测到了
// 并且这一系列操作会在标记以及gc到来前完成
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
}
复制代码

对于从上面一连串的流程分析中我们已经知道了当前的实例是否有发生泄漏, 但是存在一个问题,它是如何进行报告的产生的?

是谁对这一切进行了操作呢?

对于 LeakCanary 来说,我分析到上文代码中 注释2 的位置,知道他肯定做了事情,但是到底做了什么呢, 发出通知,生成文件这些操作呢???

Version 1.X 的源码中我们知道有这样的一个类 LeakCanary ,我们在 Version 2.X 是否还存在呢?毕竟我们上文中一直是没有提到过这样的一个类的。那就 Search 一下好了。

通过搜索发现是存在的,并且在文件的第一行写了这样的一句话

这是一个建立在 AppWatcher 上层的类, AppWatcher 会对其进行通知,让他完成堆栈的分析,那他是如何完成这项操作的呢?

下游如何通知的上游

观察 ObjectWatcher ,我们能够发现这样的一个变量,以及他的配套方法

private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
// 添加观察者
@Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
onObjectRetainedListeners.add(listener)
}
// 删除观察者
@Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
onObjectRetainedListeners.remove(listener)
}
复制代码

添加和删除方法,这和什么设计模式有点像呢???

没错了,观察者模式!!,既然 LeakCanary 是上游,那我们就把他当成观察者,而 AppWacther 就是我们的主题了。

通过调用我们能够发现这样的一个问题,在 InternalLeakCanaryinvoke 方法中就完成了一个添加操作

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
复制代码

Heap Dump Trigger — 报告文件生成触发者

回到我们之前已经讲过的代码 onObjectRetainedListeners.forEach { it.onObjectRetained() } ,通过深层调用我们能够发现,他其实深层的调用的就是如下的代码

override fun onObjectRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onObjectRetained() // 1 -->
}
}
// 1 -->
private fun scheduleRetainedObjectCheck(
reason: String,
rescheduling: Boolean,
delayMillis: Long = 0L
) {
// 一些打印。。。。
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects(reason) // 2-->
}, delayMillis)
}
复制代码

我标注了 注释2 的位置,他希望再进行一次对象的检查操作。

private fun checkRetainedObjects(reason: String) {
var retainedReferenceCount = objectWatcher.retainedObjectCount
// 再进行了一次GC操作
// 这是为了保证我们的数据不存在因为GC没有来临而强行被进行了计算的一个保证操作
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return // 1 -->
// 。。。。
dumpHeap(retainedReferenceCount, retry = true) // 其实这是最后进入的输出泄漏文件的位置
}
// 1 -->
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
// 如果发现经过GC之后实例全部已经清除,直接返回
if (retainedKeysCount == 0) {
SharkLog.d { "Check for retained object found no objects remaining" }
return true
}
// 泄漏的数量小于5时,会发出通知栏进行提醒
if (retainedKeysCount < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
if (countChanged) {
onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
}
showRetainedCountNotification() // 发出通知
scheduleRetainedObjectCheck() // 再次进行检查
return true
}
}
return false
}
复制代码

当然 LeakCanary 也有着自己去强行进行文件生成的方案,其实这个方案我们也经常用到,就是他的泄漏数据还没有满出到设定的值,但是我们已经想去打印报告了,也就是直接去点击了通知栏要求打印出分析报告。

fun dumpHeap() = InternalLeakCanary.onDumpHeapReceived(forceDump = true) // 1 -->
// 1 -->
fun onDumpHeapReceived(forceDump: Boolean) {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onDumpHeapReceived(forceDump) // 2 -->
}
}
fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
dismissNoRetainedOnTapNotification()
// 和前面一样会做一次数据的保证操作
gcTrigger.runGc()
val retainedReferenceCount = objectWatcher.retainedObjectCount
// 不强制打印,且泄漏数量为0 时才能不打印
if (!forceDump && retainedReferenceCount == 0) {
// ......
return@post
}
SharkLog.d { "Dumping the heap because user requested it" }
dumpHeap(retainedReferenceCount, retry = false) // 完成分析报告数据打印
}
}
复制代码

Dump Heap —— 报告文件的生成者

说到报告文件的生成,其实这已经不是我们主要关注的内容了,但是也值得一提。

他是通过一个服务的方式存在,完成了我们的数据分析报告的打印。其实的内部还有很多有意思的地方,透明的权限请求等等。不过这一次的
hprof

的生成我不清楚还是不是用的第三方库,不过给我的感觉应该是自己造了一个。

微信扫一扫,分享到朋友圈

锦囊篇|一文摸懂LeakCanary

用斗地主的实例学会使用java Collections工具类

上一篇

“亲密关系”才是社群运营的终局,拉群卖货不是 | 超级沙龙

下一篇

你也可能喜欢

锦囊篇|一文摸懂LeakCanary

长按储存图像,分享给朋友