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

LeiaMarlowe 8年前發布 | 17K 次閱讀 IOS iOS開發 移動開發

稍有常識的人都知道在 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;  

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

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

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

  1. 檢測循環引用的基本原理以及過程
  2. 檢測涉及 NSObject 對象的循環引用問題
  3. 檢測涉及 Associated Object 關聯對象的循環引用問題
  4. 檢測涉及 Block 的循環引用問題

這是四篇文章中的第一篇,我們會以類 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 FBObjectiveCObject 和 FBObjectiveCNSCFTimer 都實現了這個方法返回其持有的對象數組。獲取數組之后,就再把其中的對象包裝成新的 FBNodeEnumerator 實例,也就是下一個 Vertex

因為使用 - subarrayWithRange: 方法獲取的數組中的對象都是 FBNodeEnumerator 的實例,還需要一定的處理才能返回:

    • (NSArray *)_unwrapCycle:(NSArray *)cycle
    • (NSArray *)_shiftToUnifiedCycle:(NSArray *)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 中獲得它們強引用的對象的過程,這也是之后的文章要分析的主要內容。

 

來自:http://draveness.me/retain-cycle1/

 

 本文由用戶 LeiaMarlowe 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!