大概一年之前,寫過一篇 LeakCanary 源碼解析 ,當時是基於 1.5.4
版本進行分析的 。Square 公司在今年四月份發佈了全新的 2.0
版本,徹底使用 Kotlin 進行重構,核心原理並無太大變化,可是作了必定的性能優化。在本文中,就讓咱們經過源碼來看看 2.0
版本發生了哪些變化。本文不會過多的分析源碼細節,詳細細節能夠閱讀我以前基於 1.5.4
版本寫的文章,兩個版本在原理方面並無太大變化。java
含註釋 fork 版本 LeakCanary 源碼android
首先來對比一下兩個版本的使用方式。git
在老版本中,咱們須要添加以下依賴:github
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}
複製代碼
leakcanary-android-no-op
庫在 release
版本中使用,其中是沒有任何邏輯代碼的。性能優化
而後須要在本身的 Application
中進行初始化。bash
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
複製代碼
LeakCanary.install()
執行後,就會構建 RefWatcher
對象,開始監聽 Activity.onDestroy()
回調, 經過 RefWatcher.watch()
監測 Activity 引用的泄露狀況。發現內存泄露以後進行 heap dump
,利用 Square
公司的另外一個庫 haha(已廢棄)來分析 heap dump 文件,找到引用鏈以後通知用戶。這一套原理在新版本中仍是沒變的。微信
新版本的使用更加方便了,你只須要在 build.gradle
文件中添加以下依賴:app
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'
複製代碼
是的,你沒看過,這樣就能夠了。你確定會有一個疑問,那它是如何初始化的呢?我剛看到這個使用文檔的時候,一樣也有這個疑問。當你看看源碼以後就一目瞭然了。我先不解釋,看一下源碼中的 LeakSentryInstaller
這個類:less
/** * Content providers are loaded before the application class is created. [LeakSentryInstaller] is * used to install [leaksentry.LeakSentry] on application start. * * Content Provider 在 Application 建立以前被自動加載,所以無需用戶手動在 onCrate() 中進行初始化 */
internal class LeakSentryInstaller : ContentProvider() {
override fun onCreate(): Boolean {
CanaryLog.logger = DefaultCanaryLog()
val application = context!!.applicationContext as Application
InternalLeakSentry.install(application) // 進行初始化工做,核心
return true
}
override fun query( uri: Uri, strings: Array<String>?, s: String?, strings1: Array<String>?, s1: String? ): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert( uri: Uri, contentValues: ContentValues? ): Uri? {
return null
}
override fun delete( uri: Uri, s: String?, strings: Array<String>? ): Int {
return 0
}
override fun update( uri: Uri, contentValues: ContentValues?, s: String?, strings: Array<String>? ): Int {
return 0
}
}
複製代碼
看到這個類你應該也明白了。LeakCanary 利用 ContentProvier
進行了初始化。ContentProvier
通常會在 Application
被建立以前被加載,LeakCanary 在其 onCreate()
方法中調用了 InternalLeakSentry.install(application)
進行初始化。這應該是我第一次看到第三方庫這麼進行初始化。這的確是方便了開發者,可是仔細想一想弊端仍是很大的,若是全部第三方庫都這麼幹,開發者就無法控制應用啓動時間了。不少開發者爲了加快應用啓動速度,都下了很大心血,包括按需延遲初始化第三方庫。但在 LeakCanary 中,這個問題並不存在,由於它自己就是一個只在 debug 版本中使用的庫,並不會對 release 版本有任何影響。dom
前面提到了 InternalLeakSentry.install()
就是核心的初始化工做,其地位就和 1.5.4 版本中的 LeakCanary.install()
同樣。下面就從 install()
方法開始,走進 LeakCanary 2.0
一探究竟。
fun install(application: Application) {
CanaryLog.d("Installing LeakSentry")
checkMainThread() // 只能在主線程調用,不然會拋出異常
if (this::application.isInitialized) {
return
}
InternalLeakSentry.application = application
val configProvider = { LeakSentry.config }
ActivityDestroyWatcher.install( // 監聽 Activity.onDestroy(),見 1.1
application, refWatcher, configProvider
)
FragmentDestroyWatcher.install( // 監聽 Fragment.onDestroy(),見 1.2
application, refWatcher, configProvider
)
listener.onLeakSentryInstalled(application) // 見 1.3
}
複製代碼
install()
方法主要作了三件事:
Activity.onDestroy()
監聽Fragment.onDestroy()
監聽依次看一看。
ActivityDestroyWatcher
類的源碼很簡單:
internal class ActivityDestroyWatcher private constructor(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
refWatcher.watch(activity) // 監聽到 onDestroy() 以後,經過 refWatcher 監測 Activity
}
}
}
companion object {
fun install( application: Application, refWatcher: RefWatcher, configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(refWatcher, configProvider)
// 註冊 Activity 生命週期監聽
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
複製代碼
install()
方法中註冊了 Activity 生命週期監聽,在監聽到 onDestroy()
時,調用 RefWatcher.watch()
方法開始監測 Activity。
FragmentDestroyWatcher
是一個接口,它有兩個實現類 AndroidOFragmentDestroyWatcher
和 SupportFragmentDestroyWatcher
。
internal interface FragmentDestroyWatcher {
fun watchFragments(activity: Activity)
companion object {
private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"
fun install( application: Application, refWatcher: RefWatcher, configProvider: () -> LeakSentry.Config
) {
val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()
if (SDK_INT >= O) { // >= 26,使用 AndroidOFragmentDestroyWatcher
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(refWatcher, configProvider)
)
}
if (classAvailable(
SUPPORT_FRAGMENT_CLASS_NAME
)
) {
fragmentDestroyWatchers.add( // androidx 使用 SupportFragmentDestroyWatcher
SupportFragmentDestroyWatcher(refWatcher, configProvider)
)
}
if (fragmentDestroyWatchers.size == 0) {
return
}
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) {
for (watcher in fragmentDestroyWatchers) {
watcher.watchFragments(activity)
}
}
})
}
private fun classAvailable(className: String): Boolean {
return try {
Class.forName(className)
true
} catch (e: ClassNotFoundException) {
false
}
}
}
}
複製代碼
若是我沒記錯的話,1.5.4
是不監測 Fragment 的泄露的。而 2.0
版本提供了對 Android O
以及 androidx
版本中的 Fragment 的內存泄露檢測。 AndroidOFragmentDestroyWatcher
和 SupportFragmentDestroyWatcher
的實現代碼實際上是一致的,Android O 及之後,androidx 都具有對 Fragment 生命週期的監聽功能。以 AndroidOFragmentDestroyWatcher
爲例,簡單看一下它的實現。
@RequiresApi(Build.VERSION_CODES.O) //
internal class AndroidOFragmentDestroyWatcher(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) : FragmentDestroyWatcher {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed( fm: FragmentManager, fragment: Fragment ) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
refWatcher.watch(view)
}
}
override fun onFragmentDestroyed( fm: FragmentManager, fragment: Fragment ) {
if (configProvider().watchFragments) {
refWatcher.watch(fragment)
}
}
}
override fun watchFragments(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}
複製代碼
一樣,仍是使用 RefWatcher.watch()
方法來進行監測。
onLeakSentryInstalled()
回調中會初始化一些檢測內存泄露過程當中須要的對象,以下所示:
override fun onLeakSentryInstalled(application: Application) {
this.application = application
val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) // 用於 heap dump
val gcTrigger = GcTrigger.Default // 用於手動調用 GC
val configProvider = { LeakCanary.config } // 配置項
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper) // 發起內存泄漏檢測的線程
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
)
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
addDynamicShortcut(application)
}
複製代碼
對老版本代碼熟悉的同窗,看到這些對象應該很熟悉。
heapDumper
用於確認內存泄漏以後進行 heap dump 工做。gcTrigger
用於發現可能的內存泄漏以後手動調用 GC 確認是否真的爲內存泄露。這兩個對象是 LeakCanary 檢測內存泄漏的核心。後面會進行詳細分析。
到這裏,整個 LeakCanary 的初始化工做就完成了。與 1.5.4 版本不一樣的是,新版本增長了對 Fragment 以及 androidx 的支持。當發生 Activity.onDestroy()
,Fragment.onFragmentViewDestroyed()
, Fragment.onFragmentDestroyed()
三者之一時,RefWatcher
就開始工做了,調用其 watch()
方法開始檢測引用是否泄露。
在看源碼以前,咱們先來看幾個後面會使用到的隊列。
/** * References passed to [watch] that haven't made it to [retainedReferences] yet. * watch() 方法傳進來的引用,還沒有斷定爲泄露 */
private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
/** * References passed to [watch] that we have determined to be retained longer than they should * have been. * watch() 方法傳進來的引用,已經被斷定爲泄露 */
private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>() // 引用隊列,配合弱引用使用
複製代碼
經過 watch()
方法傳入的引用都會保存在 watchedReferences
中,被斷定泄露以後保存在 retainedReferences
中。注意,這裏的斷定過程不止會發生一次,已經進入隊列 retainedReferences
的引用仍有可能被移除。queue
是一個 ReferenceQueue
引用隊列,配合弱引用使用,這裏記住一句話:
弱引用一旦變得弱可達,就會當即入隊。這將在 finalization 或者 GC 以前發生。
也就是說,會被 GC 回收的對象引用,會保存在隊列 queue
中。
回頭再來看看 watch()
方法的源碼。
@Synchronized fun watch( watchedReference: Any, referenceName: String ) {
if (!isEnabled()) {
return
}
removeWeaklyReachableReferences() // 移除隊列中將要被 GC 的引用,見 2.1
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = // 構建當前引用的弱引用對象,並關聯引用隊列 queue
KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
if (referenceName != "") {
CanaryLog.d(
"Watching instance of %s named %s with key %s", reference.className,
referenceName, key
)
} else {
CanaryLog.d(
"Watching instance of %s with key %s", reference.className, key
)
}
watchedReferences[key] = reference // 將引用存入 watchedReferences
checkRetainedExecutor.execute {
moveToRetained(key) // 若是當前引用未被移除,仍在 watchedReferences 隊列中,
// 說明仍未被 GC,移入 retainedReferences 隊列中,暫時標記爲泄露
// 見 2.2
}
}
複製代碼
邏輯仍是比較清晰的,首先會調用 removeWeaklyReachableReferences()
方法,這個方法在整個過程當中會屢次調用。其做用是移除 watchedReferences
中將被 GC 的引用。
private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
// 弱引用一旦變得弱可達,就會當即入隊。這將在 finalization 或者 GC 以前發生。
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference? // 隊列 queue 中的對象都是會被 GC 的
if (ref != null) {
val removedRef = watchedReferences.remove(ref.key)
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
// 移除 watchedReferences 隊列中的會被 GC 的 ref 對象,剩下的就是可能泄露的對象
}
} while (ref != null)
}
複製代碼
整個過程當中會屢次調用,以確保將已經入隊 queue
的將被 GC 的對象引用移除掉,避免無謂的 heap dump 操做。而仍在 watchedReferences
隊列中的引用,則可能已經泄露,移到隊列 retainedReferences
中,這就是 moveToRetained()
方法的邏輯。代碼以下:
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableReferences() // 再次調用,防止遺漏
val retainedRef = watchedReferences.remove(key)
if (retainedRef != null) {
retainedReferences[key] = retainedRef
onReferenceRetained()
}
}
複製代碼
這裏的 onReferenceRetained()
最後會回調到 InternalLeakCanary.kt
中。
override fun onReferenceRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onReferenceRetained()
}
}
複製代碼
調用了 HeapDumpTrigger
的 onReferenceRetained()
方法。
fun onReferenceRetained() {
scheduleRetainedInstanceCheck("found new instance retained")
}
private fun scheduleRetainedInstanceCheck(reason: String) {
if (checkScheduled) {
return
}
checkScheduled = true
backgroundHandler.post {
checkScheduled = false
checkRetainedInstances(reason) // 檢測泄露實例
}
}
複製代碼
checkRetainedInstances()
方法是肯定泄露的最後一個方法了。這裏會確認引用是否真的泄露,若是真的泄露,則發起 heap dump,分析 dump 文件,找到引用鏈,最後通知用戶。總體流程和老版本是一致的,但在一些細節處理,以及 dump 文件的分析上有所區別。下面仍是經過源碼來看看這些區別。
private fun checkRetainedInstances(reason: String) {
CanaryLog.d("Checking retained instances because %s", reason)
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
return
}
var retainedKeys = refWatcher.retainedKeys
// 當前泄露實例個數小於 5 個,不進行 heap dump
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountWithDebuggerAttached(retainedKeys.size)
scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
CanaryLog.d(
"Not checking for leaks while the debugger is attached, will retry in %d ms",
WAIT_FOR_DEBUG_MILLIS
)
return
}
// 可能存在被觀察的引用將要變得弱可達,可是還未入隊引用隊列。
// 這時候應該主動調用一次 GC,可能能夠避免一次 heap dump
gcTrigger.runGc()
retainedKeys = refWatcher.retainedKeys
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)
CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
dismissNotification()
val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
if (heapDumpFile == null) {
CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
showRetainedCountWithHeapDumpFailed(retainedKeys.size)
return
}
refWatcher.removeRetainedKeys(retainedKeys) // 移除已經 heap dump 的 retainedKeys
HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
}
複製代碼
首先調用 checkRetainedCount()
函數判斷當前泄露實例個數若是小於 5 個,僅僅只是給用戶一個通知,不會進行 heap dump 操做,並在 5s 後再次發起檢測。這是和老版本一個不一樣的地方。
private fun checkRetainedCount( retainedKeys: Set<String>, retainedVisibleThreshold: Int // 默認爲 5 個 ): Boolean {
if (retainedKeys.isEmpty()) {
CanaryLog.d("No retained instances")
dismissNotification()
return true
}
if (retainedKeys.size < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
CanaryLog.d(
"Found %d retained instances, which is less than the visible threshold of %d",
retainedKeys.size,
retainedVisibleThreshold
)
// 通知用戶 "App visible, waiting until 5 retained instances"
showRetainedCountBelowThresholdNotification(retainedKeys.size, retainedVisibleThreshold)
scheduleRetainedInstanceCheck( // 5s 後再次發起檢測
"Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
)
return true
}
}
return false
}
複製代碼
當集齊 5 個泄露實例以後,也並不會立馬進行 heap dump。而是先手動調用一次 GC。固然不是使用 System.gc()
,以下所示:
object Default : GcTrigger {
override fun runGc() {
// Code taken from AOSP FinalizationTest:
// https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
// java/lang/ref/FinalizationTester.java
// System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perform a gc.
Runtime.getRuntime()
.gc()
enqueueReferences()
System.runFinalization()
}
複製代碼
那麼,爲何要進行此次 GC 呢?可能存在被觀察的引用將要變得弱可達,可是還未入隊引用隊列的狀況。這時候應該主動調用一次 GC,可能能夠避免一次額外的 heap dump 。GC 以後再次調用 checkRetainedCount()
判斷泄露實例個數。若是此時仍然知足條件,就要發起 heap dump 操做了。具體邏輯在 AndroidHeapDumper.dumpHeap()
方法中,核心方法就是下面這句代碼:
Debug.dumpHprofData(heapDumpFile.absolutePath)
複製代碼
生成 heap dump 文件以後,要刪除已經處理過的引用,
refWatcher.removeRetainedKeys(retainedKeys)
複製代碼
最後啓動一個前臺服務 HeapAnalyzerService
來分析 heap dump 文件。老版本中是使用 Square 本身的 haha 庫來解析的,這個庫已經廢棄了,Square 徹底重寫了解析庫,主要邏輯都在 moudle leakcanary-analyzer
中。這部分我尚未閱讀,就不在這裏分析了。對於新的解析器,官網是這樣介紹的:
Uses 90% less memory and 6 times faster than the prior heap parser.
減小了 90% 的內存佔用,並且比原來快了 6 倍。後面有時間單獨來分析一下這個解析庫。
後面的過程就再也不贅述了,經過解析庫找到最短 GC Roots 引用路徑,而後展現給用戶。
通讀完源碼,LeakCanary 2 仍是帶來了不少的優化。與老版本相比,主要有如下不一樣:
文章首發微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解。更多相關知識,掃碼關注我吧!