LeakCanary 2 分析

1,176 阅读5分钟

说明

  1. 基于 LeakCanary 2 的 main branch 分析,5e56f44
  2. 目前在尝试不贴源码直接讲核心内容

问题

  1. 如何监视 Activity、Fragment、ViewModel 泄漏?
  2. 如何得到 Heap Dump(.hprof)?
  3. 如何分析 Heap Dump?
  4. 为什么 LeakCanary 2 比 1 分析性能要高那么多?

如何监视 Activity、Fragment、ViewModel 泄漏

触发监视

涉及代码:

ActivityDestroyWatcher.kt -> install
FragmentDestroyWatcher.kt -> AndroidXFragmentDestroyWatcher.kt -> invoke
ViewModelClearedWatcher.kt -> install

Activity

  1. ActivityLifecycleCallbacks#onActivityDestroyed 时触发对 Activity 的监视

Fragment

  1. 通过 ActivityLifecycleCallbacks#onActivityCreated 获取 ActivitysupportFragmentManager 并注册 FragmentLifecycleCallbacks
  2. onFragmentViewDestroyed 时触发对 Fragment 的监视

ViewModel

  1. 为每个 ViewModelStoreOwner 创建 ViewModelProvider,并指定自定义的 ViewModelProvider.Factory。在 Factory#create 方法中创建自定义的 ViewModelViewModelClearedWatcher),用以监听 ViewModel#onCleared
  2. onCleared 被触发时,对 ViewModelStoreOwner.viewModelStore.mMap.values 进行监视(values 存放的是 ViewModelStoreOwner 包含的所有 ViewModel

获取 ViewModelStoreOwner 的方式:

  1. 通过 ActivityLifecycleCallbacks#onActivityCreated 获取 Activity(ViewModelStoreOwner
  2. 通过 FragmentLifecycleCallbacks#onFragmentCreated 获取 Fragment(ViewModelStoreOwner

判断泄漏

涉及代码:

ObjectWatcher.kt -> watch
  1. 对某个对象监视时,调用的是 ObjectWatcher#watch。该方法中会以目标对象、ReferenceQueue 作为参数创建对应的 KeyedWeakReference 并将其加入到 Map 中,然后执行延时检测(默认延迟 5s)
  2. 5s 后,先将 ReferenceQueue 中存在的 KeyedWeakReferenceWeakReference 持有的 obj 被 GC 时,WeakReference 会加入到 Queue 中)全部从 Map 中移除
  3. 移除后,若被监视对象在 Map 中仍存在对应的 KeyedWeakReference,则证明该对象泄漏(会调用 InternalLeakCanary#onObjectRetained 通知泄漏)

如何得到 Heap Dump(.hprof)

涉及代码:

InternalLeakCanary.kt -> onObjectRetained
HeapDumpTrigger.kt -> scheduleRetainedObjectCheck
AndroidHeapDumper.kt -> dumpHeap
  1. 在真正 dump Heap 之前,会尝试 GC 一次,再判断剩余对象个数,若大于等于阈值(默认 5 个),则会通过 Debug.dumpHprofData 获取 hprof

如何分析 Heap Dump

涉及代码:

HeapAnalyzerService.kt
HeapAnalyzer.kt
KeyedWeakReferenceFinder.kt // 主要用于提取被监视对象的 object id
AndroidReferenceMatchers.kt
ObjectInspectors.kt
AndroidObjectInspectors.kt
ProguardMappingReader.kt
ProguardMapping.kt
HprofHeapGraph.kt // 根据数据的 index、size 等解析出对应的数据
HprofInMemoryIndex.kt // 解析 hprof 文件的核心,主要生成 hprof 核心数据各自的 index
StreamingHprofReader.kt

分析 Heap 的逻辑都在 shark 相关的 module 中,主要讲一下 shark 的核心流程:

  1. 解析 hprof 的 HEADER(HEADER 中最关键的信息是 ID,表示所有数据的 ID 字段(object id、string id 等)对应的位数
  2. 第一轮解析
    • 统计 CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 的个数及各自单条数据的最大占用空间
    • 统计所有 CLASS_DUMP 的 static fields 和 fileds 数据的最大占用空间
  3. 根据各数据的最大占用 size,计算表述 size 的数值以 byte 表示所需的位数
  4. 根据各类型数据的关键信息所需的总 byte 数生成四个容器(UnsortedByteEntries
  5. 第二轮解析
    • 缓存所有字符串
    • 缓存所有 GcRoot
    • 分别缓存 CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 每条数据的起始位、占用空间等关键信息
  6. 根据所有被监视对象的 object id,找出其关联的最短引用路径

为什么 LeakCanary 2 比 1 分析性能要高那么多

Version 2.0 Alpha 1 (2019-04-23) 的 Change Log 中提到:

  • Uses 90% less memory and 6 times faster than the prior heap parser.

好奇这点而了解了下 LeakCanary 1 和 LeakCanary 2 的 Heap 分析流程:

LeakCanary 1

使用的是 haha,实际上是 Android Studio 中 Heap Analyzer(记住这点)的代码

主要的点:

  1. 分析大部分的 TAG(hprof 中不同数据的标识),解析其数据转成对应的对象(ClassObjClassInstance 等)并存放到容器中。此过程中,class loader object IDstack trace serial number 等不常用信息都会存放在对象中
  2. 会生成所有对象的引用链
  3. 使用 trove4j 的容器进行存储(key 为原始类型),避免拆箱装箱

LeakCanary 2

使用的是 shark

主要的点:

  1. 只分析 STRING_IN_UTF8、LOAD_CLASS、CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 及其他 16 个 GCRoot 相关的 TAG
  2. 两轮解析只保存了每组数据的 ID、占用空间等核心索引信息
  3. 按需要监视对象的 object id 来查找引用链,并以引用链上相关 obj 的索引信息读取、生成对应的对象(HeapClassHeapInstance 等)。此过程中,只会保存核心部分信息
  4. 自定义容器存储数据(key 为原始类型),避免拆箱装箱
  5. UnsortedByteEntries 储存的主要是 ByteArray(HotSpot 64 位虚拟机中每个对象头占 12 bytes,特殊的还要考虑 padding。以 ByteArray 直接存储数据,每组数据至少能节省 12 bytes)

对比

  • shark 没有解析 STACK_FRAME、STACK_TRACE、START_THREAD
  • shark 按需取必要数据,而 haha 将大部分数据都缓存在内存中
  • shark 按引用链来读取所需对象的实际数据,并且只会读取必要的分析数据(class loader object IDstack trace serial number 等不常用信息直接丢弃)

小结

所以 LeakCanary 2 的内存消耗、分析速度表现比 LeakCanary 1 好都是有理有据的,但直接以 LeakCanary 的场景对比 shark 和 haha 是不公平的。shark 针对的是特定的数据,而 haha 本身就是设计为要呈现 hprof 中所有信息(AS 的功能),两者面对的问题规模不同。

HRPOF FOMAT

没找到 1.0.3 的官方文档,只能直接看 AS 里面解析的源码或生成的代码,相对 1.0.2,新增了如下 TAG:

  • HEAP_DUMP_INFO
    • u1: Tag value (0xFE)
    • u4: heap ID
    • ID: heap name string ID
  • ROOT_INTERNED_STRING
    • u1: Tag value(0x89)
    • ID: object id
  • ROOT_FINALIZING
    • u1: Tag value(0x8a)
    • ID: object id
  • ROOT_DEBUGGER
    • u1: Tag value(0x8b)
    • ID: object id
  • ROOT_REFERENCE_CLEANUP
    • u1: Tag value(0x8c)
    • ID: object id
  • ROOT_VM_INTERNAL
    • u1: Tag value(0x8d)
    • ID: object id
  • ROOT_JNI_MONITOR
    • u1: Tag value(0x8e)
    • ID: object id
    • u4: thread serial number
    • u4: stack depth
  • ROOT_UNREACHABLE
    • u1: Tag value(0x90)
    • ID: object id