首頁>技術>

作為android進階知識,效能優化不管是在社招面試還是在日常工作中都是相當實用的知識,並且也是區分中級和高階程式設計師的試金石。我現在就會以不同的專題來進行講解,希望大家喜歡,如果想了解更多的話,歡迎關注我一起學習。

首先什麼是效能:在同一個手機裡面,同樣功能的app,哪個跑的快,哪個不卡,哪個就效能高。我們這節就是解決那些效能慢的問題:

1)我們要找到效能低的地方,並且把這些地方解決掉,這個就是效能優化;

2)我們要讓自己具備一開始寫的程式碼,它執行起來就是高效能的,所以這個就是設計思想和程式碼品質優化。一個app的效能好不好我們需要從兩個層面努力。

第一個層面:從寫程式碼的時候就需要注意,讓自己的程式碼是高效能高可用的程式碼,這個過程是書寫高效能程式碼;

第二個層面:對已經成型的程式碼通過工具檢查程式碼的問題,通過檢查到的問題來指導我們進行程式碼的刪改,這個過程被稱為調優。

下面的關於效能優化的導圖,是我花費2個月整理了很多資料才做出來的,現在拿出來分享給大家

今天我們先學習記憶體優化中的一個小知識點,就是記憶體洩露的檢測和解決。當然,如何解決是大多數中級工程師都要去學習的東西,網上也有大量的資料,所以我這裡不會詳解。而是主要著眼於記憶體洩露的檢測。Square公司出品的大名鼎鼎的LeakCanary,就是業界知名的記憶體洩露檢測的利器。

閱讀本篇文章,預計需要20分鐘,你將會學習到:

LeakCanary檢測記憶體洩露的原理使用ContentProvider進行三方庫初始化的方法原理概述

關於LeakCanary的原理,官網上已經給出了詳細的解釋。翻譯過來就是:1.LeakCanary使用ObjectWatcher來監控Android的生命週期。當Activity和Fragment被destroy以後,這些引用被傳給ObjectWatcher以WeakReference的形式引用著。如果gc完5秒鐘以後這些引用還沒有被清除掉,那就是記憶體洩露了。2.當被洩露掉的物件達到一個閾值,LeakCanary就會把java的堆疊資訊dump到.hprof檔案中。3.LeakCanary用Shark庫來解析.hprof檔案,找到無法被清理的引用的引用棧,然後再根據對Android系統的知識來判定是哪個例項導致的洩露。4.通過洩露資訊,LeakCanary會將一條完整的引用鏈縮減到一個小的引用鏈,其餘的因為這個小的引用鏈導致的洩露鏈都會被聚合在一起。

通過官網的介紹,我們很容易就抓住了學習LeakCanary這個庫的重點:

LeakCanary是如何使用ObjectWatcher 監控生命週期的?LeakCanary如何dump和分析.hprof檔案的?

看官方原理總是感覺不過癮,下面我們從程式碼層面上來分析。本文基於LeakCanary 2.0 beta版。

基本使用

LeakCanary的使用相當的簡單。只需要在module的build.gradle新增一行依賴,程式碼侵入少。

ependencies {  // debugImplementation because LeakCanary should only run in debug builds.  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-3'}

就這樣,應用非常簡單就接入了LeakCanary記憶體檢測功能。當然還有一些更高階的用法,比如更改自定義config,增加監控項等,大家可以參考官網

原始碼分析1. 初始化

和之前的1.x版本相比,2.0甚至都不需要再在Application裡面增加install的程式碼。可能很多的同學都會疑惑,LeakCanary是如何插入自己的初始化程式碼的呢? 其實這裡LeakCanary是使用了ContentProvider來進行初始化。我之前在介紹Android外掛化系列三:技術流派和四大元件支援的時候曾經介紹過ContentProvider的特點,即在打包的過程中來自不同module的ContentProvider最後都會merge到一個檔案中,啟動app的時候ContentProvider是自動安裝,並且安裝會比Application的onCreate還早。LeakCanary就是依據這個原理進行的設計。具體可以參考【譯】你的Android庫是否還在Application中初始化?

我們可以檢視LeakCanary原始碼,發現它在leakcanary-object-watcher-android的AndroidManifest.xml中有一個ContentProvider。

<provider        android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"        android:authorities="${applicationId}.leakcanary-installer"        android:exported="false"/>

然後我們檢視AppWatcherInstaller的程式碼,發現內部是使用InternalAppWatcher進行的install。

 // AppWatcherInstalleroverride fun onCreate(): Boolean {    val application = context!!.applicationContext as Application    InternalAppWatcher.install(application)    return true}  // InternalAppWatcherfun install(application: Application) {    // 省略部分程式碼    checkMainThread()    if (this::application.isInitialized) {      return    }    InternalAppWatcher.application = application    val configProvider = { AppWatcher.config }    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)    onAppWatcherInstalled(application)}

可以看到這裡主要把Activity和Fragment區分了開來,然後分別進行註冊。Activity的生命週期監聽是藉助於Application.ActivityLifecycleCallbacks。

fecycleCallbacks =    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {      override fun onActivityDestroyed(activity: Activity) {        if (configProvider().watchActivities) {          objectWatcher.watch(activity)        }      }    }application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)

而Fragment的生命週期監聽是藉助了Activity的ActivityLifecycleCallbacks生命週期回撥,當Activity建立的時候去呼叫FragmentManager.registerFragmentLifecycleCallbacks方法註冊Fragment的生命週期監聽。

override fun onFragmentViewDestroyed(      fm: FragmentManager,      fragment: Fragment    ) {      val view = fragment.view      if (view != null && configProvider().watchFragmentViews) {        objectWatcher.watch(view)      }    }    override fun onFragmentDestroyed(      fm: FragmentManager,      fragment: Fragment    ) {      if (configProvider().watchFragments) {        objectWatcher.watch(fragment)      }    }  }

最終,Activity和Fragment都將自己的引用傳入了ObjectWatcher.watch()進行監控。從這裡開始進入到LeakCanary的引用監測邏輯。

題外話:LeakCanary 2.0版本和1.0版本相比,增加了Fragment的生命週期監聽,每個類的職責也更加清晰。但是我個人覺得使用 (Activty)->Unit 這種lambda表示式作為類的寫法不是很優雅,倒不如面向介面程式設計。完全可以設計成ActivityWatcher和FragmentWatcher都繼承自某個介面,這樣也方便後續擴充套件。

2. 引用監控

2.1 引用和GC

引用首先我們先介紹一點準備知識。大家都知道,java中存在四種引用:強引用:垃圾回收器絕不會回收它,當記憶體空間不足,Java虛擬機器寧願丟擲OOM軟引用:只有在記憶體不足的時候JVM才會回收僅有軟引用指向的物件所佔的空間弱引用:當JVM進行垃圾回收時,無論記憶體是否充足,都會回收僅被弱引用關聯的物件。虛引用:和沒有任何引用一樣,在任何時候都可能被垃圾回收。

一個物件在被gc的時候,如果發現還有軟引用(或弱引用,或虛引用)指向它,就會在回收物件之前,把這個引用加入到與之關聯的引用佇列(ReferenceQueue)中去。如果一個軟引用(或弱引用,或虛引用)物件本身在引用佇列中,就說明該引用物件所指向的物件被回收了。

當軟引用(或弱引用,或虛引用)物件所指向的物件被回收了,那麼這個引用物件本身就沒有價值了,如果程式中存在大量的這類物件(注意,我們建立的軟引用、弱引用、虛引用物件本身是個強引用,不會自動被gc回收),就會浪費記憶體。因此我們這就可以手動回收位於引用佇列中的引用物件本身。

比如我們經常看到這種用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);

還有也有這樣一種用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list, new ReferenceQueue<WeakReference<ArrayList>>());

這樣就可以把物件和ReferenceQueue關聯起來,進行物件是否gc的判斷了。另外我們從弱引用的特徵中看到,弱引用是不會影響到這個物件是否被gc的,很適合用來監控物件的gc情況。

2.GCjava中有兩種手動呼叫GC的方式。

System.gc();// 或者Runtime.getRuntime().gc();
2.2 監控

我們在第一節中提到,Activity和Fragment都依賴於響應的LifecycleCallback來回調銷燬資訊,然後呼叫了ObjectWatcher.watch添加了銷燬後的監控。接下來我們看ObjectWatcher.watch做了什麼操作

@Synchronized fun watch(    watchedObject: Any,    name: String  ) {    removeWeaklyReachableObjects()    val key = UUID.randomUUID().toString()    val watchUptimeMillis = clock.uptimeMillis()    val reference =      KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)    watchedObjects[key] = reference    checkRetainedExecutor.execute {      moveToRetained(key)    }  }  private fun removeWeaklyReachableObjects() {    // 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.    var ref: KeyedWeakReference?    do {      ref = queue.poll() as KeyedWeakReference?      if (ref != null) {        watchedObjects.remove(ref.key)      }    } while (ref != null)  }  @Synchronized private fun moveToRetained(key: String) {    removeWeaklyReachableObjects()    val retainedRef = watchedObjects[key]    if (retainedRef != null) {      retainedRef.retainedUptimeMillis = clock.uptimeMillis()      onObjectRetainedListeners.forEach { it.onObjectRetained() }    }  }

這裡我們看到,有一個儲存著KeyedWeakReference的ReferenceQueue物件。在每次增加watch object的時候,都會去把已經處於ReferenceQueue中的物件給從監控物件的map即watchObjects中清理掉,因為這些物件都已經被回收了。然後再去生成一個KeyedWeakReference,這個物件就是一個持有了key和監測開始時間的WeakReference物件。最後再去呼叫moveToRetained,相當於記錄和回撥給監控方這個物件正式開始監測的時間。

那麼我們現在已經拿到了需要監控的物件了,但是又是怎麼去判斷這個物件已經記憶體洩露的呢?這就要繼續往下面看。我們主要到前面在講解InternalAppWatcher的install方法的時候,除了install了Activity和Fragment的檢測器,還呼叫了onAppWatcherInstalled(application)方法,看程式碼發現這個方法就是InternalLeakCanary的invoke方法。

 override fun invoke(application: Application) {    this.application = application    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)    val gcTrigger = GcTrigger.Default    val configProvider = { LeakCanary.config }    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)    handlerThread.start()    val backgroundHandler = Handler(handlerThread.looper)    heapDumpTrigger = HeapDumpTrigger(        application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,        configProvider    )  }  override fun onObjectRetained() {    if (this::heapDumpTrigger.isInitialized) {      heapDumpTrigger.onObjectRetained()    }  }

我們看到首先是初始化了heapDumper,gcTrigger,heapDumpTrigger等物件用於gc和heapDump,同時還實現了OnObjectRetainedListener,並把自己新增到了上面的onObjectRetainedListeners中,以便每個物件moveToRetained的時候,InternalLeakCanary都能獲取到onObjectRetained()的回撥,回撥裡就只是回調了heapDumpTrigger.onObjectRetained()方法。看來都是依賴於HeapDumpTrigger這個類。

HeapDumpTrigger主要的處理邏輯都在checkRetainedObjects方法中。

 private fun checkRetainedObjects(reason: String) {    val config = configProvider()    var retainedReferenceCount = objectWatcher.retainedObjectCount    if (retainedReferenceCount > 0) {      gcTrigger.runGc()  // 觸發一次GC操作,只保留不能被回收的物件      retainedReferenceCount = objectWatcher.retainedObjectCount    }    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {      showRetainedCountWithDebuggerAttached(retainedReferenceCount)      scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)      return    }    val heapDumpUptimeMillis = SystemClock.uptimeMillis()    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis    dismissRetainedCountNotification()    val heapDumpFile = heapDumper.dumpHeap()    if (heapDumpFile == null) {      scheduleRetainedObjectCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)      showRetainedCountWithHeapDumpFailed(retainedReferenceCount)      return    }    lastDisplayedRetainedObjectCount = 0    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)    HeapAnalyzerService.runAnalysis(application, heapDumpFile)  }

那麼HeapDumpTrigger具體做了些啥呢?我理了一下主要是下面幾個功能:

後臺執行緒輪詢當前還存活著的物件如果存活的物件大於0,那就觸發一次GC操作,回收掉沒有洩露的物件GC完後,仍然存活著的物件數和預定的物件數相比較,如果多了就呼叫heapDumper.dumpHeap()方法把物件dump成檔案,並交給HeapAnalyzerService去分析根據存活情況展示通知

2.3 總結

看到了這裡,我們應該腦海中有概念了。Activity和Fragment通過註冊系統的監聽在onDestroy的時候把自己的引用放入ObjectWatcher進行監測,監測主要是通過HeapDumpTrigger類輪詢進行,主要是呼叫AndroidHeapDumper來dump出文件來,然後依賴於HeapAnalyzerService來進行分析。後面一小節,我們將會聚焦於物件dump操作和HeapAnalyzerService的分析過程。

3. dump物件及分析

3.1 dump物件

hprof是JDK提供的一種JVM TI Agent native工具。JVM TI,全拼是JVM Tool interface,是JVM提供的一套標準的C/C++程式設計介面,是實現Debugger、Profiler、Monitor、Thread Analyser等工具的統一基礎,在主流Java虛擬機器中都有實現。hprof工具事實上也是實現了這套介面,可以認為是一套簡單的profiler agent工具。我們在新知周推:10.8-10.14(啟動篇)中也提到過,可以參考其中美團的文章。

用過Android Studio Profiler工具的同學對hprof檔案都不會陌生,當我們使用Memory Profiler工具的Dump Java heap圖示的時候,profiler工具就會去捕獲你的記憶體分配情況。但是捕獲以後,只有在Memory Profiler正在執行的時候我們才能檢視,那麼我們要怎麼樣去儲存當時的記憶體使用情況呢,又或者我想用別的工具來分析堆分配情況呢,這時候hprof檔案就派上用場了。Android Studio可以把這些物件給export到hprof檔案中去。

LeakCanary也是使用的hprof檔案進行物件儲存。hprof檔案比較簡單,整體按照 前置資訊 + 記錄表的格式來組織的。但是記錄的種類相當之多。具體種類可以檢視HPROF Agent。

同時,android中也提供了一個簡便的方法Debug.dumpHprofData(filePath)可以把物件dump到指定路徑下的hprof檔案中。LeakCanary使用使用Shark庫來解析Hprof檔案中的各種record,比較高效,使用Shark中的HprofReader和HprofWriter來進行讀寫解析,獲取我們需要的資訊。大家可以關注一些比較重要的,比如:

Class DumpInstance DumpObject Array DumpPrimitive Array Dump

dump具體的程式碼在AndroidHeapDumper類中。HprofReader和HprofWriter過於複雜,有興趣的直接檢視原始碼吧

override fun dumpHeap(): File? {    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null    return try {      Debug.dumpHprofData(heapDumpFile.absolutePath)      if (heapDumpFile.length() == 0L) {        null      } else {        heapDumpFile      }    } catch (e: Exception) {      null    } finally {    }  }

3.2 物件分析

前面我們已經分析到了,HeapDumpTrigger主要是依賴於HeapAnalyzerService進行分析。那麼這個HeapAnalyzerService究竟有什麼玄機?讓我們繼續往下面看。可以看到HeapAnalyzerService其實是一個ForegroundService。在接收到分析的Intent後就會呼叫HeapAnalyzer的analyze方法。所以最終進行分析的地方就是HeapAnalyzer的analyze方法。

核心程式碼如下

override fun dumpHeap(): File? {    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null    return try {      Debug.dumpHprofData(heapDumpFile.absolutePath)      if (heapDumpFile.length() == 0L) {        null      } else {        heapDumpFile      }    } catch (e: Exception) {      null    } finally {    }  }

這段程式碼中涉及到了專為LeakCanary設計的Shark庫的用法,在這裡就不多解釋了。大概介紹一下每一步的作用:

首先呼叫HprofHeapGraph.indexHprof方法,這個方法會把dump出來的各種例項instance,Class類物件和Array物件等都建立起查詢的索引,以record的id作為key,把需要的資訊都儲存在Map中便於後續取用呼叫findLeakInput.findLeak方法,這個方法會從GC Root開始查詢,找到最短的一條導致洩露的引用鏈,然後再根據這條引用鏈構建出LeakTrace。把查詢出來的LeakTrace對外展示總結

本篇文章分析了LeakCanary檢測記憶體洩露的思路和一些程式碼的設計思想,但是限於篇幅不能面面俱到。接下來我們回答一下文章開頭提出的問題。

1.LeakCanary是如何使用ObjectWatcher 監控生命週期的?LeakCanary使用了Application的ActivityLifecycleCallbacks和FragmentManager的FragmentLifecycleCallbacks方法進行Activity和Fragment的生命週期檢測,當Activity和Fragment被回撥onDestroy以後就會被ObjectWatcher生成KeyedReference來檢測,然後藉助HeapDumpTrigger的輪詢和觸發gc的操作找到彈出提醒的時機。

2.LeakCanary如何dump和分析.hprof檔案的?使用Android平臺自帶的Debug.dumpHprofData方法獲取到hprof檔案,使用自建的Shark庫進行解析,獲取到LeakTrace

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • React vs Vue —應用程式建立和顯示