FBRetainCycleDetector源码分析

概述

FBRetainCycleDetector是facebook开源的一个用来检测对象是否有强引用循环的静态库。

strong和weak

strongweak在声明中使用表示这是一个强引用还是弱引用对象。

  • 强引用:只要引用存在,对象就不能被销毁。
  • 弱引用:弱引用不会导致对象不能销毁,只要没有强引用了,对象就会销毁,对象销毁后,弱引用会自动设置为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MyObject1* obj1 = [MyObject1 new];
MyObject2* obj2 = [MyObject2 new];
MyObject3* obj3 = [MyObject3 new];

obj1.object1 = obj2;
obj1.name = @"obj1";
obj2.object2 = obj3;
obj2.name = @"obj2";
obj3.object3 = obj1;
obj3.name = @"obj3";

//初始化一个引用循环检测器
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
//增加需要检测的对象
[detector addCandidate:obj1];
//寻找引用循环
NSSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [detector findRetainCycles];
//输出结果
NSLog(@"%@", retainCycles);

运行结果如下:

1
2
3
4
5
6
7
2016-05-15 12:38:25.208 FBRetainCycleDetectorDemo[57524:6267406] {(
(
"-> _object3 -> MyObject1 ",
"-> _object1 -> MyObject2 ",
"-> _object2 -> MyObject3 "
)
)}

也就是说检测的对象存在引用循环。

执行流程

首先需要初始化一个FBRetainCycleDetector检测器,先来分析这个类都有什么接口及其功能。

1
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];

new 相当于[[alloc] init],所以会调用FBRetainCycleDetector的init函数。

1
2
3
4
5
6
7
- (instancetype)init
{
//默认初始化一个标准的过滤器,并检查NSTimer
return [self initWithConfiguration:
[[FBObjectGraphConfiguration alloc] initWithFilterBlocks:FBGetStandardGraphEdgeFilters()
shouldInspectTimers:YES]];
}

调用了initWithConfiguration:初始化了一个标准的过滤器,这个过滤器是为了过滤引用循环中的一些类和方法。

然后调用addCandidate添加一个需要被检测的对象。

1
2
3
4
5
6
7
- (void)addCandidate:(id)candidate
{
//初始化一个FBObjectiveCGraphElement对象
FBObjectiveCGraphElement *graphElement = FBWrapObjectGraphElement(candidate, _configuration);
//加入NSMutableArray
[_candidates addObject:graphElement];
}

跟进FBWrapObjectGraphElement函数,发现里面调用了-[FBObjectiveCGraphElement initWithObject:configuration:namePath:]方法,其中就是初始化一个FBObjectiveCGraphElement。

重点是findRetainCycles这个方法来查找是否存在引用循环。这个方法调用了findRetainCyclesWithMaxCycleLength:来根据指定的深度进行深度搜索,默认深度是10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length
{
NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *allRetainCycles = [NSMutableSet new];
//遍历需要检测的对象,逐个进行检测
for (FBObjectiveCGraphElement *graphElement in _candidates) {
NSSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [self _findRetainCyclesInObject:graphElement
stackDepth:length];
//合并集合
[allRetainCycles unionSet:retainCycles];
}
[_candidates removeAllObjects];

return allRetainCycles;
}

继续跟进_findRetainCyclesInObject:stackDepth:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
stackDepth:(NSUInteger)stackDepth
{
NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new];
//初始化深度搜索树中的一个节点
FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];

//根据需要检测的对象进行深度搜索
// We will be doing DFS over graph of objects

//保存当前DFS搜索树中的搜索路径
// Stack will keep current path in the graph
NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new];
//保存搜索路径中访问过的对象
// To make the search non-linear we will also keep
// a set of previously visited nodes.
NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new];
//增加根节点,从根节点开始搜索
// Let's start with the root
[stack addObject:wrappedObject];
//判断是否已经搜索完毕
while ([stack count] > 0) {
// Algorithm creates many short-living objects. It can contribute to few
// hundred megabytes memory jumps if not handled correctly, therefore
// we're gonna drain the objects with our autoreleasepool.
@autoreleasepool {
// Take topmost node in stack and mark it as visited
//访问搜索栈中的最上面一个节点
FBNodeEnumerator *top = [stack lastObject];
//添加到objectsOnPath
[objectsOnPath addObject:top];

// Take next adjecent node to that child. Wrapper object can
// persist iteration state. If we see that node again, it will
// give us new adjacent node unless it runs out of them
//寻找下一个未访问的节点
FBNodeEnumerator *firstAdjacent = [top nextObject];
//如果存在未访问到的节点
if (firstAdjacent) {
// Current node still has some adjacent not-visited nodes

BOOL shouldPushToStack = NO;

// Check if child was already seen in that path
//如果该节点已经存在被访问过的对象中,说明构成了retain cycle
if ([objectsOnPath containsObject:firstAdjacent]) {
// We have caught a retain cycle

// Ignore the first element which is equal to firstAdjacent, use firstAdjacent
// we're doing that because firstAdjacent has set all contexts, while its
// first occurence could be a root without any context
NSUInteger index = [stack indexOfObject:firstAdjacent];
NSInteger length = [stack count] - index;

if (index == NSNotFound) {
// Object got deallocated between checking if it exists and grabbing its index
shouldPushToStack = YES;
} else {
NSRange cycleRange = NSMakeRange(index, length);
NSMutableArray<FBNodeEnumerator *> *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy];
[cycle replaceObjectAtIndex:0 withObject:firstAdjacent];

// 1. Unwrap the cycle
// 2. Shift to lowest address (if we omit that, and the cycle is created by same class,
// we might have duplicates)
// 3. Shift by class (lexicographically)
//为了判断是不是同一个环
[retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]];
}
} else {
// Node is clear to check, add it to stack and continue
shouldPushToStack = YES;
}

if (shouldPushToStack) {
if ([stack count] < stackDepth) {
[stack addObject:firstAdjacent];
}
}
} else {
// Node has no more adjacent nodes, it itself is done, move on
[stack removeLastObject];
[objectsOnPath removeObject:top];
}
}
}
return retainCycles;
}

上面这个函数就是DFS深度搜索的代码,以搜索对象相关的所有属性构成的搜索树进行深度搜索,一旦发现构成了环就记录下,然后继续搜索直接到达到指定的深度或搜索完毕。以上面的例子为例,搜索图是这样的:

图中从obj1节点开始搜索,依次遍历,n1、obj2、n2、obj3、n3、obj1,然后发现构成了环,然后保存环节点。

获取强引用

怎样获取自己所持有的所有引用的对象,FBRetainCycleDetector把传入的对象都封装成了一个FBObjectiveCGraphElement对象,根据对象的不同类型分为派生出子类FBObjectiveCBlock(Block)对象,FBObjectiveCBlock对象以及FBObjectiveCNSCFTimer(NSTimer)对象,不同对象获取所持有的引用时都会调用父类的allRetainedObjects,然后再进行自己的处理。

先来看看FBObjectiveCGraphElement的allRetainedObjects方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSSet *)allRetainedObjects
{
NSArray *retainedObjectsNotWrapped = [FBAssociationManager associationsForObject:_object];
NSMutableSet *retainedObjects = [NSMutableSet new];

for (id obj in retainedObjectsNotWrapped) {
[retainedObjects addObject:FBWrapObjectGraphElementWithContext(obj,
_configuration,
@[@"__associated_object"])];
}

return retainedObjects;
}

[FBAssociationManager associationsForObject:]获取该对象所有通过objc_setAssociatedObject关联的对象。因为后者会增加对象的引用,为了做到这一点,必须main.m中调用[FBAssociationManager hook]进行fishhook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)hook
{
#if _INTERNAL_RCD_ENABLED
std::lock_guard<std::mutex> l(*FB::AssociationManager::hookMutex);
rcd_rebind_symbols((struct rcd_rebinding[2]){
{
"objc_setAssociatedObject",
(void *)FB::AssociationManager::fb_objc_setAssociatedObject,
(void **)&FB::AssociationManager::fb_orig_objc_setAssociatedObject
},
{
"objc_removeAssociatedObjects",
(void *)FB::AssociationManager::fb_objc_removeAssociatedObjects,
(void **)&FB::AssociationManager::fb_orig_objc_removeAssociatedObjects
}}, 2);
FB::AssociationManager::hookTaken = true;
#endif //_INTERNAL_RCD_ENABLED
}

该函数hook了objc_setAssociatedObjectobjc_removeAssociatedObjects这两个C函数来监控。接下来看看子类的不同处理。

FBObjectiveCObject

FBObjectiveCObject的allRetainedObjects方法中首先调用了_unfilteredRetainedObjects获取所有引用对象,然后调用了filterObjects:调用过滤接口来进行过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
- (NSArray *)_unfilteredRetainedObjects
{
Class aCls = object_getClass(self.object);
if (!self.object || !aCls) {
return nil;
}

//获取一个对象的所有强引用属性
NSArray *strongIvars = FBGetObjectStrongReferences(self.object);

NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy];

//获取强引用的引用对象
for (id<FBObjectReference> ref in strongIvars) {
id referencedObject = [ref objectReferenceFromObject:self.object];

if (referencedObject) {
NSArray<NSString *> *namePath = [ref namePath];
[retainedObjects addObject:FBWrapObjectGraphElementWithContext(referencedObject,
self.configuration,
namePath)];
}
}

if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) {
/**
If we are dealing with toll-free bridged collections, we are not guaranteed that the collection
will hold only Objective-C objects. We are not able to check in runtime what callbacks it uses to
retain/release (if any) and we could easily crash here.
*/
return retainedObjects;
}

if (class_isMetaClass(aCls)) {
// If it's a meta-class it can conform to following protocols,
// but it would crash when trying enumerating
return nil;
}

//获取集合类的引用
if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) {
BOOL retainsKeys = [self _objectRetainsEnumerableKeys];
BOOL retainsValues = [self _objectRetainsEnumerableValues];

BOOL isKeyValued = NO;
if ([aCls instancesRespondToSelector:@selector(objectForKey:)]) {
isKeyValued = YES;
}

/**
This codepath is prone to errors. When you enumerate a collection that can be mutated while enumeration
we fall into risk of crash. To save ourselves from that we will catch such exception and try again.
We should not try this endlessly, so at some point we will simply give up.
*/
NSInteger tries = 10;
for (NSInteger i = 0; i < tries; ++i) {
// If collection is mutated we want to rollback and try again - let's keep refs in temporary set
NSMutableSet *temporaryRetainedObjects = [NSMutableSet new];
@try {
for (id subobject in self.object) {
if (retainsKeys) {
[temporaryRetainedObjects addObject:FBWrapObjectGraphElement(subobject, self.configuration)];
}
if (isKeyValued && retainsValues) {
[temporaryRetainedObjects addObject:FBWrapObjectGraphElement([self.object objectForKey:subobject],
self.configuration)];
}
}
}
@catch (NSException *exception) {
// mutation happened, we want to try enumerating again
continue;
}

// If we are here it means no exception happened and we want to break outer loop
[retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]];
break;
}
}

return retainedObjects;
}

该函数首先调用父类方法allRetainedObjects获取associated objects。然后调用FBGetObjectStrongReferences获取强引用的属性,并得到属性的对象,最后判断是否为集合来获取集合里面的引用。FBGetObjectStrongReferences内部调用了FBGetStrongReferencesForClass通过类来获取强引用,后者先通过FBGetClassReferences获取所有引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) {
NSMutableArray<id<FBObjectReference>> *result = [NSMutableArray new];

unsigned int count;
//获取变量列表
Ivar *ivars = class_copyIvarList(aCls, &count);

for (unsigned int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
//包装成FBIvarReference
FBIvarReference *wrapper = [[FBIvarReference alloc] initWithIvar:ivar];
//针对struct类型的处理
if (wrapper.type == FBStructType) {
NSString *encoding = @(ivar_getTypeEncoding(wrapper.ivar));
NSArray<FBObjectInStructReference *> *references = FBGetReferencesForObjectsInStructEncoding(wrapper, encoding);

[result addObjectsFromArray:references];
} else {
[result addObject:wrapper];
}
}
free(ivars);

return [result copy];
}

获得所有引用后,再通过class_getIvarLayout来提取强引用。

FBObjectiveCBlock

流程类似先获取引用然后过滤,只是获取强引用的方法是通过FBGetBlockStrongReferences来实现的。判断是不是Block的方式是新建一个空的Block然后判断该对象的是不是其子类。具体获取方法_GetBlockStrongLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static NSIndexSet *_GetBlockStrongLayout(void *block) {
struct BlockLiteral *blockLiteral = block;

/**
BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains
objects that are not pointer aligned, so omit them.

!BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and
we are not able to blackbox it.
*/
if ((blockLiteral->flags & BLOCK_HAS_CTOR)
|| !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
return nil;
}

void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
const size_t ptrSize = sizeof(void *);

// Figure out the number of pointers it takes to fill out the object, rounding up.
const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

// Create a fake object of the appropriate length.
void *obj[elements];
void *detectors[elements];

for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}

@autoreleasepool {
dispose_helper(obj);
}

// Run through the release detectors and add each one that got released to the object's
// strong ivar layout.
NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];

for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
if (detector.isStrong) {
[layout addIndex:i];
}

// Destroy detectors
[detector trueRelease];
}

return layout;
}

通过block的size_t大小创建相同大小的数组类型,其对象类型是FBBlockStrongRelationDetector,然后调用dispose_helper,根据判断是否调用了FBBlockStrongRelationDetector对象的release方法来判断是不是强引用,最后返回一个表示位置的数组,然后根据该数组获取具体的对象

这里使用了一个黑盒技术。创建一个对象来假扮想要调查的Block。因为知道Block的接口,知道在哪可以找到Block持有的引用。伪造的对象将会拥有“释放检测(release detectors)”来代替这些引用。释放检测器是一些很小的对象,这里是FBBlockStrongRelationDetector,它们会观察发送给它们的释放消息。当持有者想要放弃它的持有的时候,这些消息会发送给强引用对象。当释放伪造的对象的时候,可以检测哪些检测器接收到了这些消息。只要知道哪些索引在伪造的对象的检测器中,就可以找到原来Block中实际持有的对象。

image

FBObjectiveCNSCFTimer

主要是通过CFRunLoopTimerGetContext获取CFRunLoopTimerContext然后获取targetuserInfo

过滤配置器

FBObjectGraphConfiguration是一个过滤的配置器,根据传入的Block来过滤的Block调用,以及是否检查NSTimer。传入FBGraphEdgeFilterBlock的定义如下:

1
2
typedef FBGraphEdgeType (^FBGraphEdgeFilterBlock)(FBObjectiveCGraphElement *_Nullable fromObject,
FBObjectiveCGraphElement *_Nullable toObject);

fromObject是传入的对象,toObject是传入对象引用的对象,根据指定的规则来判断是否需要过滤,过滤的话就相当于断掉两个节点之间的连线,来看看官方的使用例子:

1
2
3
4
5
6
7
8
9
10
11
NSMutableArray *filters = @[
FBFilterBlockWithObjectIvarRelation([UIView class], @"_subviewCache"),
];

// Configuration object can describe filters as well as some options
FBObjectGraphConfiguration *configuration =
[[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filters
shouldInspectTimers:YES];
FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration];
[detector addCandidate:myObject];
NSSet *retainCycles = [detector findRetainCycles];

这里过滤的是UIView类的_subviewCache属性的引用。

总结

FBRetainCycleDetector目前来说肯定还存在一些问题,还有引用关系比较复杂的话,DFS占用的内存还是挺大的。

AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!