本文基于
leakcanary-android:2.5
我的所有原創Android知識體系,已打包整理到GitHub.努力打造一系列適合初中高級工程師能夠看得懂的優質文章,歡迎star~
思維導圖

1. 背景
Android開發中,記憶體泄露時常有發生在,有可能是你自己寫的,也有可能是三方庫里面的.程式中已動態分配的堆記憶體由于某種特殊原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至程式崩潰等嚴重后果.本來Android記憶體就吃緊,還記憶體泄露的話,后果不堪設想.所以我們要盡量避免記憶體泄露,一方面我們要學習哪些常見場景下會發生記憶體泄露,一方面我們引入LeakCanary幫我們自動檢測有記憶體泄露的地方.
LeakCanary是Square公司(對,又是這個公司,OkHttp和Retrofit等都是這家公司開源的)開源的一個庫,通過它我們可以在App運行的程序中檢測記憶體泄露,它把物件記憶體泄露的參考鏈也給開發人員分析出來了,我們去修復這個記憶體泄露非常方面.
ps: LeakCanary直譯過來是記憶體泄露的金絲雀,關于這個名字其實有一個小故事在里面.金絲雀,美麗的鳥兒.她的歌聲不僅動聽,還曾挽救過無數礦工的生命.17世紀,英國礦井工人發現,金絲雀對瓦斯這種氣體十分敏感.空氣中哪怕有極其微量的瓦斯,金絲雀也會停止歌唱;而當瓦斯含量超過一定限度時,雖然魯鈍的人類毫無察覺,金絲雀卻早已毒發身亡.當時在采礦設備相對簡陋的條件下,工人們每次下井都會帶上一只金絲雀作為"瓦斯檢測指標",以便在危險狀況下緊急撤離. 同樣的,LeakCanary這只"金絲雀"能非常敏感地幫我們發現記憶體泄露,從而避免OOM的風險.
2. 初始化
在引入LeakCanary的時候,只需要在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! 我們不需要改動任何的代碼,就這樣,LeakCanary就已經引入進來了. 那我有疑問了?我們一般引入一個庫都是在Application的onCreate中初始化,它不需要在代碼中初始化,它是如何起作用的呢?
我只想到一種方案可以實作這個,就是它在內部定義了一個ContentProvider,然后在ContentProvider的里面進行的初始化.
咱驗證一下: 引入LeakCanary之后,運行一下專案,然后在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對應的是@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里面進行的初始化.App在啟動時會自動初始化ContentProvider,也就自動呼叫了AppWatcher.manualInstall()進行了初始化.一開始的時候,我覺得這樣挺好的,挺優雅,后來發現好多三方庫都這么干了.每個庫一個ContentProvider進行初始化,有點冗余的感覺.后來Jetpack推出了App Startup,解決了這個問題,它就是基于這個原理進行的封裝.
需要注意的是ContentProvider的onCreate執行時機比Application的onCreate執行時機還早.如果你想在其他時機進行初始化優化啟動時間,也是可以的.只需要在app里重寫@bool/leak_canary_watcher_auto_install的值為false即可.然后手動在合適的地方呼叫AppWatcher.manualInstall(application).但是LeakCanary本來就是在debug的時候用的,所以感覺優化啟動時間不是那么必要.
3. 監聽泄露的時機
LeakCanary自動檢測以下物件的泄露:
- destroyed Activity instances
- destroyed Fragment instances
- destroyed fragment View instances
- cleared ViewModel instances
可以看到,檢測的都是些Android開發中容易被泄露的東西.那么它是如何檢測的,下面我們來分析一下
3.1 Activity
通過Application#registerActivityLifecycleCallbacks()注冊Activity生命周期監聽,然后在onActivityDestroyed()中進行objectWatcher.watch(activity,....)進行檢測物件是否泄露.檢測物件是否泄露這塊后面單獨分析.
3.2 Fragment、Fragment View
同樣的,檢測這2個也是需要監聽周期,不過這次監聽的是Fragment的生命周期,利用fragmentManager.registerFragmentLifecycleCallbacks可以實作.Fragment是在onFragmentDestroy()中檢測Fragment物件是否泄露,Fragment View在onFragmentViewDestroyed()里面檢測Fragment View物件是否泄露.
但是,拿到這個fragmentManager的程序有點曲折.
- Android O以上,通過activity#getFragmentManager()獲得. (AndroidOFragmentDestroyWatcher)
- AndroidX中,通過activity#getSupportFragmentManager()獲得. (AndroidXFragmentDestroyWatcher)
- support包中,通過activity#getSupportFragmentManager()獲得. (AndroidSupportFragmentDestroyWatcher)
可以看到,不同的場景下,取FragmentManager的方式是不同的.取FragmentManager的實作程序、注冊Fragment生命周期、在onFragmentDestroyed和onFragmentViewDestroyed中檢測物件是否有泄漏這一套邏輯,在不同的環境下,實作不同.所以把它們封裝進不同的策略(對應著上面3種策略)中,這就是策略模式的應用.
因為上面獲取FragmentManager需要Activity實體,所以這里還需要監聽Activity生命周期,在onActivityCreated()中拿到Activity實體,從而拿到FragmentManager去監聽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中還會單獨監聽onFragmentCreated()
override fun onFragmentCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
ViewModelClearedWatcher.install(fragment, objectWatcher, configProvider)
}
install里面實際是通過fragment和ViewModelProvider生成一個ViewModelClearedWatcher,這是一個新的ViewModel,然后在這個ViewModel的onCleared()里面檢測這個fragment里面的每個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. 監測物件是否泄露
在講這個之前得先回顧一個知識點,Java中的WeakReference是弱參考型別,每當發生GC時,它所持有的物件如果沒有被其他強參考所持有,那么它所參考的物件就會被回收,同時或者稍后的時間這個WeakReference會被入隊到ReferenceQueue中.LeakCanary中檢測記憶體泄露就是基于這個原理.
/**
* 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);
}
}
實作要點:
- 當一個物件需要被回收時,生成一個唯一的key,將它們封裝進KeyedWeakReference中,并傳入自定義的ReferenceQueue
- 將key和KeyedWeakReference放入一個map中
- 過一會兒之后(默認是5秒)主動觸發GC,將自定義的ReferenceQueue中的KeyedWeakReference全部移除(它們所參考的物件已被回收),并同時根據這些KeyedWeakReference的key將map中的KeyedWeakReference也移除掉.
- 此時如果map中還有KeyedWeakReference剩余,那么就是沒有入隊的,也就是說這些KeyedWeakReference所對應的物件還沒被回收.這是不合理的,這里就產生了記憶體泄露.
- 將這些記憶體泄露的物件分析參考鏈,保存資料
下面來看具體代碼:
//ObjectWatcher.kt
/**
* Watches the provided [watchedObject].
*
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
......
//移除參考佇列中的所有KeyedWeakReference,同時也將其從map中移除
removeWeaklyReachableObjects()
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
//存入map
watchedObjects[key] = reference
//默認5秒之后執行moveToRetained()檢查
//這里是用的handler.postDelay實作的延遲
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
@Synchronized private fun moveToRetained(key: String) {
//移除那些已經被回收的
removeWeaklyReachableObjects()
//判斷一下這個key鎖對應的KeyedWeakReference是否被移除了
val retainedRef = watchedObjects[key]
//沒有被移除的話,說明是發生記憶體泄露了
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
需要被回收的Activity、Fragment什么的都會走watch()這個方法這里,檢測是否有記憶體泄露發生.上面這塊代碼對應著實作要點的1-4步.接下來具體分析記憶體泄露了是怎么走的
//InternalLeakCanary#onObjectRetained()
//InternalLeakCanary#scheduleRetainedObjectCheck()
//HeapDumpTrigger#scheduleRetainedObjectCheck()
//HeapDumpTrigger#checkRetainedObjects()
private fun checkRetainedObjects() {
//比如如果是在除錯,那么暫時先不dump heap,延遲20秒再判斷一下狀態
val config = configProvider()
......
//還剩多少物件沒被回收 這些物件可能不是已經泄露的
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//手動觸發GC,這里觸發GC時還延遲了100ms,給那些回收了的物件入參考佇列一點時間,好讓結果更準確.
gcTrigger.runGc()
//再看看還剩多少物件沒被回收
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//checkRetainedCount這里有2中情況回傳true,流程return.
//1. 未被回收的物件數是0,展示無泄漏的通知
//2. 當retainedReferenceCount小于5個,展示有泄漏的通知(app可見或不可見超過5秒),延遲2秒再進行檢查checkRetainedObjects()
//app可見是在VisibilityTracker.kt中判斷的,通過記錄Activity#onStart和onStop的數量來判斷
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
//1分鐘之內才dump過,再過會兒再來
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這個IntentService去分析heap,具體在HeapAnalyzerService#runAnalysis()
dumpHeap(retainedReferenceCount, retry = true)
}
HeapAnalyzerService 里呼叫的是 Shark 庫對 heap 進行分析,分析的結果再回傳到 DefaultOnHeapAnalyzedListener.onHeapAnalyzed 進行分析結果入庫、發送通知訊息,
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. 總結
LeakCanary是一只優雅的金絲雀,幫助我們監測記憶體泄露.本文主要分析了LeakCanary的初始化、監聽泄露的時機、監測某個物件泄露的程序.原始碼中實作非常優雅,本文中未完全展現出來,比較原始碼太多貼上來不太雅觀.讀原始碼不僅能讓我們學到新東西,而且也讓我們以后寫代碼有可以模仿的物件,甚至還可以在面試時得心應手,一舉三得.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/235418.html
標籤:AI
