说明
- 基于 LeakCanary 2 的 main branch 分析,5e56f44
- 目前在尝试不贴源码直接讲核心内容
问题
- 如何监视 Activity、Fragment、ViewModel 泄漏?
- 如何得到 Heap Dump(.hprof)?
- 如何分析 Heap Dump?
- 为什么 LeakCanary 2 比 1 分析性能要高那么多?
如何监视 Activity、Fragment、ViewModel 泄漏
触发监视
涉及代码:
ActivityDestroyWatcher.kt -> install
FragmentDestroyWatcher.kt -> AndroidXFragmentDestroyWatcher.kt -> invoke
ViewModelClearedWatcher.kt -> install
Activity
- 在
ActivityLifecycleCallbacks#onActivityDestroyed时触发对Activity的监视
Fragment
- 通过
ActivityLifecycleCallbacks#onActivityCreated获取Activity的supportFragmentManager并注册FragmentLifecycleCallbacks - 在
onFragmentViewDestroyed时触发对Fragment的监视
ViewModel
- 为每个
ViewModelStoreOwner创建ViewModelProvider,并指定自定义的ViewModelProvider.Factory。在Factory#create方法中创建自定义的ViewModel(ViewModelClearedWatcher),用以监听ViewModel#onCleared - 在
onCleared被触发时,对ViewModelStoreOwner.viewModelStore.mMap.values进行监视(values 存放的是ViewModelStoreOwner包含的所有ViewModel)
获取 ViewModelStoreOwner 的方式:
- 通过
ActivityLifecycleCallbacks#onActivityCreated获取 Activity(ViewModelStoreOwner) - 通过
FragmentLifecycleCallbacks#onFragmentCreated获取 Fragment(ViewModelStoreOwner)
判断泄漏
涉及代码:
ObjectWatcher.kt -> watch
- 对某个对象监视时,调用的是
ObjectWatcher#watch。该方法中会以目标对象、ReferenceQueue作为参数创建对应的KeyedWeakReference并将其加入到 Map 中,然后执行延时检测(默认延迟 5s) - 5s 后,先将
ReferenceQueue中存在的KeyedWeakReference(WeakReference持有的 obj 被 GC 时,WeakReference会加入到 Queue 中)全部从 Map 中移除 - 移除后,若被监视对象在 Map 中仍存在对应的
KeyedWeakReference,则证明该对象泄漏(会调用InternalLeakCanary#onObjectRetained通知泄漏)
如何得到 Heap Dump(.hprof)
涉及代码:
InternalLeakCanary.kt -> onObjectRetained
HeapDumpTrigger.kt -> scheduleRetainedObjectCheck
AndroidHeapDumper.kt -> dumpHeap
- 在真正 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 的核心流程:
- 解析 hprof 的 HEADER(HEADER 中最关键的信息是 ID,表示所有数据的 ID 字段(object id、string id 等)对应的位数
- 第一轮解析
- 统计 CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 的个数及各自单条数据的最大占用空间
- 统计所有 CLASS_DUMP 的 static fields 和 fileds 数据的最大占用空间
- 根据各数据的最大占用 size,计算表述 size 的数值以 byte 表示所需的位数
- 根据各类型数据的关键信息所需的总 byte 数生成四个容器(
UnsortedByteEntries) - 第二轮解析
- 缓存所有字符串
- 缓存所有 GcRoot
- 分别缓存 CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 每条数据的起始位、占用空间等关键信息
- 根据所有被监视对象的 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(记住这点)的代码
主要的点:
- 分析大部分的 TAG(hprof 中不同数据的标识),解析其数据转成对应的对象(
ClassObj、ClassInstance等)并存放到容器中。此过程中,class loader object ID、stack trace serial number等不常用信息都会存放在对象中 - 会生成所有对象的引用链
- 使用 trove4j 的容器进行存储(key 为原始类型),避免拆箱装箱
LeakCanary 2
使用的是 shark
主要的点:
- 只分析 STRING_IN_UTF8、LOAD_CLASS、CLASS_DUMP、INSTANCE_DUMP、OBJECT_ARRAY_DUMP、PRIMITIVE_ARRAY_DUMP 及其他 16 个 GCRoot 相关的 TAG
- 两轮解析只保存了每组数据的 ID、占用空间等核心索引信息
- 按需要监视对象的 object id 来查找引用链,并以引用链上相关 obj 的索引信息读取、生成对应的对象(
HeapClass、HeapInstance等)。此过程中,只会保存核心部分信息 - 自定义容器存储数据(key 为原始类型),避免拆箱装箱
UnsortedByteEntries储存的主要是ByteArray(HotSpot 64 位虚拟机中每个对象头占 12 bytes,特殊的还要考虑 padding。以 ByteArray 直接存储数据,每组数据至少能节省 12 bytes)
对比
- shark 没有解析 STACK_FRAME、STACK_TRACE、START_THREAD
- shark 按需取必要数据,而 haha 将大部分数据都缓存在内存中
- shark 按引用链来读取所需对象的实际数据,并且只会读取必要的分析数据(
class loader object ID、stack 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