锦囊篇|一文摸懂LeakCanary

LeakCanary走漏目标推测

LeakCanary想来也是咱们的一个老朋友了,可是它是怎么做到对咱们的App进行内存走漏剖析的呢?这也是咱们今天要去研究的主题了。

咱们要先5 U N y b 1思考的榜首个问题也便是App中现已存在走漏了,那咱们该怎样知道他走漏了K D $ q k呢?

,我么应该知道在JVMh ^ * K h { C r S中存在这样的两种关于实例是否需要收回的算法:

  1. 引证计数法
  2. 可达Z Q – ] s 0 H V性剖析法

引证计数法

关于 引证计数法 而言,存在一个十分丧命的循环引证问题,下面咱们将用图剖析一下。

锦囊篇|一文摸懂LeakCanary

类A和类B作为一个实例,那么类A和类B的计数0 -0 b 1 6 g ] Q b t> 1,不过咱们可以留意到里边还有一个叫做Instance的目标别离指向了对方的实例,即类A.Instancec 0 O L ? 1 ; - = 类B类B.Instance = 类A,那么这个时分类A和类B的计数为1 -> 2了,即便咱们做了类A= U S ` @ = null类B = null这样的操作,类; 6 @ u i &A和类B的计数也只是从2 -> 1,并未变成0,也就导致了内存走漏的问题q z m ]

可达性剖析法

和引证计数法比较,可达性剖析法多了一个叫做GCP [ X N i [ - X RW M q K ] (oot的概念,而这些GC Roots便是咱们可达性剖析法的起点,在周志明前辈的《深入理解Java虚拟机》中就现已说到过了这个概念,它首要分为几类:

  • 在办法区中类静态属性引证的目标,比如Java类的引证类型静态变量。
  • 在办法区中常量引证的目标,比如字符串常量r 1 . O n I ]池里的引证。
  • 在本地办法栈中JNI引证的目标。
  • 在Java虚拟机栈中引证的目标,比如Android的主进口类ActivityThread
  • 所有被同步锁持有的目标。
  • 。。。。。

咱们同样用上述循环引证的事例作为剖析,看看可达性剖析是否还会呈现这样的内存走漏问题。

锦囊篇|一文摸懂LeakCanary

这个时分类B = null了,那会发作什么样的情况呢?

锦囊篇|一文摸懂LeakCanary

已然类B = null,那么咱们的Instance也应该等于null,这个时分也就少掉一根引证线,咱们在上面说过了,可达性剖析D I P D & Z { Q q法的起点,这时分再从Roots进行可达性剖析时发现类B不再存在通路到4 N C达,那类B就会被加上整理标志e ~ 4 = r d / k 6,等候GC的到来。

知道了咱们的两种走漏目标查看的计划,咱们就看看在LeakCanary中究竟是不是经过这两种计划完结?J ( b T E假如不p [ ~ O % B是,那他的完 f ] +结办法又是什么呢?

L% o 1 A V ~eakCanary使用办法

% X g了许多使用介绍的博客,可是我用Version 2.X时,发现一个问题,全都没有LeakCanary.install(this)这样的函数调用8 T ~ * :,后来才知道是架构重构过,完结了静默j q R p ^ f v 9加载,不需要咱们手动再去调用了。

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

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'

给出一个可以跑出内存走漏的Demo,也5 o & 8 5便是一个单例形式,你要做的是在Activity1中完结往Activity2的跳转功用,Activu T # / 0ity2实例化单例,这样再进行回来后就能查看到LeakCanary给咱们放出的内存走漏问! 6 N题了。

public class Si . v k N l Ningleton {
privatz 8 d  !e Context context;
private Singleton(Context context){
this.context = context;
}
public static class Holder{
private statiL k X y xc Singleton Instance;
public static Sinz * 4 V ugleton getInstance! K @ c a c ! L(Context context){
ifH 9 T(Instance == null) {G w w & f i ] d
synchronized (Singleton.class){
if (Instance == null) Instance = new Singleton(context);
}
}
return Instance;
}
}
}

发作内存走漏时,你要去告诉栏中s – ) c进行查看3 5 Z 1 #

锦囊篇|一文摸懂LeakCanary

点击,并等候他拉取完结之后咱们就可以开端从一个叫做Leaks的App中进行查看了。

锦囊篇|一文摸懂LeakCanary

能看到现已判定了instance这个实9 J _ u o a例现已发作了走漏,原因是什么?

由于Activity2现已被毁掉了,可是context依旧被持有,导致Activity2无法被整理,而咱们又不会. s G + $再使用到,也就发作了内存走漏。假如你把context修改成context.getAppli@ 6 , T ? acationContext()也就能处理这个问题了,由于单例中的context的周期这个时分现已修改成和Application共同,那么Activix J ^ { 3ty2的整理. { o G h @ K |时由于context不再和单例中所保存的共同,所以不会导致走漏的发作。

LeakCanary是怎么完结任务的?S U Q e v 8 s

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

经过大局查找,咱们可以定位到一个类叫做AppWaV Z F Z A # |tcherInstaller,可是有一点古怪的工作发作| G 6 % x,它承继了ContentProvider这个四大组件。

/**
* Content providers are loaded before the application class is created. [AppWatcherInstallerh y 9 ; X k M | :] is
*. i I B j S used to install [leakcanary.AppWatcher] on application start.
* Content providers比Application更早进行创建,这个类的存在是为了加载AppWatcher
*/

已然时为了去) Q p # ( S k加载AppWatcher,那咱们就先去看看AppWatcherq % & s Y F尊大佛究竟能干些什么% R Z6 + K v [ = H {作呢?

AppW) 1 P 6 H %atcher —— 内存走漏查看的发起人

截取必要代码如下:

object AppWatcher {
data class Config(
// AppWatcher时刻重视目标的使能变量
val enabled: Boolean = InternalAppWaV V ] Otcher.ise [ Y DebuggableBuild,
// AppWatG d b o %cher时刻重视毁掉Activity实例G r w .的使能变量
val watO M # x + 6chActivities: Boolean = true,
// AppWatcher时刻重视毁掉R E & ) aFragment实例的使能变量
val watchFragments: Boolean = true,
// AppWaN S j O Utcher时刻重视毁掉Fragment View实例的使能变量
val watchFragmentViews: Boolean = true,
//q q R ` j o $ AppWatcher时刻K E B t重视毁掉View? [ : 4 T J ] FModel实例的使能变量
val watchViewModels: Boolean = truP @ U K |e,
// 关于驻留目标做出陈述时刻的设置
val waty g - J 7 #chDurationMillisU N : b N + x Q: Long = TimeUnit.SECONDS.toMillis(5)
) {
var config;
// 用于监测依旧存活的目{ _ | s k d
val objectW& S 5 _atcher
get() = InternalAppWatcher.objectWatcher
val isInstalled
get() = InternalAppWatcher.isInstalled
}
}

否则发现这里边唯一作出监测动作的目标M s ( @ 1 v也仅仅只要一个,也便是ObjectWatcher。那咱们猜测在代码Q U B ` F中必定会有对ObjectWatcher的使用,那咱们现, o ) Q F回归到AppWaW b @ ~ 1t) = e vcherInsta? S b & ;ller中,可以发现他重写了一个叫做onCreate()的办法,而且做了这样的一件事InternalAppWatcher.insN , k 0 W _ X $ Dtall(applip 6 ? 5cation),那咱们就进到里边去看看。

fun installE r Q Z p(application: Application) {
SharkLog.logger = DefaultCanaryLog()
// 查看当前是否在主线程
checkMainThreadY G c `()
if (this::appliR D d m S % G [cation.isInitialized) {
return
}
InternalAppWatcher.applie N - 4 F O o 2cation = applic8 T & Qation
val configProvidv ~ ^ t Q b 1 % ser = { AppWatcher.co2 b Y d Snfig }
ActivityDestrd ( K =oyWatcher.install(applicatx N : 6 + T @ 9 ion, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configA ; HProvider)
onAppWatcherInstalled(application)
}

代码中说到会加载毁掉时ActivityFragment的调查者,那咱们就选择Activity的源码来进行查看。

internal class Ac_ { /tivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCalD S y o ?lbacks =
object : Application.ActivityLifecycleCallbacks by noOpDeS ( 7 9 h M wlegat@ ) $ I J e U de()w j 7 S {
// 重写生命周期中的onActivityDestroyed()办法
// 阐明在毁掉时e 1 O w O N才开端% W _ ` ~ Q调用督查
over% S G I 7 ` | S Nride fun onActivityDestroyZ # l M $ed(activitR & h Z X (y: Activity) {
if (configProvidev j | I T 7 Br().watchActivities) {
objectWatcher.w+ S ^ Latch(
activity, "${activity::class, $ :.java.G 8 Lname} received Activity#onDestroy() callback"
)
}
}
}
companion object {
fun install(
applicati3 Y ;on: Application,
objectWatcher: ObjectWatch5 { } J ] &er,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
application.registerActivityLifecycleCallbacc ) o B 0ks(activityDestroyWatcher.lifecycleCA H ! i Vallbacu l = ~ vks1 K p W ! + o /)
}
}
}

这一个类中最要害的当地我现已写了注释,你可以看到ObjectWatcherE l ) [ V k M个类被第2次使用了,而且使用了一个叫做watch()的办法,那必定有必要去看看。

ObjN I 6 X 3 8 LectWatF 5 _ & 3 % 9che D +er —— 检测的执行者

class ObjectWatcher constructor(
privats ^ i k p S % ~e val clock: Clock,
// 经过池来@ c ]查看是否还有被保存的S 5 x Z F  f实例
private val chL ~ ~ 8eckRetainedExecutor: Executor,6 q %  M *
private val isEnabA O `led: () -> Booleanr 5 * = { true }
) {
// 没有被收回的实例的监听
pP ; s I )rivate val onObjectRetainedListeners = mutableSetOf<OnObje7 ( a v ^ sctRetainedListener>()F = Q T Q P @ Q z
// 经过j w N `特定的String,也便是UUID,与弱引证关联
private val wc d M & A jatchedObjects = mutableMapOf<String, KeyedWeakReference>()
// 一个引证I L ; 4行列
private val queue = ReferenceQueue<Any>()
// 上文中说到的被调用用来做监测的办法
@Synchronized fun watch(
watchedObject:t  | M x Any,
des[ t F - = ~ B b Wcriptiob / ; M q zn: String
) {
if (!isEnabled()) {
return
}
// 删去弱可达的目标
removeWeaklyReachableObjects()
// 随机生成ID作为Key,确保了唯一性
val key = UUID.randomUUID()
.toString()
val watchUptim) h ] E DeMillis = clock.uptimeMillis()
// 经过activity、引证行列等构建弱引证
val r] Q ( u } , & t 4eference =
KeyedWeakb e o + EReference(watchedi s c ; R l ? N OObject, key, description, watchUptimeMillis, queue)
SharkLogR , | $ N J 3 7.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.jaE ] K - p 3 2 .vaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}
watchedObjects[key] = reference
// 一个十分赞的规划,加入了线程池来异步处理
checkRetainedExeS $ , - h ^cutor.execute {
moveToRetained(key) // 1-5 , ` h->
}
}
// 1--&: 7 T { o f x I jgt;
@Synchronized private fun moveToRe: } h m b W u 1 ftained(key: String) {
// 进行了第j P = ? l V y ( R2次的删去弱可达的目标
// 用于二次确认,确保数据的正确性
ref 4 ` pmoveWeaklyReachabl] K : C . 4eObjects()
// 这个时分咱~ H % u U 7们的数据应该需要验证是否现已为空
val retainedRef = watchedObjects[key]
// 假如数据不为空阐= y e } w明发作了走漏
if (retainedRef != nullU B C : Z r +) {
retainedRef.retainedUptimeMillis = clock.uptimeU g Z ~  P ) *Millis()
on; . F E } _ =ObjectRetainedLisO W B 7teners.forEach { it.onObjectRetained() }] w ^ w r j 4 // 2!!!
}
}
// 数据现已被加载到引: M [ m证行列中
// 阐明这个原有的强引证实例现已置空,而且被监测到了
// 而且这一系列操作会在. L z P标记. ] u @ c Q V V以及gc到来前完结
private fun remo~ o 3 9 ) p [veWeaklyReachableObjeC C } i %cts() {
var res K { ? Cf: Keyedp y ) XWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjectC 7 cs.remove(ref.key)
}
} whif ? s c ~ Y ( -le (ref != nul3 / - n Jl)
}
}

关于从上面一连串的流程剖析中咱们现已知道了当前的实例是否有发作走漏,可是存在一个问题,它是怎么进行陈述的发生的?

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

关于LeakCanary来说,我剖析到上文代码中注释2 的位置,知p # n 4道他必4 ( ?定做了工作,可是究7 r B q o u I E竟做了什么呢,发出告诉,A q 5 | . ; g生成文件这些操作呢???

Version 1.X的源码中咱们知道有这样的一个类LeakCanary,咱们在Version 2.X是否还存在呢?究竟; Y . –咱们上文中一直是没有说到过这样t ( ? K t的一个类的。那1 O 2 QSearch一下好了。

经过查找发现是存在的,而且在文件的榜首行写了; F K h这样的一句话

锦囊篇|一文摸懂LeakCanary

这是一个建立在AppWatcher上层的类,AppWd ] , v Jatcher会对其进行告诉,让他完结仓库的剖析,那他是怎么完结这项操作的呢?

下流怎么告诉的上游

调查ObjectWatcher,咱们可以发现这样的一个变量,以及他的配套办法

p# _ z A O m H % Orivate val onObjectRetainedListeW + v N E 6 9 #ners = mutableSetOf<OnOb% 2 7 % % 3 yjectRetainedListener>()
// 增加调查者
@Synchg x  } Kronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
onObj] 6 ,  [ -ectRetainedListeners.add(listener)
}
// 删去调查者
@Synchronized fun remj 3 ~ 9 9 e XoveOnObjectRetainedLisg B & z A s J C (tener(listener: OnObjectRetainedListener) {
onObjectRetainedListeners.remove(listener)
}

增加和删去办法,这和什么规划形式有点像呢???

没错了,调查者形式!!,已然LeakCanary是上游,那咱们就把他当成调查= s 3 o者,而AppWacther便是咱们的主题了。

经过调用咱们可以发现这样的一个问题,在InternalLeakCanaryinvoke办法中就完结了一个增加操作

AppWatcher.obje/ / UctWao ; S 8 - ? ] y !tcher.addOnObq 3 , [ 9 F ^ ljectR6 k W d @ f 5 uetainedListener(this)

Heap Dump Trigger — 陈述文件生成触发者

回到咱们之前现已讲过的代码onObjectRetainedListen2 F ^ ? [ a .ers.forEach { it.onObjectRetained() },经过深层调用咱们可以发现,他其实深层的调用的便是如下的代码

override fun onObjectRetained(m 5 y U -) {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onObjI ` 0 SectRetained() // 1 -->
}
}
// 1 -->
private fun scheduleRetainedObjectCheck(
reason: Stringi c / ; f,
reschedu7 W & u 6ling: Boolean,
delayMillis: Long = 0L
) {
// 一些打印。。。。
backgroundHM C I ] 8 J z 1andler.postDelayed(2 / 4 Q N F C k{
checkScheduleU u g } : E _ )dAt = 0
checkH 2 ) _RetainedObjectsM [ f l ) y(reason) // 2-->
}, delayMillis)
}

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

private fun checkRetainedObjects(reason: String) {
var retainedReferenceCount = ou Q p i R 9bjectWatcher.retainedObjectCount
// 再进行了一次GB K C操作
// 这是为了确保咱们的数据不存在由于GC没有降临而强行被进行了计算的一个确保操作
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCouo p 7 U mnt, c5 ] Monfig.retainedViK ` E ( ?sibleThreshold)) return // 1 -->
// 。。。。
dumpHeap(retainedReferenceCount, retry = true) // 其实这是最终进入的Y l - !输出走漏F A V T v O + `文件的位置
}
// 1 -->
private fun chec] W ? ] VkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int
): Boolean {
val countChanged = lastDiz , z m V W &spla7 % ) 5 y 3 Q RyedRetainedObjectCount != retainedKeD M ^ = 3 NysCounts 3 k 0 Q p . 
lastDisplayedRetainedq o : $ JObjectCount = retainedKeysCount
// 假如发现经过GB # y * k / %C之后实例悉数现已清除,直接回来
if (retainedKeysCount == 0) {
SharkLog.d { "Check for retained object found no objects remaining" }
return true
}
// 走漏的数量小于5时,会发出告诉栏进行提醒
if (retainedKeysCount < retainedVisibleThreshold# = T _ ) {
if (applicatio T C ` ( Z t a _onVisible || applicati C s . $ F # yionInvisibleLessThanWatchPeriod) {
if (countChanged) {
onRetainInstanceListener.onEvent(BelowThreshold(retained. K | LKeysCount))
}
showRetainedCountNot J P u ? w H Vificatioi c d 8 ( An() // 发出告诉
scheduleRetain` : 9 K NedOb. % @jectCheck() // 再次v : m . k #进行查看
return true
}
}
return false
}

当然LeakCanary也有着自己去强行进行文件生成的计划,其实这个计划咱们也常常用到,便是他的走漏数据还没有满出到设定的值,可是咱们现已想去V – 8打印陈述了,也便是直接去点击了告诉栏要求打印出剖析陈述。

fun dumpHeap() = InternalLeakCanary.onDumpHR { + _ @ b ueapReceived(forceDump = true) // 1 -->
// 1 -->
fe l J C _un onDumpHeapReceived(forch 0 A a 2 ieDump: Boolean) {
ifM K + (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onDumpHeapReceived(forceDump) // 2 -->
}
}
fun onDumpHeapReceived(forc) X % [eDump: Boolean) {
bQ ` 7 # :ackgroundHandlerV # 1.post {
dismissNoRet{ = j } _ | + 5ainedOnTapNotification()
// 和前面一样会做一次数据的确保操作
gcTrigger.runGc()
val retainedReferenceCount = objectWatcher.retainedObjectCount
// 不强制打印,且走漏数量为0 时才干不打印
if (!forceDump &&a{ u c h h j k a Zmp; retainedRefee m 4 l k [ 7renceCount == 0) {
// ......
return@post
}
SharkLog.d { "Dumping the heap4 q i z !  R } & because user requested it" }
dumpHeap(retainedReferenceCo& H 7unt, retry = false) // 完结剖析陈述数据打印
}
}

Dump Heap —— 陈述S i L F Y – N文件的生成者

说到陈述文件的生成,其实这现已B 7 P b ! C不是咱们首要重视的内容了,可是也值得一提。

锦囊篇|一文摸懂LeakCanary

他是经过一个服务的办法存在,完结了咱们的数据剖析z 6 m陈述的打印。其实的内部还有许多有意思的当地,透明的权限请求等等。_ $ i不过这一次的hprof的生成我不清[ T : l s _ r m 4楚还是不是用的第三方库,不过给我的感觉应该是自己造了一个。

总结

咱们上面G N I 0 v U z }讲了许多关于源码的内容,从头整理一下框架,其实无非就四个内容m & y 0 h $ I ,

锦囊篇|一文摸懂LeakCanary

留意点

  1. 调查者形式
  2. 目标走漏的调查办法:弱引证 + 引证行列