`LeakCanary`泄漏目标推测
LeakCanary
想来也是我们的一个老朋友了,但是它是如何做到对我们的App
进行内存泄漏分析的呢?这也是我们今天要去研究的主题了。
**我们要先思考的第一个问题也就是App
中已经存在泄漏了,那我们该怎么知道他泄漏了呢?**
🤔🤔🤔,我么应该知道在JVM
中存在这样的两种关于实例是否需要回收的算法:
引用计数法
可达性分析法
引用计数法
对于 引用计数法 而言,存在一个非常致命的循环引用问题,下面我们将用图分析一下。
类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(
val enabled: Boolean = InternalAppWatcher.isDebuggableBuild,
val watchActivities: Boolean = true,
val watchFragments: Boolean = true,
val watchFragmentViews: Boolean = true,
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)
}
代码中说到会加载销毁时Activity
和Fragment
的观察者,那我们就挑选Activity
的源码来进行查看。
internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
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>()
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>()
@Synchronized fun watch(
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)
}
}
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
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
就是我们的主题了。
通过调用我们能够发现这样的一个问题,在InternalLeakCanary
的invoke
方法中就完成了一个添加操作
AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
`Heap Dump Trigger` -- 报告文件生成触发者
回到我们之前已经讲过的代码onObjectRetainedListeners.forEach { it.onObjectRetained() }
,通过深层调用我们能够发现,他其实深层的调用的就是如下的代码
override fun onObjectRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onObjectRetained()
}
}
private fun scheduleRetainedObjectCheck(
reason: String,
rescheduling: Boolean,
delayMillis: Long = 0L
) {
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects(reason)
}, delayMillis)
}
我标注了注释2的位置,他希望再进行一次对象的检查操作。
private fun checkRetainedObjects(reason: String) {
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
dumpHeap(retainedReferenceCount, retry = true)
}
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
if (retainedKeysCount == 0) {
SharkLog.d { "Check for retained object found no objects remaining" }
return true
}
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)
fun onDumpHeapReceived(forceDump: Boolean) {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onDumpHeapReceived(forceDump)
}
}
fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
dismissNoRetainedOnTapNotification()
gcTrigger.runGc()
val retainedReferenceCount = objectWatcher.retainedObjectCount
if (!forceDump && retainedReferenceCount == 0) {
return@post
}
SharkLog.d { "Dumping the heap because user requested it" }
dumpHeap(retainedReferenceCount, retry = false)
}
}
`Dump Heap` —— 报告文件的生成者
说到报告文件的生成,其实这已经不是我们主要关注的内容了,但是也值得一提。
他是通过一个服务的方式存在,完成了我们的数据分析报告的打印。其实的内部还有很多有意思的地方,透明的权限请求等等。不过这一次的hprof
的生成我不清楚还是不是用的第三方库,不过给我的感觉应该是自己造了一个。
总结
我们上面讲了很多关于源码的内容,重新梳理一下框架,其实无非就四个内容。
注意点
观察者模式
对象泄漏的观察方式:弱引用 + 引用队列
评论