關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
在上一篇文章中介紹了 FBRetainCycleDetector
的基本工作原理,這一篇文章中我們開始分析它是如何從每一個物件中獲得它持有的強指標的。
如果沒有看第一篇文章這裡還是最好看一下,瞭解一下
FBRetainCycleDetector
的工作原理,如何在 iOS 中解決迴圈引用的問題。
FBRetainCycleDetector
獲取物件的強指標是通過 FBObjectiveCObject
類的 - allRetainedObjects
方法,這一方法是通過其父類 FBObjectiveCGraphElement
繼承過來的,只是內部有著不同的實現。
allRetainedObjects 方法
我們會以 XXObject
為例演示 - allRetainedObjects
方法的呼叫過程:
1 2 3 4 5 6 7 8 9 10 11 12 |
#import @interface XXObject : NSObject @property (nonatomic, strong) id first; @property (nonatomic, weak) id second; @property (nonatomic, strong) id third; @property (nonatomic, strong) id forth; @property (nonatomic, weak) id fifth; @property (nonatomic, strong) id sixth; @end |
使用 FBRetainCycleDetector
的程式碼如下:
1 2 3 4 5 |
XXObject *object = [[XXObject alloc] init]; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:object]; __unused NSSet *cycles = [detector findRetainCycles]; |
在 FBObjectiveCObject
中,- allRetainedObjects
方法只是呼叫了 - _unfilteredRetainedObjects
,然後進行了過濾,文章主要會對 - _unfilteredRetainedObjects
的實現進行分析:
1 2 3 4 |
- (NSSet *)allRetainedObjects { NSArray *unfiltered = [self _unfilteredRetainedObjects]; return [self filterObjects:unfiltered]; } |
方法 - _unfilteredRetainedObjects
的實現程式碼還是比較多的,這裡會將程式碼分成幾個部分,首先是最重要的部分:如何得到物件持有的強引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (NSArray *)_unfilteredRetainedObjects NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache); NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy]; for (id ref in strongIvars) { id referencedObject = [ref objectReferenceFromObject:self.object]; if (referencedObject) { NSArray *namePath = [ref namePath]; FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, referencedObject, self.configuration, namePath); if (element) { [retainedObjects addObject:element]; } } } ... } |
獲取強引用是通過 FBGetObjectStrongReferences
這一函式:
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 |
NSArray> *FBGetObjectStrongReferences(id obj, NSMutableDictionary> *> *layoutCache) { NSMutableArray> *array = [NSMutableArray new]; __unsafe_unretained Class previousClass = nil; __unsafe_unretained Class currentClass = object_getClass(obj); while (previousClass != currentClass) { NSArray> *ivars; if (layoutCache && currentClass) { ivars = layoutCache[currentClass]; } if (!ivars) { ivars = FBGetStrongReferencesForClass(currentClass); if (layoutCache && currentClass) { layoutCache[(id)currentClass] = ivars; } } [array addObjectsFromArray:ivars]; previousClass = currentClass; currentClass = class_getSuperclass(currentClass); } return [array copy]; } |
上面程式碼的核心部分是執行 FBGetStrongReferencesForClass
返回 currentClass
中的強引用,只是在這裡我們遞迴地查詢了所有父類的指標,並且加入了快取以加速查詢強引用的過程,接下來就是從物件的結構中獲取強引用的過程了:
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 |
static NSArray> *FBGetStrongReferencesForClass(Class aCls) { NSArray> *ivars = [FBGetClassReferences(aCls) filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { if ([evaluatedObject isKindOfClass:[FBIvarReference class]]) { FBIvarReference *wrapper = evaluatedObject; return wrapper.type != FBUnknownType; } return YES; }]]; const uint8_t *fullLayout = class_getIvarLayout(aCls); if (!fullLayout) { return nil; } NSUInteger minimumIndex = FBGetMinimumIvarIndex(aCls); NSIndexSet *parsedLayout = FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout); NSArray> *filteredIvars = [ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; }]]; return filteredIvars; } |
該方法的實現大約有三個部分:
- 呼叫
FBGetClassReferences
從類中獲取它指向的所有引用,無論是強引用或者是弱引用 - 呼叫
FBGetLayoutAsIndexesForDescription
從類的變數佈局中獲取強引用的位置資訊 - 使用
NSPredicate
過濾陣列中的弱引用
獲取類的 Ivar 陣列
FBGetClassReferences
方法主要呼叫 runtime 中的 class_copyIvarList
得到類的所有 ivar
:
這裡省略對結構體屬性的處理,因為太過複雜,並且涉及大量的C++ 程式碼,有興趣的讀者可以檢視
FBGetReferencesForObjectsInStructEncoding
方法的實現。
1 2 3 4 5 6 7 |
NSArray> *FBGetClassReferences(Class aCls) { NSMutableArray> *result = [NSMutableArray new]; unsigned int count; Ivar *ivars = class_copyIvarList(aCls, &count); for (unsigned int i = 0; i |
上述實現還是非常直接的,遍歷 ivars
陣列,使用 FBIvarReference
將其包裝起來然後加入 result
中,其中的類 FBIvarReference
僅僅起到了一個包裝的作用,將 Ivar 中儲存的各種屬性全部儲存起來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
typedef NS_ENUM(NSUInteger, FBType) { FBObjectType, FBBlockType, FBStructType, FBUnknownType, }; @interface FBIvarReference : NSObject @property (nonatomic, copy, readonly, nullable) NSString *name; @property (nonatomic, readonly) FBType type; @property (nonatomic, readonly) ptrdiff_t offset; @property (nonatomic, readonly) NSUInteger index; @property (nonatomic, readonly, nonnull) Ivar ivar; - (nonnull instancetype)initWithIvar:(nonnull Ivar)ivar; @end |
包括屬性的名稱、型別、偏移量以及索引,型別是通過型別編碼來獲取的,在 FBIvarReference
的例項初始化時,會通過私有方法 - _convertEncodingToType:
將型別編碼轉換為列舉型別:
1 2 3 4 5 6 7 8 9 10 |
- (FBType)_convertEncodingToType:(const char *)typeEncoding { if (typeEncoding[0] == '{') return FBStructType; if (typeEncoding[0] == '@') { if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType; return FBObjectType; } return FBUnknownType; } |
當程式碼即將從 FBGetClassReferences
方法中返回時,使用 lldb 列印 result
中的所有元素:
上述方法成功地從 XXObject
類中獲得了正確的屬性陣列,不過這些陣列中不止包含了強引用,還有被 weak
標記的弱引用:
1 2 3 4 5 6 7 8 |
( [_first, index: 1], [_second, index: 2], [_third, index: 3], [_forth, index: 4], [_fifth, index: 5], [_sixth, index: 6] ) |
獲取 Ivar Layout
當我們取出了 XXObject
中所有的屬性之後,還需要對其中的屬性進行過濾;那麼我們如何判斷一個屬性是強引用還是弱引用呢?Objective-C 中引入了 Ivar Layout 的概念,對類中的各種屬性的強弱進行描述。
它是如何工作的呢,我們先繼續執行 FBGetStrongReferencesForClass
方法:
在 ObjC 執行時中的 class_getIvarLayout
可以獲取某一個類的 Ivar Layout,而 XXObject
的 Ivar Layout 是什麼樣的呢?
1 2 |
(lldb) po fullLayout "\x01\x12\x11" |
Ivar Layout 就是一系列的字元,每兩個一組,比如 \xmn
,每一組 Ivar Layout 中第一位表示有 m
個非強屬性,第二位表示接下來有 n
個強屬性;如果沒有明白,我們以 XXObject
為例演示一下:
1 2 3 4 5 6 7 8 9 10 |
@interface XXObject : NSObject @property (nonatomic, strong) id first; @property (nonatomic, weak) id second; @property (nonatomic, strong) id third; @property (nonatomic, strong) id forth; @property (nonatomic, weak) id fifth; @property (nonatomic, strong) id sixth; @end |
- 第一組的
\x01
表示有 0 個非強屬性,然後有 1 個強屬性first
- 第二組的
\x12
表示有 1 個非強屬性second
,然後有 2 個強屬性third
forth
- 第三組的
\x11
表示有 1 個非強屬性fifth
, 然後有 1 個強屬性sixth
在對 Ivar Layout 有一定了解之後,我們可以繼續對 FBGetStrongReferencesForClass
分析了,下面要做的就是使用 Ivar Layout 提供的資訊過濾其中的所有非強引用,而這就需要兩個方法的幫助,首先需要 FBGetMinimumIvarIndex
方法獲取變數索引的最小值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static NSUInteger FBGetMinimumIvarIndex(__unsafe_unretained Class aCls) { NSUInteger minimumIndex = 1; unsigned int count; Ivar *ivars = class_copyIvarList(aCls, &count); if (count > 0) { Ivar ivar = ivars[0]; ptrdiff_t offset = ivar_getOffset(ivar); minimumIndex = offset / (sizeof(void *)); } free(ivars); return minimumIndex; } |
然後執行 FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout)
獲取所有強引用的 NSRange
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) { NSMutableIndexSet *interestingIndexes = [NSMutableIndexSet new]; NSUInteger currentIndex = minimumIndex; while (*layoutDescription != '\x00') { int upperNibble = (*layoutDescription & 0xf0) >> 4; int lowerNibble = *layoutDescription & 0xf; currentIndex += upperNibble; [interestingIndexes addIndexesInRange:NSMakeRange(currentIndex, lowerNibble)]; currentIndex += lowerNibble; ++layoutDescription; } return interestingIndexes; } |
因為高位表示非強引用的數量,所以我們要加上 upperNibble
,然後 NSMakeRange(currentIndex, lowerNibble)
就是強引用的範圍;略過 lowerNibble
長度的索引,移動 layoutDescription
指標,直到所有的 NSRange
都加入到了 interestingIndexes
這一集合中,就可以返回了。
過濾陣列中的弱引用
在上一階段由於已經獲取了強引用的範圍,在這裡我們直接使用 NSPredicate
謂詞來進行過濾就可以了:
1 2 3 4 5 |
NSArray> *filteredIvars = [ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; }]]; |
====
接下來,我們回到文章開始的 - _unfilteredRetainedObjects
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (NSSet *)allRetainedObjects { NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache); NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy]; for (id ref in strongIvars) { id referencedObject = [ref objectReferenceFromObject:self.object]; if (referencedObject) { NSArray *namePath = [ref namePath]; FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, referencedObject, self.configuration, namePath); if (element) { [retainedObjects addObject:element]; } } } ... } |
FBGetObjectStrongReferences
只是返回 id
物件,還需要 FBWrapObjectGraphElementWithContext
把它進行包裝成 FBObjectiveCGraphElement
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(id object, FBObjectGraphConfiguration *configuration, NSArray *namePath) { if (FBObjectIsBlock((__bridge void *)object)) { return [[FBObjectiveCBlock alloc] initWithObject:object configuration:configuration namePath:namePath]; } else { if ([object_getClass(object) isSubclassOfClass:[NSTimer class]] && configuration.shouldInspectTimers) { return [[FBObjectiveCNSCFTimer alloc] initWithObject:object configuration:configuration namePath:namePath]; } else { return [[FBObjectiveCObject alloc] initWithObject:object configuration:configuration namePath:namePath]; } } } |
最後會把封裝好的例項新增到 retainedObjects
陣列中。
- _unfilteredRetainedObjects
同時也要處理集合類,比如陣列或者字典,但是如果是無縫橋接的 CF 集合,或者是元類,雖然它們可能遵循 NSFastEnumeration
協議,但是在這裡並不會對它們進行處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (NSArray *)_unfilteredRetainedObjects { ... if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) { return retainedObjects; } if (class_isMetaClass(aCls)) { return nil; } ... } |
在遍歷內容時,Mutable 的集合類中的元素可能會改變,所以會重試多次以確保集合類中的所有元素都被獲取到了:
1 2 3 4 5 6 7 |
- (NSArray *)_unfilteredRetainedObjects { ... if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) { NSInteger tries = 10; for (NSInteger i = 0; i |
這裡將遍歷集合中的元素的程式碼放入了 @try
中,如果在遍歷時插入了其它元素,就會丟擲異常,然後 continue
重新遍歷集合,最後返回所有持有的物件。
最後的過濾部分會使用 FBObjectGraphConfiguration
中的 filterBlocks
將不需要加入集合中的元素過濾掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (NSSet *)filterObjects:(NSArray *)objects { NSMutableSet *filtered = [NSMutableSet new]; for (FBObjectiveCGraphElement *reference in objects) { if (![self _shouldBreakGraphEdgeFromObject:self toObject:reference]) { [filtered addObject:reference]; } } return filtered; } - (BOOL)_shouldBreakGraphEdgeFromObject:(FBObjectiveCGraphElement *)fromObject toObject:(FBObjectiveCGraphElement *)toObject { for (FBGraphEdgeFilterBlock filterBlock in _configuration.filterBlocks) { if (filterBlock(fromObject, toObject) == FBGraphEdgeInvalid) return YES; } return NO; } |
總結
FBRetainCycleDetector
在物件中查詢強引用取決於類的 Ivar Layout,它為我們提供了與屬性引用強弱有關的資訊,幫助篩選強引用。
關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github