概述
FBRetainCycleDetector
是facebook开源的一个用来检测对象是否有强引用循环的静态库。
strong和weak
strong
和weak
在声明中使用表示这是一个强引用还是弱引用对象。
- 强引用:只要引用存在,对象就不能被销毁。
- 弱引用:弱引用不会导致对象不能销毁,只要没有强引用了,对象就会销毁,对象销毁后,弱引用会自动设置为nil。
- 当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使该对象还有weak类型的指针指向它。
- 一旦最后一个指向该对象的strong类型的指针离开,这个对象将被释放,如果这个时候还有weak指针指向该对象,则会清除所有剩余的weak指针。
在OC中strong就相当于retain属性,而weak相当于assign。使用weak也就是为了避免retain cycles,比如父类中含有子类对象(retain了子类),子类中又有父类的对象(子类又retain了父类),这样就会形成retain cycles,导致两个对象都无法release。而FBRetainCycleDetector做的事情就是去检测是否存在这样的retain cycles。下面是循环引用的一个例图:
在 Objective-C 中找循环引用类似于在一个有向无环图(directed acyclic graph)中找环, 而节点就是对象,边就是对象之间的引用(如果对象A持有对象B,那么,A到B之间就存在着引用)。我们的 Objective-C 对象已经在我们的图中,我们要做的就是用深度优先搜索遍历它。
使用方法
构建一个引用循环,然后使用FBRetainCycleDetector提供的接口来进行检测,具体代码如下:
1 | MyObject1* obj1 = [MyObject1 new]; |
运行结果如下:
1 | 2016-05-15 12:38:25.208 FBRetainCycleDetectorDemo[57524:6267406] {( |
也就是说检测的对象存在引用循环。
执行流程
首先需要初始化一个FBRetainCycleDetector检测器,先来分析这个类都有什么接口及其功能。
1 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; |
new 相当于[[alloc] init]
,所以会调用FBRetainCycleDetector的init函数。
1 | - (instancetype)init |
调用了initWithConfiguration:
初始化了一个标准的过滤器,这个过滤器是为了过滤引用循环中的一些类和方法。
然后调用addCandidate
添加一个需要被检测的对象。
1 | - (void)addCandidate:(id)candidate |
跟进FBWrapObjectGraphElement函数,发现里面调用了-[FBObjectiveCGraphElement initWithObject:configuration:namePath:]
方法,其中就是初始化一个FBObjectiveCGraphElement。
重点是findRetainCycles
这个方法来查找是否存在引用循环。这个方法调用了findRetainCyclesWithMaxCycleLength:
来根据指定的深度进行深度搜索,默认深度是10。
1 | - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length |
继续跟进_findRetainCyclesInObject:stackDepth:
1 | - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement |
上面这个函数就是DFS深度搜索的代码,以搜索对象相关的所有属性构成的搜索树进行深度搜索,一旦发现构成了环就记录下,然后继续搜索直接到达到指定的深度或搜索完毕。以上面的例子为例,搜索图是这样的:
图中从obj1节点开始搜索,依次遍历,n1、obj2、n2、obj3、n3、obj1,然后发现构成了环,然后保存环节点。
获取强引用
怎样获取自己所持有的所有引用的对象,FBRetainCycleDetector把传入的对象都封装成了一个FBObjectiveCGraphElement对象,根据对象的不同类型分为派生出子类FBObjectiveCBlock(Block)对象,FBObjectiveCBlock对象以及FBObjectiveCNSCFTimer(NSTimer)对象,不同对象获取所持有的引用时都会调用父类的allRetainedObjects,然后再进行自己的处理。
先来看看FBObjectiveCGraphElement的allRetainedObjects方法:
1 | - (NSSet *)allRetainedObjects |
[FBAssociationManager associationsForObject:]
获取该对象所有通过objc_setAssociatedObject
关联的对象。因为后者会增加对象的引用,为了做到这一点,必须main.m
中调用[FBAssociationManager hook]
进行fishhook。
1 | + (void)hook |
该函数hook了objc_setAssociatedObject
和objc_removeAssociatedObjects
这两个C函数来监控。接下来看看子类的不同处理。
FBObjectiveCObject
FBObjectiveCObject的allRetainedObjects
方法中首先调用了_unfilteredRetainedObjects
获取所有引用对象,然后调用了filterObjects:
调用过滤接口来进行过滤。
1 | - (NSArray *)_unfilteredRetainedObjects |
该函数首先调用父类方法allRetainedObjects
获取associated objects。然后调用FBGetObjectStrongReferences
获取强引用的属性,并得到属性的对象,最后判断是否为集合来获取集合里面的引用。FBGetObjectStrongReferences内部调用了FBGetStrongReferencesForClass
通过类来获取强引用,后者先通过FBGetClassReferences
获取所有引用。
1 | NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) { |
获得所有引用后,再通过class_getIvarLayout
来提取强引用。
FBObjectiveCBlock
流程类似先获取引用然后过滤,只是获取强引用的方法是通过FBGetBlockStrongReferences
来实现的。判断是不是Block的方式是新建一个空的Block然后判断该对象的是不是其子类。具体获取方法_GetBlockStrongLayout
。
1 | static NSIndexSet *_GetBlockStrongLayout(void *block) { |
通过block的size_t大小创建相同大小的数组类型,其对象类型是FBBlockStrongRelationDetector,然后调用dispose_helper
,根据判断是否调用了FBBlockStrongRelationDetector对象的release方法来判断是不是强引用,最后返回一个表示位置的数组,然后根据该数组获取具体的对象
这里使用了一个黑盒技术。创建一个对象来假扮想要调查的Block。因为知道Block的接口,知道在哪可以找到Block持有的引用。伪造的对象将会拥有“释放检测(release detectors)”来代替这些引用。释放检测器是一些很小的对象,这里是FBBlockStrongRelationDetector,它们会观察发送给它们的释放消息。当持有者想要放弃它的持有的时候,这些消息会发送给强引用对象。当释放伪造的对象的时候,可以检测哪些检测器接收到了这些消息。只要知道哪些索引在伪造的对象的检测器中,就可以找到原来Block中实际持有的对象。
FBObjectiveCNSCFTimer
主要是通过CFRunLoopTimerGetContext
获取CFRunLoopTimerContext
然后获取target
和userInfo
。
过滤配置器
FBObjectGraphConfiguration
是一个过滤的配置器,根据传入的Block来过滤的Block调用,以及是否检查NSTimer。传入FBGraphEdgeFilterBlock
的定义如下:
1 | typedef FBGraphEdgeType (^FBGraphEdgeFilterBlock)(FBObjectiveCGraphElement *_Nullable fromObject, |
fromObject是传入的对象,toObject是传入对象引用的对象,根据指定的规则来判断是否需要过滤,过滤的话就相当于断掉两个节点之间的连线,来看看官方的使用例子:
1 | NSMutableArray *filters = @[ |
这里过滤的是UIView
类的_subviewCache
属性的引用。
总结
FBRetainCycleDetector
目前来说肯定还存在一些问题,还有引用关系比较复杂的话,DFS占用的内存还是挺大的。