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佔用的內存還是挺大的。

轉:http://www.alonemonkey.com/2016/05/15/fbretaincycledetector-analyse/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章