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

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

写给程序员的内存泄漏治理手册中我们介绍了android内存泄漏的原理以及治理方案。通过上一节的学习我们可以做到尽可能的避免写出有可能内存泄漏的代码。但是实际开发过程中,由于一个项目往往有多人一起开发,以及有时候项目开发节奏比较快,所以项目开发过程中依然很有可能会出现一些内存泄漏问题,但是内存泄漏问题往往比较隐蔽,不容易发现。所以这里就介绍一款非常好用的内存泄漏检测工具LeakCanary

LeakCanary官方网站: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'
}

上面这个是官网中给出的依赖方式,目的是为了防止大家在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. 先创建一个单例类

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()会执行,但是单例类中依然持有SecondActivity的引用,这时候SecondActivity就会出现内存泄漏

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监测到内存泄漏后会发送一个通知
内存泄漏通知
  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能够实时地帮我们监测出程序中内存泄漏问题,且定位非常准确,可以说是非常的强大!既然这个工具如此强大,所以我们在使用的同时,最好也能理解其中的原理,这样才能真正融会贯通,为己所用

LeakCanary原理

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

用过LeakCanary1.x的同学一定知道,过去LeakCanary初始化的时候都是需要在Application中调用LeakCanary.install()进行注册的,升级到2.0之后连注册的代码都省了。那LeanCanary2.0是如何生效的呢?

LeakCanary初始化

查看LeakCanary源码,依然发现了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. 如果要在非主进程监听内存泄漏,需要手动调用AppWatcher#manualInstall()方法

既然这个AppWatcherInstaller会在AndroidManifest.xml中被注册,那么它一定是四大组件之一,查看源码发现,其实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
    //执行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构建经过manifest-merge后会合并多个清单文件,这个ContentProvider会被合并到唯一的manifest.xml中. 当APP初始化时会加载这个LeakSentryInstaller,就会自动帮我们执行InternalLeakSentry.install(application)

作为ContentProvider他的其他CRUD实现都是空的,作者只是巧妙利用了ContentProvider无需显式初始化的特性(对比Service、BroadcastReceiver)来实现了自动注册

如何检测Activity内存泄漏

我们再来看看InternalAppWatcher#install()方法做了什么

InternalAppWatcher.java

这个方法中比较关键的就是ActivityDestroyWatcher#install()FragmentDestroyWatcher#install()

这两个方法内部实现思路相似,所以就这里只分析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的生命周期进行监听

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) 
     //生命周期监听
   application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

ObjectWatcher.java

源码看到目前为止,小结一下其实就是Activity会在onDestory()之后,调用下面的ObjectWatcher#watch(),这个方法比较关键,不仅仅可以检测Activity的内存泄漏,还可以通过这个方法检测任何对象的内存泄漏

@Synchronized fun watch(
  watchedObject: Any,
  name: String
) {
  if (!isEnabled()) {
    return
  }
  //1.移除弱可达引用
  //弱可达:一个对象只被弱引用所引用,由于弱引用的特性,这样的对象是不会出现内存泄漏的
  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保存到一个数组中
  watchedObjects[key] = reference
  checkRetainedExecutor.execute {
    //4.五秒后执行后续流程(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?
    // 已经回收掉的弱引用对象会存放在RefrenceQueue中,循环移除
    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:如何判断一个对象是否被回收

其实注释中已经基本解释了

如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue中,所以当ReferenceQueue中有这个对象就代表这个对象已经被回收,反之就是没有被回收

思考2: 这里为什么要延迟五秒执行任务

我们都知道GC不是即时的, 页面销毁后预留5秒的时间给GC操作, 再后续分析引用泄露, 避免无效的分析

HeapDumpTrigger.java

//仅展示关键代码
private fun checkRetainedObjects(reason: String) {
    ...
  //移除弱可达对象后,统计中还剩下的引用数
  var retainedReferenceCount = objectWatcher.retainedObjectCount

  if (retainedReferenceCount > 0) {
    //手动进行一次GC
    gcTrigger.runGc()
    //在GC后, 再次统计剩下的引用数
    //到这一步剩下的就是没被回收掉的就是可能发生泄露的引用. 需要后续的dump分析
    retainedReferenceCount = objectWatcher.retainedObjectCount
  }
    
  //判断当前泄露实例个数如果小于5个,仅仅只是给用户一个通知,不会进行heap dump 操作,并在5s后再次发起检测
  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 后再次发起检测
        scheduleRetainedObjectCheck(
            "Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
}

生成heap dump 文件

AndroidHeapDumper.java

这个过程主要就是两步

1.发送通知

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.发送通知
  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无需显式初始化的特性来实现了自动注册
  2. 通过application#registerActivityLifecycleCallbacks()对Activity的生命周期进行监听
  3. Activity销毁时,将Activity添加到一个WeakReference中,利用WeakReferenceReferenceQueue的特性,如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue中,所以当ReferenceQueue中有这个对象就代表这个对象已经被回收,反之就是没有被回收
  4. 调用Android原生提供的捕获堆转储的方法Debug.dumpHprofData(heapDumpFile.absolutePath)
  5. 使用解析库来分析 heap dump 文件
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350