欲言又止!面试官:说一下LeakCanary的原理!

我的心是冰冰的

1. 背景

Android开发中,内存泄露时常有发生在,有可能是你自己写的,也有可能是三方库里面的.程序中已动态分配的堆内存由于某种特殊原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至程序崩溃等严重后果.本来Android内存就吃紧,还内存泄露的话,后果不堪设想.所以我们要尽量避免内存泄露,一方面我们要学习哪些常见场景下会发生内存泄露,一方面我们引入LeakCanary帮我们自动检测有内存泄露的地方。

LeakCanary是Square公司(对,又是这个公司,OkHttp和Retrofit等都是这家公司开源的)开源的一个库,通过它我们可以在App运行的过程中检测内存泄露,它把对象内存泄露的引用链也给开发人员分析出来了,我们去修复这个内存泄露非常方面.

LeakCanary直译过来是内存泄露的金丝雀,关于这个名字其实有一个小故事在里面.金丝雀,美丽的鸟儿.她的歌声不仅动听,还曾挽救过无数矿工的生命.17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感.空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡.当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为"瓦斯检测指标",以便在危险状况下紧急撤离. 同样的,LeakCanary这只"金丝雀"能非常敏感地帮我们发现内存泄露,从而避免OOM的风险.

2. 初始化

在引入LeakCanary的时候,只需要在build.gradle中加入下面这行配置即可:

// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'

That’s it, there is no code change needed! 我们不需要改动任何的代码,就这样,LeakCanary就已经引入进来了. 那我有疑问了?我们一般引入一个库都是在Application的onCreate中初始化,它不需要在代码中初始化,它是如何起作用的呢?

我只想到一种方案可以实现这个,就是它在内部定义了一个ContentProvider,然后在ContentProvider的里面进行的初始化。

咱验证一下: 引入LeakCanary之后,运行一下项目,然后在debug的apk里面查看AndroidManifest文件,搜一下provider定义.果然,我找到了:

<provider
    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
    android:enabled="@ref/0x7f040007"
    android:exported="false"
    android:authorities="com.xfhy.allinone.leakcanary-installer" />
<!--这里的@ref/0x7f040007对应的是@bool/leak_canary_watcher_auto_install-->
class AppWatcherInstaller : ContentProvider() {
    override fun onCreate(): Boolean {
        val application = context!!.applicationContext as Application
        AppWatcher.manualInstall(application)
        return true
    }
}

哈哈,果然是在ContentProvider里面进行的初始化.App在启动时会自动初始化ContentProvider,也就自动调用了AppWatcher.manualInstall()进行了初始化.一开始的时候,我觉得这样挺好的,挺优雅,后来发现好多三方库都这么干了.每个库一个ContentProvider进行初始化,有点冗余的感觉.后来Jetpack推出了App Startup,解决了这个问题,它就是基于这个原理进行的封装。

需要注意的是ContentProvider的onCreate执行时机比Application的onCreate执行时机还早.如果你想在其他时机进行初始化优化启动时间,也是可以的.只需要在app里重写@bool/leak_canary_watcher_auto_install的值为false即可.然后手动在合适的地方调用AppWatcher.manualInstall(application).但是LeakCanary本来就是在debug的时候用的,所以感觉优化启动时间不是那么必要。

3. 监听泄露的时机

LeakCanary自动检测以下对象的泄露:

  • destroyed Activity instances
  • destroyed Fragment instances
  • destroyed fragment View instances
  • cleared ViewModel instances

可以看到,检测的都是些Android开发中容易被泄露的东西.那么它是如何检测的,下面我们来分析一下

3.1 Activity

通过Application#registerActivityLifecycleCallbacks()注册Activity生命周期监听,然后在onActivityDestroyed()中进行objectWatcher.watch(activity,....)进行检测对象是否泄露.检测对象是否泄露这块后面单独分析。

3.2 Fragment、Fragment View

同样的,检测这2个也是需要监听周期,不过这次监听的是Fragment的生命周期,利用fragmentManager.registerFragmentLifecycleCallbacks可以实现.Fragment是在onFragmentDestroy()中检测Fragment对象是否泄露,Fragment View在onFragmentViewDestroyed()里面检测Fragment View对象是否泄露。

但是,拿到这个fragmentManager的过程有点曲折.

  • Android O以上,通过activity#getFragmentManager()获得.
    (AndroidOFragmentDestroyWatcher)
  • AndroidX中,通过activity#getSupportFragmentManager()获得.
    (AndroidXFragmentDestroyWatcher)
  • support包中,通过activity#getSupportFragmentManager()获得.
    (AndroidSupportFragmentDestroyWatcher)

可以看到,不同的场景下,取FragmentManager的方式是不同的.取FragmentManager的实现过程、注册Fragment生命周期、在onFragmentDestroyed和onFragmentViewDestroyed中检测对象是否有泄漏这一套逻辑,在不同的环境下,实现不同.所以把它们封装进不同的策略(对应着上面3种策略)中,这就是策略模式的应用.
因为上面获取FragmentManager需要Activity实例,所以这里还需要监听Activity生命周期,在onActivityCreated()中拿到Activity实例,从而拿到FragmentManager去监听Fragment生命周期。

//AndroidOFragmentDestroyWatcher.kt

override fun onFragmentViewDestroyed(
  fm: FragmentManager,
  fragment: Fragment
) {
  val view = fragment.view
  if (view != null && configProvider().watchFragmentViews) {
    objectWatcher.watch(
        view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
        "(references to its views should be cleared to prevent leaks)"
    )
  }
}

override fun onFragmentDestroyed(
  fm: FragmentManager,
  fragment: Fragment
) {
  if (configProvider().watchFragments) {
    objectWatcher.watch(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
    )
  }
}

3.3 ViewModel

在前面讲到的AndroidXFragmentDestroyWatcher中还会单独监听onFragmentCreated()

override fun onFragmentCreated(
  fm: FragmentManager,
  fragment: Fragment,
  savedInstanceState: Bundle?
) {
  ViewModelClearedWatcher.install(fragment, objectWatcher, configProvider)
}

install里面实际是通过fragment和ViewModelProvider生成一个ViewModelClearedWatcher,这是一个新的ViewModel,然后在这个ViewModel的onCleared()里面检测这个fragment里面的每个ViewModel是否存在泄漏

//ViewModelClearedWatcher.kt

init {
    // We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,
    // however that was added in 2.1.0 and we support AndroidX first stable release. viewmodel-2.0.0
    // does not have ViewModelStore#keys. All versions currently have the mMap field.
    //通过反射拿到该fragment的所有ViewModel
    viewModelMap = try {
      val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
      mMapField.isAccessible = true
      @Suppress("UNCHECKED_CAST")
      mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) {
      null
    }
  }

  override fun onCleared() {
    if (viewModelMap != null && configProvider().watchViewModels) {
      viewModelMap.values.forEach { viewModel ->
        objectWatcher.watch(
            viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
        )
      }
    }
  }

4. 监测对象是否泄露

在讲这个之前得先回顾一个知识点,Java中的WeakReference是弱引用类型,每当发生GC时,它所持有的对象如果没有被其他强引用所持有,那么它所引用的对象就会被回收,同时或者稍后的时间这个WeakReference会被入队到ReferenceQueue中.LeakCanary中检测内存泄露就是基于这个原理.

/**
 * Weak reference objects, which do not prevent their referents from being
 * made finalizable, finalized, and then reclaimed.  Weak references are most
 * often used to implement canonicalizing mappings.
 *
 * <p> Suppose that the garbage collector determines at a certain point in time
 * that an object is <a href="package-summary.html#reachability">weakly
 * reachable</a>.  At that time it will atomically clear all weak references to
 * that object and all weak references to any other weakly-reachable objects
 * from which that object is reachable through a chain of strong and soft
 * references.  At the same time it will declare all of the formerly
 * weakly-reachable objects to be finalizable.  At the same time or at some
 * later time it will enqueue those newly-cleared weak references that are
 * registered with reference queues.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */

public class WeakReference<T> extends Reference<T> {

    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

实现要点:

  • 当一个对象需要被回收时,生成一个唯一的key,将它们封装进KeyedWeakReference中,并传入自定义的ReferenceQueue。
  • 将key和KeyedWeakReference放入一个map中。
  • 过一会儿之后(默认是5秒)主动触发GC,将自定义的ReferenceQueue中的KeyedWeakReference全部移除(它们所引用的对象已被回收),并同时根据这些KeyedWeakReference的key将map中的KeyedWeakReference也移除掉。
  • 此时如果map中还有KeyedWeakReference剩余,那么就是没有入队的,也就是说这些KeyedWeakReference所对应的对象还没被回收.这是不合理的,这里就产生了内存泄露。
  • 将这些内存泄露的对象分析引用链,保存数据。

下面来看具体代码:

//ObjectWatcher.kt

/**
* Watches the provided [watchedObject].
*
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
    ......
    //移除引用队列中的所有KeyedWeakReference,同时也将其从map中移除
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)

    //存入map    
    watchedObjects[key] = reference
    
    //默认5秒之后执行moveToRetained()检查
    //这里是用的handler.postDelay实现的延迟
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
}

@Synchronized private fun moveToRetained(key: String) {
    //移除那些已经被回收的
    removeWeaklyReachableObjects()
    //判断一下这个key锁对应的KeyedWeakReference是否被移除了
    val retainedRef = watchedObjects[key]
    //没有被移除的话,说明是发生内存泄露了
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
}

需要被回收的Activity、Fragment什么的都会走watch()这个方法这里,检测是否有内存泄露发生.上面这块代码对应着实现要点的1-4步.接下来具体分析内存泄露了是怎么走的

//InternalLeakCanary#onObjectRetained()
//InternalLeakCanary#scheduleRetainedObjectCheck()
//HeapDumpTrigger#scheduleRetainedObjectCheck()
//HeapDumpTrigger#checkRetainedObjects()

private fun checkRetainedObjects() {
    //比如如果是在调试,那么暂时先不dump heap,延迟20秒再判断一下状态

    val config = configProvider()
    
    ......
    //还剩多少对象没被回收  这些对象可能不是已经泄露的
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      //手动触发GC,这里触发GC时还延迟了100ms,给那些回收了的对象入引用队列一点时间,好让结果更准确.
      gcTrigger.runGc()
      //再看看还剩多少对象没被回收
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    
    //checkRetainedCount这里有2中情况返回true,流程return.
    //1. 未被回收的对象数是0,展示无泄漏的通知
    //2. 当retainedReferenceCount小于5个,展示有泄漏的通知(app可见或不可见超过5秒),延迟2秒再进行检查checkRetainedObjects()
    //app可见是在VisibilityTracker.kt中判断的,通过记录Activity#onStart和onStop的数量来判断
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      //1分钟之内才dump过,再过会儿再来
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )
      scheduleRetainedObjectCheck(
          delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    //开始dump
    //通过 Debug.dumpHprofData(filePath)  dump heap
    //开始dump heap之前还得objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) 清除一下这次dump开始之前的所有引用
    //最后是用HeapAnalyzerService这个IntentService去分析heap,具体在HeapAnalyzerService#runAnalysis()
    dumpHeap(retainedReferenceCount, retry = true)
  }

HeapAnalyzerService 里调用的是 Shark 库对 heap 进行分析,分析的结果再返回到DefaultOnHeapAnalyzedListener.onHeapAnalyzed 进行分析结果入库、发送通知消息。

Shark 🦈 :Shark is the heap analyzer that powers LeakCanary 2. It's a Kotlin standalone heap analysis library that runs at 「high speed」 with a 「low memory footprint」.

5. 总结

LeakCanary是一只优雅的金丝雀,帮助我们监测内存泄露.本文主要分析了LeakCanary的初始化、监听泄露的时机、监测某个对象泄露的过程.源码中实现非常优雅,本文中未完全展现出来,比较源码太多贴上来不太雅观.读源码不仅能让我们学到新东西,而且也让我们以后写代码有可以模仿的对象,甚至还可以在面试时得心应手,一举三得。

文末

感谢大家关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
Android架构师系统进阶学习路线、58万字学习笔记、教学视频免费分享地址:我的GitHub
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解,助你早日升职加薪。
B站直通车:https://space.bilibili.com/547363040
本文源码地址:https://github.com/simplepeng/SpiderMan

原文作者:潇风寒月
原文链接:https://juejin.cn/post/6905285883298054157

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 本文基于 leakcanary-android:2.5 1. 背景 Android开发中,内存泄露时常有发生在,有...
    潇风寒月阅读 664评论 0 2
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,485评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,551评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,715评论 1 1
  • 在妖界我有个名头叫胡百晓,无论是何事,只要找到胡百晓即可有解决的办法。因为是只狐狸大家以讹传讹叫我“倾城百晓”,...
    猫九0110阅读 3,255评论 7 3