如何在 iOS 中解決循環引用的問題

關注倉庫,及時得到更新:iOS-Source-Code-Analyzegit

Follow: Draveness · Githubgithub

稍有常識的人都知道在 iOS 開發時,咱們常常會遇到循環引用的問題,好比兩個強指針相互引用,可是這種簡單的狀況做爲稍有經驗的開發者都會輕鬆地查找出來。算法

可是遇到下面這樣的狀況,若是隻看其實現代碼,也很難僅僅憑藉肉眼上的觀察以及簡單的推理就能分析出其中存在的循環引用問題,更況且真實狀況每每比這複雜的多:數組

testObject1.object = testObject2;
testObject1.secondObject = testObject3;
testObject2.object = testObject4;
testObject2.secondObject = testObject5;
testObject3.object = testObject1;
testObject5.object = testObject6;
testObject4.object = testObject1;
testObject5.secondObject = testObject7;
testObject7.object = testObject2;

上述代碼確實是存在循環引用的問題:app

detector-retain-objects

這一次分享的內容就是用於檢測循環引用的框架 FBRetainCycleDetector 咱們會分幾個部分來分析 FBRetainCycleDetector 是如何工做的:框架

  1. 檢測循環引用的基本原理以及過程性能

  2. 檢測涉及 NSObject 對象的循環引用問題spa

  3. 檢測涉及 Associated Object 關聯對象的循環引用問題指針

  4. 檢測涉及 Block 的循環引用問題code

這是四篇文章中的第一篇,咱們會以類 FBRetainCycleDetector- findRetainCycles 方法爲入口,分析其實現原理以及運行過程。

簡單介紹一下 FBRetainCycleDetector 的使用方法:

_RCDTestClass *testObject = [_RCDTestClass new];
testObject.object = testObject;

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:testObject];
NSSet *retainCycles = [detector findRetainCycles];

NSLog(@"%@", retainCycles);
  1. 初始化一個 FBRetainCycleDetector 的實例

  2. 調用 - addCandidate: 方法添加潛在的泄露對象

  3. 執行 - findRetainCycles 返回 retainCycles

在控制檯中的輸出是這樣的:

2016-07-29 15:26:42.043 xctest[30610:1003493] {(
        (
        "-> _object -> _RCDTestClass "
    )
)}

說明 FBRetainCycleDetector 在代碼中發現了循環引用。

findRetainCycles 的實現

在具體開始分析 FBRetainCycleDetector 代碼以前,咱們能夠先觀察一下方法 findRetainCycles 的調用棧:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles
└── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length
    └── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement stackDepth:(NSUInteger)stackDepth
        └── - (instancetype)initWithObject:(FBObjectiveCGraphElement *)object
            └── - (FBNodeEnumerator *)nextObject
                ├── - (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle
                ├── - (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array
                └── - (void)addObject:(ObjectType)anObject;

調用棧中最上面的兩個簡單方法的實現都是比較容易理解的:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles {
    return [self findRetainCyclesWithMaxCycleLength:kFBRetainCycleDetectorDefaultStackDepth];
}

- (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;
}

- findRetainCycles 調用了 - findRetainCyclesWithMaxCycleLength: 傳入了 kFBRetainCycleDetectorDefaultStackDepth 參數來限制查找的深度,若是超過該深度(默認爲 10)就不會繼續處理下去了(查找的深度的增長會對性能有很是嚴重的影響)。

- findRetainCyclesWithMaxCycleLength: 中,咱們會遍歷全部潛在的內存泄露對象 candidate,執行整個框架中最核心的方法 - _findRetainCyclesInObject:stackDepth:,因爲這個方法的實現太長,這裏會分幾塊對其進行介紹,並會省略其中的註釋:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
                                                                 stackDepth:(NSUInteger)stackDepth {
    NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new];
    FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];
    
    NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new];
    
    NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new];

    ...
}

其實整個對象的相互引用狀況能夠看作一個有向圖,對象之間的引用就是圖的 Edge,每個對象就是 Vertex查找循環引用的過程就是在整個有向圖中查找環的過程,因此在這裏咱們使用 DFS 來掃面圖中的環,這些環就是對象之間的循環引用。

文章中並不會介紹 DFS 的原理,若是對 DFS 不瞭解的讀者能夠看一下這個視頻,或者找如下相關資料瞭解一下 DFS 的實現。

接下來就是 DFS 的實現:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
                                                                 stackDepth:(NSUInteger)stackDepth {
    ...
    [stack addObject:wrappedObject];
    
    while ([stack count] > 0) {
        @autoreleasepool {
            FBNodeEnumerator *top = [stack lastObject];
            [objectsOnPath addObject:top];

            FBNodeEnumerator *firstAdjacent = [top nextObject];
            if (firstAdjacent) {

                BOOL shouldPushToStack = NO;
                
                if ([objectsOnPath containsObject:firstAdjacent]) {
                    NSUInteger index = [stack indexOfObject:firstAdjacent];
                    NSInteger length = [stack count] - index;
                    
                    if (index == NSNotFound) {
                        shouldPushToStack = YES;
                    } else {
                        NSRange cycleRange = NSMakeRange(index, length);
                        NSMutableArray<FBNodeEnumerator *> *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy];
                        [cycle replaceObjectAtIndex:0 withObject:firstAdjacent];
                        
                        [retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]];
                    }
                } else {
                    shouldPushToStack = YES;
                }
                
                if (shouldPushToStack) {
                    if ([stack count] < stackDepth) {
                        [stack addObject:firstAdjacent];
                    }
                }
            } else {
                [stack removeLastObject];
                [objectsOnPath removeObject:top];
            }
        }
    }
    return retainCycles;
}

這裏其實就是對 DFS 的具體實現,其中比較重要的有兩點,一是使用 nextObject 獲取下一個須要遍歷的對象,二是對查找到的環進行處理和篩選;在這兩點之中,第一點相對重要,由於 nextObject 的實現是調用 allRetainedObjects 方法獲取被當前對象持有的對象,若是沒有這個方法,咱們就沒法獲取當前對象的鄰接結點,更無從談起遍歷了:

- (FBNodeEnumerator *)nextObject {
    if (!_object) {
        return nil;
    } else if (!_retainedObjectsSnapshot) {
        _retainedObjectsSnapshot = [_object allRetainedObjects];
        _enumerator = [_retainedObjectsSnapshot objectEnumerator];
    }
    
    FBObjectiveCGraphElement *next = [_enumerator nextObject];
    
    if (next) {
        return [[FBNodeEnumerator alloc] initWithObject:next];
    }
    
    return nil;
}

基本上全部圖中的對象 FBObjectiveCGraphElement 以及它的子類 FBObjectiveCBlock FBObjectiveCObjectFBObjectiveCNSCFTimer 都實現了這個方法返回其持有的對象數組。獲取數組以後,就再把其中的對象包裝成新的 FBNodeEnumerator 實例,也就是下一個 Vertex

由於使用 - subarrayWithRange: 方法獲取的數組中的對象都是 FBNodeEnumerator 的實例,還須要必定的處理才能返回:

    • (NSArray<FBObjectiveCGraphElement > )_unwrapCycle:(NSArray<FBNodeEnumerator > )cycle

    • (NSArray<FBObjectiveCGraphElement > )_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement > )array

- _unwrapCycle: 的做用是將數組中的每個 FBNodeEnumerator 實例轉換成 FBObjectiveCGraphElement

- (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle {
    NSMutableArray *unwrappedArray = [NSMutableArray new];
    for (FBNodeEnumerator *wrapped in cycle) {
        [unwrappedArray addObject:wrapped.object];
    }
    
    return unwrappedArray;
}

- _shiftToUnifiedCycle: 方法將每個環中的元素按照地址遞增以及字母順序來排序,方法簽名很好的說明了它們的功能,兩個方法的代碼就不展現了,它們的實現沒有什麼值得注意的地方:

- (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array {
    return [self _shiftToLowestLexicographically:[self _shiftBufferToLowestAddress:array]];
}

方法的做用是防止出現相同環的不一樣表示方式,好比說下面的兩個環實際上是徹底相同的:

-> object1 -> object2
-> object2 -> object1

在獲取圖中的環並排序好以後,就能夠講這些環 union 一下,去除其中重複的元素,最後返回全部查找到的循環引用了。

總結

到目前爲止整個 FBRetainCycleDetector 的原理介紹大概就結束了,其原理徹底是基於 DFS 算法:把整個對象的之間的引用狀況當作圖進行處理,查找其中的環,就找到了循環引用。不過原理真的很簡單,若是這個 lib 的實現僅僅是這樣的話,我也不會寫幾篇文章來專門分析這個框架,真正讓我感興趣的仍是 - allRetainedObjects 方法在各類對象以及 block 中得到它們強引用的對象的過程,這也是以後的文章要分析的主要內容。

關注倉庫,及時得到更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

原文連接: http://draveness.me/retain-cy...

相關文章
相關標籤/搜索