首頁>技術>

1.使用

LeakCancary 2.0使用,只需要配置如下程式碼,便可以進行使用,比LeakCanary1.0不知道高到哪裡去了~

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'
2.原始碼分析

閱讀原始碼後可以看到leakcancary-leaksentry模組的Androidmanifest檔案,可以看到下面的內容:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.squareup.leakcanary.leaksentry" > <application> <provider android:name="leakcanary.internal.LeakSentryInstaller" android:authorities="${applicationId}.leak-sentry-installer" android:exported="false"/> </application></manifest>

然後我們可以看到LeakSentryInstaller這個類到底做了什麼

internal class LeakSentryInstaller : ContentProvider() { override fun onCreate(): Boolean { CanaryLog.logger = DefaultCanaryLog() val application = context!!.applicationContext as Application //利用系統自動呼叫ContentProvider的onCreate來進行安裝 InternalLeakSentry.install(application) return true } ...

至於為什麼系統會呼叫ContentProvider的onCreate方法,我們可以看看原始碼,在ActivityThread中的H中的handleMessage可以看到

public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; //關鍵方法 handleBindApplication(data); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break;

然後在handleBindApplication中可以看到

// don't bring up providers in restricted mode; they may depend on the// app's custom Application classif (!data.restrictedBackupMode) { if (!ArrayUtils.isEmpty(data.providers)) { //contentprovider初始化,裡面會呼叫onCreate方法 installContentProviders(app, data.providers); }}// Do this after providers, since instrumentation tests generally start their// test thread at this point, and we don't want that racing.try { mInstrumentation.onCreate(data.instrumentationArgs);}catch (Exception e) { throw new RuntimeException( "Exception thrown in onCreate() of " + data.instrumentationName + ": " + e.toString(), e);}try { //app的onCreate方法呼叫 mInstrumentation.callApplicationOnCreate(app);} catch (Exception e) {

具體呼叫contentprovider的onCreate程式碼邏輯如下

@UnsupportedAppUsageprivate void installContentProviders( Context context, List<ProviderInfo> providers) { final ArrayList<ContentProviderHolder> results = new ArrayList<>(); for (ProviderInfo cpi : providers) { ··· //installProvider方法 ContentProviderHolder cph = installProvider(context, null, cpi, false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/); if (cph != null) { cph.noReleaseNeeded = true; results.add(cph); } } //installProvider方法,然後一步步跟進 //1 //XXX Need to create the correct context for this provider. localProvider.attachInfo(c, info); //2 public void attachInfo(Context context, ProviderInfo info) { attachInfo(context, info, false); } //3 private void attachInfo(Context context, ProviderInfo info, boolean testing) { mNoPerms = testing; mCallingPackage = new ThreadLocal<>(); if (mContext == null) { ··· ContentProvider.this.onCreate(); } }

通過上面的分析,可以知道在我們引入依賴後,依賴包中的AndroidMainfest.xml檔案便會主動合併到主AndroidManifest.xml檔案中,然後在程式啟動過程中便會自動建立ContentProvider,然後進行InternalLeakSentry.install(application),接下來進行一些列的監控和dump操作等。

2.1 InternalLeakSentry.install(application)

下面來分析InternalLeakSentry.install(application)裡面都做了一些什麼,可以看到

fun install(application: Application) { CanaryLog.d("Installing LeakSentry") checkMainThread() if (this::application.isInitialized) { return } InternalLeakSentry.application = application val configProvider = { LeakSentry.config } // 1.監聽 Activity.onDestroy() ActivityDestroyWatcher.install( application, refWatcher, configProvider ) // 2.監聽 Fragment.onDestroy() FragmentDestroyWatcher.install( application, refWatcher, configProvider ) // 3.監聽完成後進行一些初始化工作 listener.onLeakSentryInstalled(application) }

從命名上可以看到在Activity和Fragment進行destory的時候進行watch

1. 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) { // 監聽到 onDestroy() 之後,通過 refWatcher 監測 Activity refWatcher.watch(activity) } } } companion object { fun install( application: Application, refWatcher: RefWatcher, configProvider: () -> Config ) { val activityDestroyWatcher = ActivityDestroyWatcher(refWatcher, configProvider) application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks) } }}

2. FragmentDestroyWatcher

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>() //大於等於android O  if (SDK_INT >= O) { fragmentDestroyWatchers.add( AndroidOFragmentDestroyWatcher(refWatcher, configProvider) ) } if (classAvailable(  SUPPORT_FRAGMENT_CLASS_NAME ) ) { // androidx 使用 SupportFragmentDestroyWatcher fragmentDestroyWatchers.add( 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 } } }}

Android O 及以後,androidx 都具備對 Fragment 生命週期的監聽功能。為什麼不監聽Android O之前的呢???(待解決) 在版本為1.5.4之前是不支援Fragment記憶體洩漏監聽的,後面版本才加了進來。

3. listener.onLeakSentryInstalled(application)

該listener的最終實現類是leakcanary-android-core中的InternalLeakCanary類

override fun onLeakSentryInstalled(application: Application) { this.application = application val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) //用於發現可能的記憶體洩漏之後手動呼叫 GC 確認是否真的為記憶體洩露 val gcTrigger = GcTrigger.Default val configProvider = { LeakCanary.config } val handlerThread = HandlerThread(HeapDumpTrigger.LEAK_CANARY_THREAD_NAME) handlerThread.start() val backgroundHandler = Handler(handlerThread.looper) //用於確認記憶體洩漏之後進行 heap dump 工作。 heapDumpTrigger = HeapDumpTrigger( application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider ) application.registerVisibilityListener { applicationVisible -> this.applicationVisible = applicationVisible heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible) } addDynamicShortcut(application) }

這裡有個關於GC回收的知識點,我們可以看看優秀的第三方框架都是怎麼寫的

interface GcTrigger { fun runGc() 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() } private fun enqueueReferences() { // Hack. We don't have a programmatic way to wait for the reference queue daemon to move // references to the appropriate queues. try { Thread.sleep(100) } catch (e: InterruptedException) { throw AssertionError() } } }}

可以看到,它使用了Runtime.getRuntime().gc()而不是System.gc(),進入System.gc原始碼一看

public static void gc() { boolean shouldRunGC; synchronized (LOCK) { shouldRunGC = justRanFinalization; if (shouldRunGC) { justRanFinalization = false; } else { runGC = true; } } if (shouldRunGC) { Runtime.getRuntime().gc(); }}

可以看到System.gc原始碼的還是最終實現是Runtime.getRuntime().gc();但是需要一系列的判斷條件,我們手動呼叫System.runFinalization()可以使gc方法中的justRanFinalizationw為true,從而保證Runtime.getRuntime().gc()會被執行。

3.如何判斷物件可能洩露:ReferenceQueue含義及作用

在Activity/Fragment銷燬後,會進行一系列的物件回收,我們把這些物件分別和引用佇列進行關聯,當某個物件被回收時,(弱引用一旦變成弱可達(可達性演算法分析),引用就會加到引用佇列中,然後再進行回收)我們物件的引用就會被加入到引用佇列中。根據該原理進行一系列的操作,最終判斷是否記憶體洩漏。

3.1 引用佇列

通常我們將其ReferenceQueue翻譯為引用佇列,換言之就是存放引用的佇列,儲存的是Reference物件。其作用在於Reference物件所引用的物件被GC回收時,該Reference物件將會被加入引用佇列中(ReferenceQueue)的佇列末尾。

ReferenceQueue常用的方法:

public Reference poll():從佇列中取出一個元素,佇列為空則返回null;

public Reference remove():從佇列中出對一個元素,若沒有則阻塞至有可出隊元素;

public Reference remove(long timeout):從佇列中出對一個元素,若沒有則阻塞至有可出對元素或阻塞至超過timeout毫秒;

1、強引用2、軟引用3、弱引用4、虛引用(Phantom Reference)虛引等同於沒有引用,這意味著在任何時候都可能被GC回收,設定虛引用的目的是為了被虛引用關聯的物件在被垃圾回收器回收時,能夠收到一個系統通知。(被用來跟蹤物件被GC回收的活動)虛引用和弱引用的區別在於:虛引用在使用時必須和引用佇列(ReferenceQueue)聯合使用,其在GC回收期間的活動如下:

ReferenceQueue queue=new ReferenceQueue();

PhantomReference pr=new PhantomReference(object,queue);

也即是GC在回收一個物件時,如果發現該物件具有虛引用,那麼在回收之前會首先該物件的虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入虛引用來了解被引用的物件是否被GC回收。

3.3 記憶體是否洩漏

知道引用佇列的原理後,先大概描述一下如何判斷是否洩漏,首先建立三個佇列

/** * 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>() // 引用佇列,配合弱引用使用 //KeyedWeakReference,物件和引用佇列進行弱引用關聯,所以這個物件一定會被回收 class KeyedWeakReference( referent: Any, val key: String, val name: String, val watchUptimeMillis: Long, referenceQueue: ReferenceQueue<Any>) : WeakReference<Any>( referent, referenceQueue) { @Volatile var retainedUptimeMillis = -1L companion object { @Volatile @JvmStatic var heapDumpUptimeMillis = 0L }} 

如果一個obj物件,它和佇列queue進行弱引用關聯,在進行垃圾收集時,發現該物件具有弱引用,會把引用加入到引用佇列中,我們如果在該佇列中拿到引用,則說明該物件被回收了,如果拿不到,則說明該物件還有強/軟引用未釋放,那麼就說明物件還未回收,發生記憶體洩漏了,然後dump記憶體快照,使用第三方庫進行引用鏈分析。

這裡重點強調一點一個物件可能被多個引用持有,比如強引用,軟引用,弱引用,只要這個物件還有強引用/軟引用,與這個物件關聯的任意引用佇列就拿不到引用,引用佇列就相當於一個通知,多個引用佇列和一個物件關聯,物件被回收時,多個佇列都會受到通知。

3.4 watch()
@Synchronized fun watch( watchedReference: Any, referenceName: String) { if (!isEnabled()) { return } //移除佇列中將要被 GC 的引用 removeWeaklyReachableReferences() 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 checkRetainedExecutor.execute { //如果引用未被移除,則可能存在記憶體洩漏 moveToRetained(key) }}

removeWeaklyReachableReferences()

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) }

moveToRetained()

@Synchronized private fun moveToRetained(key: String) { removeWeaklyReachableReferences() // 再次呼叫,防止遺漏 val retainedRef = watchedReferences.remove(key) if (retainedRef != null) { retainedReferences[key] = retainedRef onReferenceRetained() } }

最後會回撥到InternalLeakCanary的onReferenceRetained()方法

override fun onReferenceRetained() { if (this::heapDumpTrigger.isInitialized) { heapDumpTrigger.onReferenceRetained() }}//1.HeapDumpTrigger 的 onReferenceRetained()fun onReferenceRetained() { scheduleRetainedInstanceCheck("found new instance retained")}//2.scheduleRetainedInstanceCheckprivate fun scheduleRetainedInstanceCheck(reason: String) { backgroundHandler.post { checkRetainedInstances(reason) }} //3.checkRetainedInstancesprivate 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() 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) HeapAnalyzerService.runAnalysis(application, heapDumpFile)}

一些細節可以看看程式碼註釋,checkRetainedCount滿足個數的話,就要發起head dump,具體的邏輯在AndroidHeapDumper.dumpHeap()中:

 override fun dumpHeap(): File? { val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null ··· return try { //Dump出文件 Debug.dumpHprofData(heapDumpFile.absolutePath) heapDumpFile } catch (e: Exception) { CanaryLog.d(e, "Could not dump heap") // Abort heap dump null } finally { cancelToast(toast) notificationManager.cancel(R.id.leak_canary_notification_dumping_heap) } }

最後啟動一個前臺服務 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 引用路徑,然後展示給使用者。

4.手動寫記憶體洩漏檢測

下面是參考Zero的Demo寫的記憶體洩漏檢測的一個例子,思路和LeakCanary一樣

fun main() { class MyKeyedWeakReference( referent: Any, val key: String, val name: String, referenceQueue: ReferenceQueue<Any> ) : WeakReference<Any>( referent, referenceQueue ) { val className: String = referent.javaClass.name override fun toString(): String { return "{key=$key,className=$className}" } } //需要觀察的物件 val watchedReferences = mutableMapOf<String,MyKeyedWeakReference>() //如果最後retainedReferences還存在引用,說明洩漏了 val retainedReferences = mutableMapOf<String,MyKeyedWeakReference>() //當與之關聯的弱引用中的例項被回收,則會加入到queue val gcQueue = ReferenceQueue<Any>() fun sleep(mills: Long){ try { Thread.sleep(mills) }catch (e: Exception){ e.printStackTrace() } } fun gc(){ println("執行gc...") Runtime.getRuntime().gc() sleep(100) System.runFinalization() } fun removeWeaklyReachableReferences(){ println("removeWeaklyReachableReferences") var ref: MyKeyedWeakReference? do { ref = gcQueue.poll() as MyKeyedWeakReference? //佇列queue中的物件都是會被GC的 println("ref=$ref,如果ref為null,說明物件還有強引用") if (ref != null){ //說明被釋放了 println("ref=$ref, 物件被釋放了,key=${ref.key}") val removedRef = watchedReferences.remove(ref.key) println("removedRef=$removedRef, 如果removedRef為null,說明已經不在watchedReferences了,key=${ref.key}") if (removedRef == null){  //不在watchedReferences則說明在retainedReferences  retainedReferences.remove(ref.key) } } }while (ref != null) } @Synchronized fun moveToRetained(key: String){ println("5.moveToRetained,key=$key") removeWeaklyReachableReferences() val retainedRef = watchedReferences.remove(key) println("retainedRef =$retainedRef 如果還有值說明沒有被釋放") if (retainedRef != null){ //新增到retainedReferences retainedReferences[key] = retainedRef } } fun watch( obj: Any, referenceName: String = ""){ println("2.watch...") removeWeaklyReachableReferences() val key = UUID.randomUUID().toString() println("3.key=$key") val reference = MyKeyedWeakReference(obj,key,referenceName,gcQueue) println("4.reference=$reference") //加入觀察列表 watchedReferences[key] = reference //過段時間檢視是否釋放 thread(start = true){ sleep(5000) moveToRetained(key) } } var obj : Any? = Object() println("1.建立一個物件obj=$obj") watch(obj!!,"") sleep(2000) obj = null if (obj == null){ println("obj=$obj 釋放了") } gc() sleep(5000) println("watchedReferences=$watchedReferences") println("retainedReferences=$retainedReferences") println("執行完畢")
5. ContentProvider的優化5.1 Content的初始化順序

通過ContentProvider來進行初始化確實能給使用者帶來便利,但是會影響啟動速度,如果有多個ContentProvider,如何控制這些ContentProvider初始化的順序呢,可以參考下面這篇文章https://sivanliu.github.io/2017/12/16/provider%E5%88%9D%E5%A7%8B%E5%8C%96/,如果一些第三方值只提供ContentProvider的初始化方式,我們又不想影響我們APP的啟動時間,該如何處理呢?

5.2

如果一些第三方庫只提供ContentProvider的初始化方式,我們又不想影響我們APP的啟動時間,該如何處理呢?

我們可以使用AOP方式進行插樁,通過Gradle+Transform+ASM進行修改ContentProvider的onCreate方法,提前返回,然後手動去呼叫初始化程式碼,如果這些初始化程式碼是私有的或者只限制包內使用的,也可以通過ASM去修改訪問許可權,然後在我們想初始化的地方再進行初始化,這可能涉及到一個先後的問題,需要先修改完然後再在某個地方初始化,這裡只是提供一個思路。

如果一個庫初始化耗時很長,又在ContentProvider中進行初始化,ContentProvider中初始化的程式碼又臭又長,又沒有提供其他初始化方法,這樣的垃圾庫你要它幹嘛!

6.總結利用ContentProvider自動初始化,無需使用者手動初始化GC回收,引用佇列1.5.4之後支援fragment,支援androidx當洩露引用到達 5 個時才會發起 heap dump全新的 heap parser,減少 90% 記憶體佔用,提升 6 倍速度ContentProvider的優劣,以及優化方案

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 使用鴻蒙 JS UI 框架開發 TV 版 Todo 應用