LeakCanary2.0使用及原理分析 — Kotlin重構版

如需轉載請評論或簡信,并注明出處,未經(jīng)允許不得轉載

目錄

前言

寫給程序員的內存泄漏治理手冊中我們介紹了android內存泄漏的原理以及治理方案。通過上一節(jié)的學習我們可以做到盡可能的避免寫出有可能內存泄漏的代碼。但是實際開發(fā)過程中,由于一個項目往往有多人一起開發(fā),以及有時候項目開發(fā)節(jié)奏比較快,所以項目開發(fā)過程中依然很有可能會出現(xiàn)一些內存泄漏問題,但是內存泄漏問題往往比較隱蔽,不容易發(fā)現(xiàn)。所以這里就介紹一款非常好用的內存泄漏檢測工具LeakCanary

LeakCanary官方網(wǎng)站:https://square.github.io/leakcanary/

LeakCanary的使用

添加依賴

LeakCanary升級到2.0之后,使用起來非常簡單,只需要在build.gradle中添加依賴

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

上面這個是官網(wǎng)中給出的依賴方式,目的是為了防止大家在release版本中使用,但是有些項目需要設置多個buildtype,那如果以上面這種方式集成就無法正常使用LeakCanary,這種情況可以使用下面這種集成方式

  1. 在自定義buildtype中設置debuggable true,并設置測試版本的簽名文件。或直接只用initWith debug
debug2{
    debuggable true
    ...
}
  1. 增加debug2Implementation
dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
  debug2Implementation 'com.squareup.leakcanary:leakcanary-android:2.0'
  ...
}

注意這里不要直接使用implementation,使用implementation雖然不會在正式版中彈出內存泄漏的彈窗(因為LeakCanary內部做了限制),但是會增大約1mb的安裝包體積

模擬內存泄漏

  1. 先創(chuàng)建一個單例類

SingleInstance.java

public class SingleInstance {
    private Context context;

    private SingleInstance(Context context) {
        this.context = context;
    }

    public static class Holder {
        private static SingleInstance INSTANCE;

        public static SingleInstance newInstance(Context context) {
            if (INSTANCE == null) {
                INSTANCE = new SingleInstance(context);
            }
            return INSTANCE;
        }
    }
}
  1. SecondActivity的引用加入到單例中

按back鍵回到MainActivity,這時候SecondActivityonDestory()會執(zhí)行,但是單例類中依然持有SecondActivity的引用,這時候SecondActivity就會出現(xiàn)內存泄漏

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        SingleInstance.Holder.newInstance(this);
    }
}
  1. LeakCanary監(jiān)測到內存泄漏后會發(fā)送一個通知
內存泄漏通知
  1. 點開通知,就可以看到詳細的內存泄漏堆棧信息
內存泄漏詳細信息

同樣我們也可以通過Logcat查看內存泄漏信息,如下所示

2019-12-30 16:36:40.130 20875-21568/com.geekholt.leakcanarydemo D/LeakCanary: ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    80988 bytes retained
    ┬
    ├─ android.os.HandlerThread
    │    Leaking: NO (PathClassLoader↓ is not leaking)
    │    Thread name: 'LeakCanary-Heap-Dump'
    │    GC Root: Local variable in native code
    │    ↓ thread HandlerThread.contextClassLoader
    ├─ dalvik.system.PathClassLoader
    │    Leaking: NO (Object[]↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ PathClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[]
    │    Leaking: NO (SingleInstance$Holder↓ is not leaking)
    │    ↓ array Object[].[757]
    ├─ com.geekholt.leakcanarydemo.SingleInstance$Holder
    │    Leaking: NO (a class is never leaking)
    │    ↓ static SingleInstance$Holder.INSTANCE
    │                                   ~~~~~~~~
    ├─ com.geekholt.leakcanarydemo.SingleInstance
    │    Leaking: UNKNOWN
    │    ↓ SingleInstance.context
    │                     ~~~~~~~
    ╰→ com.geekholt.leakcanarydemo.SecondActivity
         Leaking: YES (Activity#mDestroyed is true and ObjectWatcher was watching this)
         key = 2a99b464-42c4-4b8c-a0a8-5edb348f90bb
         watchDurationMillis = 10486
         retainedDurationMillis = 5485
    ====================================
    0 LIBRARY LEAKS
    
    Leaks coming from the Android Framework or Google libraries.
    ====================================
    METADATA
    
    Please include this in bug reports and Stack Overflow questions.
    
    Build.VERSION.SDK_INT: 28
    Build.MANUFACTURER: HUAWEI
    LeakCanary version: 2.0
    App process name: com.geekholt.leakcanarydemo
    Analysis duration: 11596 ms
    Heap dump file path: /data/user/0/com.geekholt.leakcanarydemo/files/leakcanary/2019-12-30_16-36-25_973.hprof
    Heap dump timestamp: 1577695000128
    ====================================

可以看出,LeakCanary能夠實時地幫我們監(jiān)測出程序中內存泄漏問題,且定位非常準確,可以說是非常的強大!既然這個工具如此強大,所以我們在使用的同時,最好也能理解其中的原理,這樣才能真正融會貫通,為己所用

LeakCanary原理

這里再提醒一下,本文的源碼都是基于LeakCanary2.0的哦

用過LeakCanary1.x的同學一定知道,過去LeakCanary初始化的時候都是需要在Application中調用LeakCanary.install()進行注冊的,升級到2.0之后連注冊的代碼都省了。那LeanCanary2.0是如何生效的呢?

LeakCanary初始化

查看LeakCanary源碼,依然發(fā)現(xiàn)了install()相關的代碼

AppWatcher.java

/**
 * [AppWatcher] is automatically installed on main process start by
 * [leakcanary.internal.AppWatcherInstaller] which is registered in the AndroidManifest.xml of
 * your app. If you disabled [leakcanary.internal.AppWatcherInstaller] or you need AppWatcher
 * or LeakCanary to run outside of the main process then you can call this method to install
 * [AppWatcher].
 */
fun manualInstall(application: Application) = InternalAppWatcher.install(application)

從這個方法的注釋中我們得出了以下信息:

  1. AppWatcher#manualInstall()會在主進程中自動被AppWatcherInstaller調用
  2. AppWatcherInstaller會在AndroidManifest.xml中被注冊
  3. 如果要在非主進程監(jiān)聽內存泄漏,需要手動調用AppWatcher#manualInstall()方法

既然這個AppWatcherInstaller會在AndroidManifest.xml中被注冊,那么它一定是四大組件之一,查看源碼發(fā)現(xiàn),其實AppWatcherInstaller就是ContentProvider的子類

AppWatcherInstaller.java

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller() {
    override fun onCreate(): Boolean {
      super.onCreate()
      AppWatcher.config = AppWatcher.config.copy(enabled = false)
      return true
    }
  }

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    //執(zhí)行LeakCanary初始化操作
    InternalAppWatcher.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
  }
}

APP構建經(jīng)過manifest-merge后會合并多個清單文件,這個ContentProvider會被合并到唯一的manifest.xml中. 當APP初始化時會加載這個LeakSentryInstaller,就會自動幫我們執(zhí)行InternalLeakSentry.install(application)

作為ContentProvider他的其他CRUD實現(xiàn)都是空的,作者只是巧妙利用了ContentProvider無需顯式初始化的特性(對比Service、BroadcastReceiver)來實現(xiàn)了自動注冊

如何檢測Activity內存泄漏

我們再來看看InternalAppWatcher#install()方法做了什么

InternalAppWatcher.java

這個方法中比較關鍵的就是ActivityDestroyWatcher#install()FragmentDestroyWatcher#install()

這兩個方法內部實現(xiàn)思路相似,所以就這里只分析ActivityDestroyWatcher#install()

fun install(application: Application) {
  SharkLog.logger = DefaultCanaryLog()
  SharkLog.d { "Installing AppWatcher" }
  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)
}

ActivityDestroyWatcher.java

這個方法實際上就是調用了application#registerActivityLifecycleCallbacks()對整個應用中的Activity的生命周期進行監(jiān)聽

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          //當activity onDestory的時候做處理
          objectWatcher.watch(activity)
        }
      }
    }

  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider) 
     //生命周期監(jiān)聽
   application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

ObjectWatcher.java

源碼看到目前為止,小結一下其實就是Activity會在onDestory()之后,調用下面的ObjectWatcher#watch(),這個方法比較關鍵,不僅僅可以檢測Activity的內存泄漏,還可以通過這個方法檢測任何對象的內存泄漏

@Synchronized fun watch(
  watchedObject: Any,
  name: String
) {
  if (!isEnabled()) {
    return
  }
  //1.移除弱可達引用
  //弱可達:一個對象只被弱引用所引用,由于弱引用的特性,這樣的對象是不會出現(xiàn)內存泄漏的
  removeWeaklyReachableObjects()
  val key = UUID.randomUUID()
      .toString()
  val watchUptimeMillis = clock.uptimeMillis()
  //2.將activity加入到WeakReference中
  val reference =
    KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
  SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (name.isNotEmpty()) " named $name" else "") +
          " with key $key"
  }
    //3.將reference保存到一個數(shù)組中
  watchedObjects[key] = reference
  checkRetainedExecutor.execute {
    //4.五秒后執(zhí)行后續(xù)流程(checkRetainedExecutor配置了watchDurationMillis是5秒,具體可以自己看代碼)
    moveToRetained(key)
  }
}

/**
 * 移除弱可達引用
 */
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.
    var ref: KeyedWeakReference?
    // 已經(jīng)回收掉的弱引用對象會存放在RefrenceQueue中,循環(huán)移除
    do {
      ref = queue.poll() as KeyedWeakReference?
      //如果RefrenceQueue里存在,說明這個弱引用對象被回收了
      if (ref != null) {
        val removedRef = watchedReferences.remove(ref.key)
        //如果watchedReferences中的這個弱引用對象被回收了,retainedReferences也移除掉這個弱引用
        if (removedRef == null) {
          retainedReferences.remove(ref.key)
        }
      }
    } while (ref != null)
}

思考1:如何判斷一個對象是否被回收

其實注釋中已經(jīng)基本解釋了

如果一個對象除了弱引用以外,沒有被其他對象所引用,當發(fā)生GC時,這個弱引用對象就會被回收,并且被回收掉的對象會被存放到ReferenceQueue中,所以當ReferenceQueue中有這個對象就代表這個對象已經(jīng)被回收,反之就是沒有被回收

思考2: 這里為什么要延遲五秒執(zhí)行任務

我們都知道GC不是即時的, 頁面銷毀后預留5秒的時間給GC操作, 再后續(xù)分析引用泄露, 避免無效的分析

HeapDumpTrigger.java

//僅展示關鍵代碼
private fun checkRetainedObjects(reason: String) {
    ...
  //移除弱可達對象后,統(tǒng)計中還剩下的引用數(shù)
  var retainedReferenceCount = objectWatcher.retainedObjectCount

  if (retainedReferenceCount > 0) {
    //手動進行一次GC
    gcTrigger.runGc()
    //在GC后, 再次統(tǒng)計剩下的引用數(shù)
    //到這一步剩下的就是沒被回收掉的就是可能發(fā)生泄露的引用. 需要后續(xù)的dump分析
    retainedReferenceCount = objectWatcher.retainedObjectCount
  }
    
  //判斷當前泄露實例個數(shù)如果小于5個,僅僅只是給用戶一個通知,不會進行heap dump 操作,并在5s后再次發(fā)起檢測
  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
    ...
  //生成內存泄漏堆棧信息
  val heapDumpFile = heapDumper.dumpHeap()
    .....
}

private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int   // retainedVisibleThreshold默認為 5 個
  ): Boolean {
    val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
    lastDisplayedRetainedObjectCount = retainedKeysCount
    if (retainedKeysCount == 0) {
      SharkLog.d { "No retained objects" }
      if (countChanged) {
        showNoMoreRetainedObjectNotification()
      }
      return true
    }
    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        SharkLog.d {
            "Found $retainedKeysCount retained objects, which is less than the visible threshold of $retainedVisibleThreshold"
        }
        // // 通知用戶 "App visible, waiting until 5 retained instances"
        showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)
        // 5s 后再次發(fā)起檢測
        scheduleRetainedObjectCheck(
            "Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
}

生成heap dump 文件

AndroidHeapDumper.java

這個過程主要就是兩步

1.發(fā)送通知

2.使用Debug.dumpHprofData(heapDumpFile.absolutePath)捕獲堆轉儲

override fun dumpHeap(): File? {
  val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null

  val waitingForToast = FutureResult<Toast?>()
  showToast(waitingForToast)

  if (!waitingForToast.wait(5, SECONDS)) {
    SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
    return null
  }

  //1.發(fā)送通知
  val notificationManager =
    context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  if (Notifications.canShowNotification) {
    val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
    val builder = Notification.Builder(context)
        .setContentTitle(dumpingHeap)
    val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
    notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
  }

  val toast = waitingForToast.get()

  return try {
    //2.捕獲堆轉儲
    Debug.dumpHprofData(heapDumpFile.absolutePath)
    if (heapDumpFile.length() == 0L) {
      SharkLog.d { "Dumped heap file is 0 byte length" }
      null
    } else {
      heapDumpFile
    }
  } catch (e: Exception) {
    SharkLog.d(e) { "Could not dump heap" }
    // Abort heap dump
    null
  } finally {
    cancelToast(toast)
    notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
  }
}

分析 heap dump 文件

最后啟動一個前臺服務 HeapAnalyzerService 來分析 heap dump 文件。然后通過解析庫找到最短 GC Roots 引用路徑,展示給用戶。這里不是我們的重點,就不做具體分析了,感興趣的可以自己看一下源碼

總結

  1. LeakCanary2.0利用了ContentProvider無需顯式初始化的特性來實現(xiàn)了自動注冊
  2. 通過application#registerActivityLifecycleCallbacks()對Activity的生命周期進行監(jiān)聽
  3. Activity銷毀時,將Activity添加到一個WeakReference中,利用WeakReferenceReferenceQueue的特性,如果一個對象除了弱引用以外,沒有被其他對象所引用,當發(fā)生GC時,這個弱引用對象就會被回收,并且被回收掉的對象會被存放到ReferenceQueue中,所以當ReferenceQueue中有這個對象就代表這個對象已經(jīng)被回收,反之就是沒有被回收
  4. 調用Android原生提供的捕獲堆轉儲的方法Debug.dumpHprofData(heapDumpFile.absolutePath)
  5. 使用解析庫來分析 heap dump 文件
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373