FBRetainCycleDetector 是 Facebook 开源的用于检查循环引用的库。简单来说它的工作原理就是从传入的目标对象开始查找其强引用的对象列表,在这个强引用对象列表里继续查找强引用对象,默认查找 10 层。最后在整个有向图中应用 DFS 算法查找环。如果有向图存在环,则说明目标对象存在循环引用。
其中查找强引用对象主要涉及到 NSObject 和 NSBlock 的强引用列表,需要分析 NSObject、NSBlock 类的内存布局,这也是该框架的核心内容。除此之外还有 Struct、NSTimer、Associated 等细分场景。
用法示例
1 | _RCDTestClass *testObject = [_RCDTestClass new]; |
创建 FBRetainCycleDetector
对象,将需要检查的目标对象通过 addCandidate
方法添加。然后调用 findRetainCycles
方法即可获得循环引用结果:
1 | {( |
分层概览
以下是 FBRetainCycleDetector 源码的文件夹分层概览。大体介绍下各部分职责,从上往下逐渐底层。
1、Associations
文件夹:通过 fishhook hook objc_setAssociatedObject
、objc_removeAssociatedObjects
方法,在 hook 方法里根据 OBJC_ASSOCIATION_RETAIN
、OBJC_ASSOCIATION_RETAIN_NONATOMIC
判断拿到强引用对象。(其中 fishhook 的分析可见上一篇博客:fishhook 源码笔记)
2、Detector
文件夹:FBNodeEnumerator 继承自 NSEnumerator 实现链表结构。FBRetainCycleDetector 根据传入的目标对象开始查找其强引用的对象列表,在这个强引用对象列表里继续查找强引用对象,默认查找 10 层。最后在整个有向图中应用 DFS 算法查找环。如果有向图存在环,则说明目标对象存在循环引用。
3、Filtering
文件夹:提供默认的过滤逻辑。
4、Graph
文件夹:获取 NSObject 和 NSBlock 对象的强引用对象列表。其中对于 NSTimer,从 Context 中获取强引用对象。
5、Layout
文件夹:根据 Block 结构体获取 NSBlock 的强引用布局。根据 IvarLayout 获取 NSObject 的强引用布局。将 Ivar 封装成 FBIvarReference 对象进行处理。
1 | ├── FBRetainCycleDetector |
源码分析
以下来根据源码具体分析下 FBRetainCycleDetector 是怎样获取 NSObject、NSBlock 的强引用对象的。从下层开始往上分析。
Layout
Classes
1、FBIvarReference
介绍几个关键方法:
1 | - (id)objectReferenceFromObject:(id)object |
通过 object_getIvar 方法,获取 _ivar 指向的对象。在收集强引用对象列表时,遍历对象 ivar 列表,用这个方法来判断属性是否有值。
1 | - (FBType)_convertEncodingToType:(const char *)typeEncoding |
根据 typeEncoding 判断对象类型,是 Struct、NSBlock 或者普通的 NSObject 类型。typeEncoding 字符具体的对应关系可参考 Apple 文档:Type Encodings
1 | - (instancetype)initWithIvar:(Ivar)ivar |
接收 Ivar 参数的 init 方法。
根据 ivar_getName
获取 ivar name 字符串。
根据 ivar_getTypeEncoding
获取 typeEncoding。
根据 ivar_getOffset
获取 ivar 的偏移量。
有了 _offset,除去指针类型的大小,即可获得 ivar 的角标 index。
2、FBClassStrongLayout
1、FBGetClassReferences
类方法:该类方法用于收集目标类的所有 ivar,并封装成 FBObjectInStructReference 数组
1 | NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) { |
2、FBGetMinimumIvarIndex
类方法:该类方法用于获取目标类的 minimum Ivar index,即第一个 ivar 的角标,用于判断强引用 ivar 角标位置
1 | static NSUInteger FBGetMinimumIvarIndex(__unsafe_unretained Class aCls) { |
3、FBGetLayoutAsIndexesForDescription
类方法:该类方法用于收集给定 Ivar layout 里的强引用角标 Range 集合
1 | static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) { |
这个方法属于查找 NSObject 类型对象的强引用对象列表的关键方法。
以 FBRetainCycleDetectorTests.mm
单测里提供的 _RCDTestClass
类为例:
1 | typedef void (^_RCDTestBlockType)(); |
通过 class_getIvarLayout
方法获取的 Ivar layout,点进 class_getIvarLayout
方法能看到其返回值类型为 uint8_t
的指针:
1 | /** |
** class_getIvarLayout
方法返回值是指向 uint8_t 的指针。uint8_t 大小为一个字节,即 8 位,高 4 位表示非 __strong
所有权的实例变量数量,低 4 位表示 __strong
所有权的实例变量数量。**
从上图可看出 _RCDTestClass
类的 Ivar layout 为:\x03\x12
,即两组 uint8_t
:
第一段的 \x03
代表 0 个非 __strong
的实例变量,3 个 __strong
的实例变量
第一段的 \x12
代表 1 个非 __strong
的实例变量,2 个 __strong
的实例变量
注意:_RCDTestClass
类的 someStruct
为 struct,比较特殊。即使该属性声明为 assign,但其 model 为 id 类型的强引用,所以 class_getIvarLayout
方法的返回为 \x03\x12
。
具体的取值逻辑见上图,即:
第一次循环 *layoutDescription
为 \x03
,与操作 0xf0
之后右移 4 位获得高 4 位:0
;与操作 0xf
之后获得低 4 位:3
;
第二次循环 *layoutDescription
++ 之后 为 \x12
,与操作 0xf0
之后右移 4 位获得高 4 位:1
;与操作 0xf
之后获得低 4 位:2
最后集合结果为:
1 | <NSMutableIndexSet: 0x7fb65a71c6c0>[number of indexes: 5 (in 2 ranges), indexes: (1-3 5-6)] |
4、FBGetStrongReferencesForClass
类方法:该类方法用于收集目标类的强引用 ivar 封装成的 FBIvarReference 数组
1 | static NSArray<id<FBObjectReference>> *FBGetStrongReferencesForClass(Class aCls) { |
5、FBGetObjectStrongReferences
对象方法:该方法用于收集目标对象的类的强引用 ivar 封装成的 FBIvarReference 数组
1 | NSArray<id<FBObjectReference>> *FBGetObjectStrongReferences(id obj, |
Blocks
1、FBBlockInterface
根据 Clang 文档 定义的 Block 结构:
1 | enum { // Flags from BlockLiteral |
其中,flags
值为 BLOCK_HAS_CTOR
时,表示存在 c++ 函数,可能指针不对齐,过滤此类场景;值为 BLOCK_HAS_STRET
时,表示存在 dispose 函数,能够释放持有的对象。
其中,dispose_helper
函数即为 Block 释放其强持有对象的函数,通过该函数可模拟释放过程,辨别 Block 的哪些变量为强引用对象。
2、FBBlockStrongRelationDetector
该类的作用是用于找出 Block 持有的强引用对象。根据 Block 持有的对象一一包装成 FBBlockStrongRelationDetector
对象,通过调用 dispose_helper
函数模拟释放强引用对象过程辨别 Block 的哪些变量为强引用对象。
注意:该类因重写了 release 方法,需要标记为非 ARC 编译。podspec 声明及文件宏定义校验。
1 |
|
如代码示例,重写 release
函数,将成员变量 _strong
标记为 YES
,代表这是个强引用对象。另外 trueRelease
方法,执行真正的 release 操作。
3、FBBlockStrongLayout
1、_GetBlockStrongLayout
方法:该方法用于获取 Block 强持有对象的角标列表
1 | static NSIndexSet *_GetBlockStrongLayout(void *block) { |
2、FBGetBlockStrongReferences
方法:根据 _GetBlockStrongLayout
方法拿到的 block 强持有的角标获,取该角标位置处的对象,如果该位置存在值的话说明有强持有的对象
1 | NSArray *FBGetBlockStrongReferences(void *block) { |
Graph
有了上述章节提供的工具方法,即可查找对象的强引用列表了。主要是查找 NSObject 对象的 FBObjectiveCObject
及查找 NSBlock 对象的 FBObjectiveCBlock
。
1、FBObjectiveCObject
收集 NSObject 类别对象的强引用列表
1 | - (NSSet *)allRetainedObjects |
2、FBObjectiveCNSCFTimer
其中,对于 NSTimer 类型需要额外做些处理。通过 CFRunLoopTimerGetContext
方法获取 NSTimer 对象的 context
,将其 info
强转成我们自定义的 _FBNSCFTimerInfoStruct
结构体。从中获取 target
和 userInfo
信息,判断是否有值,有值说明存在强引用。
1 | typedef struct { |
3、FBObjectiveCBlock
收集 NSBlock 类别对象的强引用列表
1 | - (NSSet *)allRetainedObjects |
Associations
FBObjectiveCBlock
和 FBObjectiveCObject
都继承自 FBObjectiveCGraphElement
,包括 FBObjectiveCNSCFTimer
都通过父类方法从 FBAssociationManager
收集强引用对象。
FBAssociationManager
原理即通过 fishhook 工具 hook objc_setAssociatedObject
、objc_removeAssociatedObjects
方法,在 hook 方法里根据 OBJC_ASSOCIATION_RETAIN
、OBJC_ASSOCIATION_RETAIN_NONATOMIC
判断拿到强引用对象。
1 | static void fb_objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) { |
其实现思路和系统的 Associated 一致:维护一个全局的 Hash 表,接管系统的处理逻辑objc_setAssociatedObject
方法将 object 做为 key 存进全局的 Hash 表objc_getAssociatedObject
方法将 object 做为 key 从进全局的 Hash 表取出数据objc_removeAssociatedObjects
方法将 object 做为 key 从进全局的 Hash 表删除数据
1 | OBJC_EXPORT void |
Detector
FBRetainCycleDetector
的 findRetainCycles
方法根据之前 addCandidate
方法传入的目标对象开始查找其强引用的对象列表,在这个强引用对象列表里继续查找强引用对象,默认查找 10 层。最后在整个有向图中应用 DFS 算法查找环。如果有向图存在环,则说明目标对象存在循环引用。
查找有向图中是否存在环的 DFS 算法并不在本篇文章探究范围内,所以文章就到这吧。
至此,FBRetainCycleDetector 的源码笔记就结束了。