本文基于
leakcanary-android:2.5
1. 背景
Android開發(fā)中,內(nèi)存泄露時(shí)常有發(fā)生在,有可能是你自己寫的,也有可能是三方庫里面的.程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種特殊原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至程序崩潰等嚴(yán)重后果.本來Android內(nèi)存就吃緊,還內(nèi)存泄露的話,后果不堪設(shè)想.所以我們要盡量避免內(nèi)存泄露,一方面我們要學(xué)習(xí)哪些常見場(chǎng)景下會(huì)發(fā)生內(nèi)存泄露,一方面我們引入LeakCanary幫我們自動(dòng)檢測(cè)有內(nèi)存泄露的地方.
LeakCanary是Square公司(對(duì),又是這個(gè)公司,OkHttp和Retrofit等都是這家公司開源的)開源的一個(gè)庫,通過它我們可以在App運(yùn)行的過程中檢測(cè)內(nèi)存泄露,它把對(duì)象內(nèi)存泄露的引用鏈也給開發(fā)人員分析出來了,我們?nèi)バ迯?fù)這個(gè)內(nèi)存泄露非常方面.
ps: LeakCanary直譯過來是內(nèi)存泄露的金絲雀,關(guān)于這個(gè)名字其實(shí)有一個(gè)小故事在里面.金絲雀,美麗的鳥兒.她的歌聲不僅動(dòng)聽,還曾挽救過無數(shù)礦工的生命.17世紀(jì),英國(guó)礦井工人發(fā)現(xiàn),金絲雀對(duì)瓦斯這種氣體十分敏感.空氣中哪怕有極其微量的瓦斯,金絲雀也會(huì)停止歌唱;而當(dāng)瓦斯含量超過一定限度時(shí),雖然魯鈍的人類毫無察覺,金絲雀卻早已毒發(fā)身亡.當(dāng)時(shí)在采礦設(shè)備相對(duì)簡(jiǎn)陋的條件下,工人們每次下井都會(huì)帶上一只金絲雀作為"瓦斯檢測(cè)指標(biāo)",以便在危險(xiǎn)狀況下緊急撤離. 同樣的,LeakCanary這只"金絲雀"能非常敏感地幫我們發(fā)現(xiàn)內(nèi)存泄露,從而避免OOM的風(fēng)險(xiǎn).
2. 初始化
在引入LeakCanary的時(shí)候,只需要在build.gradle中加入下面這行配置即可:
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
That’s it, there is no code change needed! 我們不需要改動(dòng)任何的代碼,就這樣,LeakCanary就已經(jīng)引入進(jìn)來了. 那我有疑問了?我們一般引入一個(gè)庫都是在Application的onCreate中初始化,它不需要在代碼中初始化,它是如何起作用的呢?
我只想到一種方案可以實(shí)現(xiàn)這個(gè),就是它在內(nèi)部定義了一個(gè)ContentProvider,然后在ContentProvider的里面進(jìn)行的初始化.
咱驗(yàn)證一下: 引入LeakCanary之后,運(yùn)行一下項(xiàng)目,然后在debug的apk里面查看AndroidManifest文件,搜一下provider定義.果然,我找到了:
<provider
android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:enabled="@ref/0x7f040007"
android:exported="false"
android:authorities="com.xfhy.allinone.leakcanary-installer" />
<!--這里的@ref/0x7f040007對(duì)應(yīng)的是@bool/leak_canary_watcher_auto_install-->
class AppWatcherInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
}
哈哈,果然是在ContentProvider里面進(jìn)行的初始化.App在啟動(dòng)時(shí)會(huì)自動(dòng)初始化ContentProvider,也就自動(dòng)調(diào)用了AppWatcher.manualInstall()進(jìn)行了初始化.一開始的時(shí)候,我覺得這樣挺好的,挺優(yōu)雅,后來發(fā)現(xiàn)好多三方庫都這么干了.每個(gè)庫一個(gè)ContentProvider進(jìn)行初始化,有點(diǎn)冗余的感覺.后來Jetpack推出了App Startup,解決了這個(gè)問題,它就是基于這個(gè)原理進(jìn)行的封裝.
需要注意的是ContentProvider的onCreate執(zhí)行時(shí)機(jī)比Application的onCreate執(zhí)行時(shí)機(jī)還早.如果你想在其他時(shí)機(jī)進(jìn)行初始化優(yōu)化啟動(dòng)時(shí)間,也是可以的.只需要在app里重寫@bool/leak_canary_watcher_auto_install
的值為false即可.然后手動(dòng)在合適的地方調(diào)用AppWatcher.manualInstall(application)
.但是LeakCanary本來就是在debug的時(shí)候用的,所以感覺優(yōu)化啟動(dòng)時(shí)間不是那么必要.
3. 監(jiān)聽泄露的時(shí)機(jī)
LeakCanary自動(dòng)檢測(cè)以下對(duì)象的泄露:
- destroyed Activity instances
- destroyed Fragment instances
- destroyed fragment View instances
- cleared ViewModel instances
可以看到,檢測(cè)的都是些Android開發(fā)中容易被泄露的東西.那么它是如何檢測(cè)的,下面我們來分析一下
3.1 Activity
通過Application#registerActivityLifecycleCallbacks()注冊(cè)Activity生命周期監(jiān)聽,然后在onActivityDestroyed()中進(jìn)行objectWatcher.watch(activity,....)
進(jìn)行檢測(cè)對(duì)象是否泄露.檢測(cè)對(duì)象是否泄露這塊后面單獨(dú)分析.
3.2 Fragment逛球、Fragment View
同樣的,檢測(cè)這2個(gè)也是需要監(jiān)聽周期,不過這次監(jiān)聽的是Fragment的生命周期,利用fragmentManager.registerFragmentLifecycleCallbacks
可以實(shí)現(xiàn).Fragment是在onFragmentDestroy()中檢測(cè)Fragment對(duì)象是否泄露,Fragment View在onFragmentViewDestroyed()里面檢測(cè)Fragment View對(duì)象是否泄露.
但是,拿到這個(gè)fragmentManager的過程有點(diǎn)曲折.
- Android O以上,通過activity#getFragmentManager()獲得. (AndroidOFragmentDestroyWatcher)
- AndroidX中,通過activity#getSupportFragmentManager()獲得. (AndroidXFragmentDestroyWatcher)
- support包中,通過activity#getSupportFragmentManager()獲得. (AndroidSupportFragmentDestroyWatcher)
可以看到,不同的場(chǎng)景下,取FragmentManager的方式是不同的.取FragmentManager的實(shí)現(xiàn)過程稻爬、注冊(cè)Fragment生命周期、在onFragmentDestroyed和onFragmentViewDestroyed中檢測(cè)對(duì)象是否有泄漏這一套邏輯,在不同的環(huán)境下,實(shí)現(xiàn)不同.所以把它們封裝進(jìn)不同的策略(對(duì)應(yīng)著上面3種策略)中,這就是策略模式的應(yīng)用.
因?yàn)樯厦娅@取FragmentManager需要Activity實(shí)例,所以這里還需要監(jiān)聽Activity生命周期,在onActivityCreated()中拿到Activity實(shí)例,從而拿到FragmentManager去監(jiān)聽Fragment生命周期.
//AndroidOFragmentDestroyWatcher.kt
override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
objectWatcher.watch(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}
override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
if (configProvider().watchFragments) {
objectWatcher.watch(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}
3.3 ViewModel
在前面講到的AndroidXFragmentDestroyWatcher中還會(huì)單獨(dú)監(jiān)聽onFragmentCreated()
override fun onFragmentCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
ViewModelClearedWatcher.install(fragment, objectWatcher, configProvider)
}
install里面實(shí)際是通過fragment和ViewModelProvider生成一個(gè)ViewModelClearedWatcher,這是一個(gè)新的ViewModel,然后在這個(gè)ViewModel的onCleared()里面檢測(cè)這個(gè)fragment里面的每個(gè)ViewModel是否存在泄漏
//ViewModelClearedWatcher.kt
init {
// We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,
// however that was added in 2.1.0 and we support AndroidX first stable release. viewmodel-2.0.0
// does not have ViewModelStore#keys. All versions currently have the mMap field.
//通過反射拿到該fragment的所有ViewModel
viewModelMap = try {
val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
mMapField.isAccessible = true
@Suppress("UNCHECKED_CAST")
mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
} catch (ignored: Exception) {
null
}
}
override fun onCleared() {
if (viewModelMap != null && configProvider().watchViewModels) {
viewModelMap.values.forEach { viewModel ->
objectWatcher.watch(
viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
)
}
}
}
4. 監(jiān)測(cè)對(duì)象是否泄露
在講這個(gè)之前得先回顧一個(gè)知識(shí)點(diǎn),Java中的WeakReference是弱引用類型,每當(dāng)發(fā)生GC時(shí),它所持有的對(duì)象如果沒有被其他強(qiáng)引用所持有,那么它所引用的對(duì)象就會(huì)被回收,同時(shí)或者稍后的時(shí)間這個(gè)WeakReference會(huì)被入隊(duì)到ReferenceQueue中.LeakCanary中檢測(cè)內(nèi)存泄露就是基于這個(gè)原理.
/**
* Weak reference objects, which do not prevent their referents from being
* made finalizable, finalized, and then reclaimed. Weak references are most
* often used to implement canonicalizing mappings.
*
* <p> Suppose that the garbage collector determines at a certain point in time
* that an object is <a href="package-summary.html#reachability">weakly
* reachable</a>. At that time it will atomically clear all weak references to
* that object and all weak references to any other weakly-reachable objects
* from which that object is reachable through a chain of strong and soft
* references. At the same time it will declare all of the formerly
* weakly-reachable objects to be finalizable. At the same time or at some
* later time it will enqueue those newly-cleared weak references that are
* registered with reference queues.
*
* @author Mark Reinhold
* @since 1.2
*/
public class WeakReference<T> extends Reference<T> {
/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
實(shí)現(xiàn)要點(diǎn):
- 當(dāng)一個(gè)對(duì)象需要被回收時(shí),生成一個(gè)唯一的key,將它們封裝進(jìn)KeyedWeakReference中,并傳入自定義的ReferenceQueue
- 將key和KeyedWeakReference放入一個(gè)map中
- 過一會(huì)兒之后(默認(rèn)是5秒)主動(dòng)觸發(fā)GC,將自定義的ReferenceQueue中的KeyedWeakReference全部移除(它們所引用的對(duì)象已被回收),并同時(shí)根據(jù)這些KeyedWeakReference的key將map中的KeyedWeakReference也移除掉.
- 此時(shí)如果map中還有KeyedWeakReference剩余,那么就是沒有入隊(duì)的,也就是說這些KeyedWeakReference所對(duì)應(yīng)的對(duì)象還沒被回收.這是不合理的,這里就產(chǎn)生了內(nèi)存泄露.
- 將這些內(nèi)存泄露的對(duì)象分析引用鏈,保存數(shù)據(jù)
下面來看具體代碼:
//ObjectWatcher.kt
/**
* Watches the provided [watchedObject].
*
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
......
//移除引用隊(duì)列中的所有KeyedWeakReference,同時(shí)也將其從map中移除
removeWeaklyReachableObjects()
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
//存入map
watchedObjects[key] = reference
//默認(rèn)5秒之后執(zhí)行moveToRetained()檢查
//這里是用的handler.postDelay實(shí)現(xiàn)的延遲
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
@Synchronized private fun moveToRetained(key: String) {
//移除那些已經(jīng)被回收的
removeWeaklyReachableObjects()
//判斷一下這個(gè)key鎖對(duì)應(yīng)的KeyedWeakReference是否被移除了
val retainedRef = watchedObjects[key]
//沒有被移除的話,說明是發(fā)生內(nèi)存泄露了
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
需要被回收的Activity、Fragment什么的都會(huì)走watch()這個(gè)方法這里,檢測(cè)是否有內(nèi)存泄露發(fā)生.上面這塊代碼對(duì)應(yīng)著實(shí)現(xiàn)要點(diǎn)的1-4步.接下來具體分析內(nèi)存泄露了是怎么走的
//InternalLeakCanary#onObjectRetained()
//InternalLeakCanary#scheduleRetainedObjectCheck()
//HeapDumpTrigger#scheduleRetainedObjectCheck()
//HeapDumpTrigger#checkRetainedObjects()
private fun checkRetainedObjects() {
//比如如果是在調(diào)試,那么暫時(shí)先不dump heap,延遲20秒再判斷一下狀態(tài)
val config = configProvider()
......
//還剩多少對(duì)象沒被回收 這些對(duì)象可能不是已經(jīng)泄露的
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//手動(dòng)觸發(fā)GC,這里觸發(fā)GC時(shí)還延遲了100ms,給那些回收了的對(duì)象入引用隊(duì)列一點(diǎn)時(shí)間,好讓結(jié)果更準(zhǔn)確.
gcTrigger.runGc()
//再看看還剩多少對(duì)象沒被回收
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//checkRetainedCount這里有2中情況返回true,流程return.
//1. 未被回收的對(duì)象數(shù)是0,展示無泄漏的通知
//2. 當(dāng)retainedReferenceCount小于5個(gè),展示有泄漏的通知(app可見或不可見超過5秒),延遲2秒再進(jìn)行檢查checkRetainedObjects()
//app可見是在VisibilityTracker.kt中判斷的,通過記錄Activity#onStart和onStop的數(shù)量來判斷
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
//1分鐘之內(nèi)才dump過,再過會(huì)兒再來
onRetainInstanceListener.onEvent(DumpHappenedRecently)
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
//開始dump
//通過 Debug.dumpHprofData(filePath) dump heap
//開始dump heap之前還得objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) 清除一下這次dump開始之前的所有引用
//最后是用HeapAnalyzerService這個(gè)IntentService去分析heap,具體在HeapAnalyzerService#runAnalysis()
dumpHeap(retainedReferenceCount, retry = true)
}
HeapAnalyzerService 里調(diào)用的是 Shark 庫對(duì) heap 進(jìn)行分析狼荞,分析的結(jié)果再返回到 DefaultOnHeapAnalyzedListener.onHeapAnalyzed 進(jìn)行分析結(jié)果入庫宅粥、發(fā)送通知消息啊楚。
Shark ?? :Shark is the heap analyzer that powers LeakCanary 2. It's a Kotlin standalone heap analysis library that runs at 「high speed」 with a 「low memory footprint」.
5. 總結(jié)
LeakCanary是一只優(yōu)雅的金絲雀,幫助我們監(jiān)測(cè)內(nèi)存泄露.本文主要分析了LeakCanary的初始化吠冤、監(jiān)聽泄露的時(shí)機(jī)、監(jiān)測(cè)某個(gè)對(duì)象泄露的過程.源碼中實(shí)現(xiàn)非常優(yōu)雅,本文中未完全展現(xiàn)出來,比較源碼太多貼上來不太雅觀.讀源碼不僅能讓我們學(xué)到新東西,而且也讓我們以后寫代碼有可以模仿的對(duì)象,甚至還可以在面試時(shí)得心應(yīng)手,一舉三得.