/ 今日科技快讯 /
7月2日,三星电子公司首席执行官高东真声称,他对可折叠智能手机Galaxy Fold的过早推出负责。目前,这款可折叠智能手机仍在等待重新推出的日期。
/ 作者简介 /
本篇文章来自萌新彭小铭的投稿,分享了他从源码的角度对LeakCanray工作原理的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
彭小铭的博客地址:
https://juejin.im/user/58218f26a22b9d0067e1593f
/ 原理 /
Activity onDestroy之后将它放在一个WeakReference。
这个WeakReference关联到一个ReferenceQueue。
查看ReferenceQueue是否存在Activity的引用。
如果该Activity泄露了,Dump出heap信息,然后再去分析泄露路径。
知识要点
软引用&弱引用
软引用(SoftReference)和弱引用(WeakReference)都继承Reference。
软引用:当一个对象只有软引用存在时,系统内存不足时会被gc回收。
弱引用:当一个对象只有弱引用存在时,随时被gc回收。
对象被回收后,Java虚拟机就会把这个引用加入到与之关联的引用队列中。
简单实例:
运行结果(注意内存地址是由JVM分配的,故可能有所差异):
pollRef的内存地址:java.lang.ref.WeakReference@610455d6
pollRef等于weakReference?:true
Java垃圾回收(GC)
在Java中垃圾判断方法是可达性分析算法,这个算法的基本思路是通过一系列的"GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Root没有任何引用链相连时,则证明此对象是不可用的。
GC Root的对象包括以下几种:
虚拟机栈中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
就算一个对象,通过可达性分析算法分析后,发现其是『不可达』的,也并不是非回收不可的。
一般情况下,要宣告一个对象死亡,至少要经过两次标记过程:
1、经过可达性分析后,一个对象并没有与GC Root关联的引用链,将会被第一次标记和筛选。筛选条件是此对象有没有必要执行finalize()方法。如果对象没有覆盖finalize()方法,或者已经执行过了。那就认为他可以回收了。如果有必要执行finalize()方法,那么将会把这个对象放置到F-Queue的队列中,等待执行。
2、虚拟机会建立一个低优先级的Finalizer线程执行F-Queue里面的对象的finalize()方法。如果对象在finalize()方法中可以『拯救』自己,那么将不会被回收,否则,他将被移入一个即将被回收的ReferenceQ
/ 源码分析 /
首先在gradle引入依赖
Application启动时注册Activity生命周期监听
LeakCanary在Application初始化,代码如下:
首先调用isInAnalyzerProcess()来判断当前进程是否为HeapAnalyzerService运行的进程。这个方法回调用LeakCanaryInternals.isInServiceProcess()通过PackageManager、ActivityManager以及android.os.Process来判断当前进程是否为HeapAnalyzerService运行的进程,这样子做的目的是不影响主进程的使用。
下面是debug生成的AndroidManifest.xml,可以在run应用之后再app/build/intermediates/instant_app_manifest/debug/查看。
注意上面关于LeakCanary的组件的android:enabled=false,android:enabled表示是否能够实例化该应用程序的组件,如果为true,每个组件的enabled属性决定那个组件是否可以被 enabled。如果为false,它覆盖组件指定的值;所有组件都是disabled。
这里回过来看install()方法,它调用返回RefWatcher对象,这个对象通过Application注册了Activity的生命周期监听、通过Activity注册监听Fragment的生命周期,且用到了leakcanary-support-fragment包,兼容了v4的fragment。
值得注意的是LeakCanary.refWatcher(application)返回的是一个AndroidRefWatcherBuilder对象,下面看看它的buildAndInstall():
首先判断refWatcher是否已被初始,build()创建RefWatcher对象,然后判断这对象是不是RefWatch是否可用,若可用则执行下面操作:
检测DisplayLeakActivity是否可用,若不可用则调用LeakCanaryInternals.setEnabledAsync()调用AsyncTask.THREAD_POOL_EXECUTOR这个静态异步线程池执行PackageManager.setComponentEnabledSetting()将这个Activity设置为可用。PackageManager.setComponentEnabledSetting()是IPC的阻塞操作,故作异步处理。
判断是否开启Activity内存泄露检测,若没则调用ActivityRefWatcher.install()会创建ActivityRefWatcher对象然后通过Application注册Activity的生命周期监听。
判断是否开启Fragment内存泄露检测,若没则调用FragmentRefWatcher.Helper.install(),通过Activity注册监听Fragment的生命周期,且用到了leakcanary-support-fragment包,兼容了v4的fragment。
监听Activity/Fragment的销毁
上面提到Activity和Fragment的生命周期监听,这里首先看看监听Activity的代码:
上面通过Application注册了ActivityRefWatcher成员lifecycleCallbacks监听Activity生命周期回调,lifecycleCallbacks是继承Application.ActivityLifecycleCallbacks的抽想类,这样就完成了Activity销毁时监听监听回调,并执行Activity内存泄露检测操作。下面看看它的代码实现:
现在再来看看监听Fragment的代码:
上面代码实现和监听Activity生命周期有所差异,首先创建FragmentRefWatcher的容器,判断SDK版本是否大于等于Android O,若大于等于则创建AndroidOFragmentRefWatche加入容器,然后在通过反射创建SupportFragmentRefWatcher也加入到容器中,之后创建Helper并通过Application注册了Helper成员activityLifecycleCallbacks监听Activity的生命周期,但它仅监听Activity的创建,下面来看看它的代码:
通过上面的代码可以知道,这个activityLifecycleCallbacks在Activity创建时,遍历之前FragmentRefWatcher的list并调用实例中的watchFragments(),list只有两个对象:SupportFragmentRefWatcher(兼容android O以下)和AndroidOFragmentRefWatcher(兼容android O+引入了fragment的生命周期,用户不需要在onDestroy中自行调用),它们两实现差不多,这里只看SupportFragmentRefWatcher代码:
到watchFragments()这里发现这才和监听Activity生命周期相似,通过Activity获取FragmentManager注册成员FragmentLifecycleCallbacks监听Fragment销毁生命周期,然后进行内存泄露检测操作。
检测对象弱引用关联引用队列
在之前得知在Activity和Fragment销毁时都会拿调用RefWatch.watch()方法,在此之前先了解一下RefWatch对象:
上面代码让我们有个大概认识,这小节只关注retainedKeys和queue成员变量就够了,其他成员变量会在之后分析到,我们先看看RefWatch.watch():
首先会判断当前RefWatch是否可用,然后对检测对象和对应的引用标识字符串判空,生成检查泄露开始时间,接下来为引用生成唯一的key并添加到retainedKeys,然后创建KeyedWeakReference对象,开启异步线程分析弱引用。这里看看KeyedWeakReference代码:
从上面可以看出KeyedWeakReference封装了引用唯一的key和引用标识字符串,并将检测对象的弱引用关联到RefWatch的引用队列。
等待主线程空闲
从上一小节看到RefWatch.ensureGoneAsync(),下面看看代码实现:
有上面代码看出将泄露检测转移到WatchExecutor,而它的实现是AndroidWatchExecutor,接下来看看它的代码:
AndroidWatchExecutor的构造方法会创建主线程和和后台子线的Handler,并初始化切换到子线程Handler的最大延迟时间maxBackoffFactor和初始化时间initialDelayMillis,maxBackoffFactor值为Long.MAX_VALUE / initialDelayMillis,initialDelayMillis值为5(因为AndroidWatchExecutor是被AndroidRefWatcherBuilder.defaultWatchExecutor()创建)。
execute()里的无论waitForIdle()还是postWaitForIdle(),都是需要切换到主线程执行,而且postWaitForIdle()最终切换到waitForIdle()。
waitForIdle()监听主线程Handler消息队列空闲,只要主线程空闲就会执行postToBackgroundWithDelay()操作。
postToBackgroundWithDelay()会就首先根据重试次数计算延迟执行的时间,然后延迟切换到子线程的Handler操作,如果Retryable.run()返回Result.RETRY时,会执行postWaitForIdle()继续等待主线程再次空闲。
Retryable.run()在之前RefWatcher.ensureGoneAsync()被调用,而Retryable.run()的返回值由RefWatcher.ensureGoneAsync()返回,ensureGoneAsync()在以下的情况会返回Result.RETRY:
debug模式启动时。
创建dumpHeap文件失败时。
5s后UI线程未空闲时。
泄露判断
RefWatch.ensureGone()主要是判断内存泄露,下面看看它的实现:
上面代码执行如下:
在之前的RefWatch.watch()里生成了检测引用对象的UUID的key并关联了弱引用KeyedWeakReference对象,弱引用与ReferenceQueue联合使用,如果弱引用关联的对象被回收,则会把这个弱引用加入到ReferenceQueue中。
removeWeaklyReachableReferences()尝试移除已经到达引用队列的弱引用,会对应删除KeyedWeakReference的数据。如果这个引用继续存在,那么就说明没有被回收。
gone()查看retainedKeys是否包含KeyedWeakReference的唯一key。
手动触发GC操作,gcTrigger中封装了gc操作的代码,首先会调用Runtime.getRuntime().gc()以触发系统gc操作,然后当前后台子线程sleep 100毫秒,最后调用System.runFinalization()强制系统回收没有引用的队形,这样子确保引用对象是否真的被回收了。因为在dump内存信息之前提示内存泄露的时候,希望系统经过充分gc垃圾回收,而不存在任何的误判,这是对leakcanary容错性的考虑。
再次移除不可达引用,如果引用存在了,都没有被回收则判定内存泄露。
判定泄露后调用AndroidHeapDumper.dump(),首先通过LeakDirectoryProvider的实现类DefaultLeakDirectoryProvider为.prof文件创建File,若文件创建失败也会返回RETRY_LATER让之前的AndroidWatchExecutor.execute()等待下次主线程空闲执行,它最多创建7个文件,数目超过后,删除最早创建的文件,所有文件默认保存在Download文件夹下;然后利用CountDownLatch阻塞当前后台子线程5秒并监听主线程是否空闲,若不空闲则返回RETRY_LATER,若空闲则调用android.os.Debug.dumpHprofData()生成.prof文件。
调用HeapDump.Listener分析刚生成的.prof文件。
泄露信息分析
既然判断了内存泄露,那么接下来泄露信息分析,找出泄露的对象的引用路径。
ServiceHeapDumpListener是HeapDump.Listener的实现类,在RefWatch.ensureGone()中调用了它的analyze():
HeapAnalyzerService继承抽象类ForegroundService,而IntentService继承IntentService,它的runAnalysis()会回调onHandleIntent():
HeapAnalyzer的checkForLeak():是leakcannary最核心的方法
引入HAHA库(一个heap prof堆文件分析库),将hprof文件解析成内存快照Snapshot对象进行分析。
https://github.com/square/haha
deduplicateGcRoots()使用jetBrains的THashMap(THashMap的内存占用量比HashMap小)做中转,去掉snapshot中GCRoot的重复路径,以减少内存压力。
找出泄露对象并找出泄露对象的最短路劲。
HeapAnalyzer.findLeakingReference()主要作用是找出泄露对象:
在snpashot内存快照中找到泄露对象的弱引用。
遍历这个对象所有实例。
若这个key值和最开始定义封装KeyedWeakReference的key值相同,那么返回这个泄露对象。
HeapAnalyer.findLeakTract()主要作用是找到最短泄露路径,计算泄露大小作为结果反馈:
泄露信息展示
HeapAnalyzerService.onHandleIntentInForeground():
AbstractAnalysisResultService.sendResultToListener()通过调用AnalyzedHeap.save()将之前的.prof文件保存为.result文件,然后将.result文件路径通过intent传到DisplayLeakService(继承AbstractAnalysisResultService,而AbstractAnalysisResultService是继承ForegroundService)。
这里首先调用父类AbstractAnalysisResultService.onHandleIntentInForeground()通过AnalyzedHeap.load()将.result文件生成AnalyzedHeap对象,之后调用onHeapAnalyzed()将AnalyzedHeap的信息通过Notification展示。
DisplayLeakActivity是平时用到的通过桌面入口进入的泄漏信息查看Activity在AndroidRefWatcherBuilder.buildAndInstall()被开启实例化
在onResume的时候使用了LoadLeaks(实现了Runnable接口),并传入一个Provider,这个Provider就是上面创建.result文件时所用到的DefaultLeakDirectoryProvider,而在load方法主要在线程池执行是读取.result文件,然后通过UI Handler将读取的信息更新ui中。
/ 学习借鉴 /
使用DownCountLatch同步主线程和子线程。
这里使用CopyOnWriteArraySet解决并发读写问题。
构建者模式,代码简洁、清新,链式调用创建对象,参考RefWatcherBuilder对象。
MessageQueue.addIdleHandler(IdleHandler handler),监听线程空闲。
手动GC,参考GCTrigger.runGc()。
Reference.watch()本质上是可以监控任意对象类型的,关键在于监控的时机,像activity、service、fragmen是有生命周期的,可以在ondestroy时开始监控,其他的对象类型用户可以选择合适的时机调用该方法进行监控。注意如果首页的Activity一直不销毁(onDestroy)那么将一直无法检测到首页的调用栈的内存泄漏。
借助AsyncTask.THREAD_POOL_EXECUTOR静态线程池执行异步任务。
/ 总结 /
LeakCanary的源码设计非常精妙,由于本人水平有限仅给各位提供参考,希望能够抛砖引玉。
推荐阅读:
Kotlin协程入门学习,看这一篇就足够了
View的进阶,自定义一款自带动画的雷达图
原来这些设计模式的理念都是相同的
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注