關(guān)注倉庫父款,及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
在上一篇文章中介紹了 FBRetainCycleDetector
的基本工作原理,這一篇文章中我們開始分析它是如何從每一個(gè)對(duì)象中獲得它持有的強(qiáng)指針的瞻凤。
如果沒有看第一篇文章這里還是最好看一下铛漓,了解一下
FBRetainCycleDetector
的工作原理,如何在 iOS 中解決循環(huán)引用的問題鲫构。
FBRetainCycleDetector
獲取對(duì)象的強(qiáng)指針是通過 FBObjectiveCObject
類的 - allRetainedObjects
方法浓恶,這一方法是通過其父類 FBObjectiveCGraphElement
繼承過來的,只是內(nèi)部有著不同的實(shí)現(xiàn)结笨。
allRetainedObjects 方法
我們會(huì)以 XXObject
為例演示 - allRetainedObjects
方法的調(diào)用過程:
#import <Foundation/Foundation.h>
@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
的代碼如下:
XXObject *object = [[XXObject alloc] init];
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:object];
__unused NSSet *cycles = [detector findRetainCycles];
在 FBObjectiveCObject
中包晰,- allRetainedObjects
方法只是調(diào)用了 - _unfilteredRetainedObjects
,然后進(jìn)行了過濾炕吸,文章主要會(huì)對(duì) - _unfilteredRetainedObjects
的實(shí)現(xiàn)進(jìn)行分析:
- (NSSet *)allRetainedObjects {
NSArray *unfiltered = [self _unfilteredRetainedObjects];
return [self filterObjects:unfiltered];
}
方法 - _unfilteredRetainedObjects
的實(shí)現(xiàn)代碼還是比較多的伐憾,這里會(huì)將代碼分成幾個(gè)部分,首先是最重要的部分:如何得到對(duì)象持有的強(qiáng)引用:
- (NSArray *)_unfilteredRetainedObjects
NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache);
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];
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self,
referencedObject,
self.configuration,
namePath);
if (element) {
[retainedObjects addObject:element];
}
}
}
...
}
獲取強(qiáng)引用是通過 FBGetObjectStrongReferences
這一函數(shù):
NSArray<id<FBObjectReference>> *FBGetObjectStrongReferences(id obj,
NSMutableDictionary<Class, NSArray<id<FBObjectReference>> *> *layoutCache) {
NSMutableArray<id<FBObjectReference>> *array = [NSMutableArray new];
__unsafe_unretained Class previousClass = nil;
__unsafe_unretained Class currentClass = object_getClass(obj);
while (previousClass != currentClass) {
NSArray<id<FBObjectReference>> *ivars;
if (layoutCache && currentClass) {
ivars = layoutCache[currentClass];
}
if (!ivars) {
ivars = FBGetStrongReferencesForClass(currentClass);
if (layoutCache && currentClass) {
layoutCache[(id<NSCopying>)currentClass] = ivars;
}
}
[array addObjectsFromArray:ivars];
previousClass = currentClass;
currentClass = class_getSuperclass(currentClass);
}
return [array copy];
}
上面代碼的核心部分是執(zhí)行 FBGetStrongReferencesForClass
返回 currentClass
中的強(qiáng)引用赫模,只是在這里我們遞歸地查找了所有父類的指針树肃,并且加入了緩存以加速查找強(qiáng)引用的過程,接下來就是從對(duì)象的結(jié)構(gòu)中獲取強(qiáng)引用的過程了:
static NSArray<id<FBObjectReference>> *FBGetStrongReferencesForClass(Class aCls) {
NSArray<id<FBObjectReference>> *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<id<FBObjectReference>> *filteredIvars =
[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject,
NSDictionary *bindings) {
return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]];
}]];
return filteredIvars;
}
該方法的實(shí)現(xiàn)大約有三個(gè)部分:
- 調(diào)用
FBGetClassReferences
從類中獲取它指向的所有引用瀑罗,無論是強(qiáng)引用或者是弱引用 - 調(diào)用
FBGetLayoutAsIndexesForDescription
從類的變量布局中獲取強(qiáng)引用的位置信息 - 使用
NSPredicate
過濾數(shù)組中的弱引用
獲取類的 Ivar 數(shù)組
FBGetClassReferences
方法主要調(diào)用 runtime 中的 class_copyIvarList
得到類的所有 ivar
:
這里省略對(duì)結(jié)構(gòu)體屬性的處理胸嘴,因?yàn)樘^復(fù)雜,并且涉及大量的C++ 代碼斩祭,有興趣的讀者可以查看
FBGetReferencesForObjectsInStructEncoding
方法的實(shí)現(xiàn)劣像。
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 *wrapper = [[FBIvarReference alloc] initWithIvar:ivar];
[result addObject:wrapper];
}
free(ivars);
return [result copy];
}
上述實(shí)現(xiàn)還是非常直接的,遍歷 ivars
數(shù)組摧玫,使用 FBIvarReference
將其包裝起來然后加入 result
中耳奕,其中的類 FBIvarReference
僅僅起到了一個(gè)包裝的作用,將 Ivar 中保存的各種屬性全部保存起來:
typedef NS_ENUM(NSUInteger, FBType) {
FBObjectType,
FBBlockType,
FBStructType,
FBUnknownType,
};
@interface FBIvarReference : NSObject <FBObjectReference>
@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
的實(shí)例初始化時(shí)芍躏,會(huì)通過私有方法 - _convertEncodingToType:
將類型編碼轉(zhuǎn)換為枚舉類型:
- (FBType)_convertEncodingToType:(const char *)typeEncoding {
if (typeEncoding[0] == '{') return FBStructType;
if (typeEncoding[0] == '@') {
if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType;
return FBObjectType;
}
return FBUnknownType;
}
當(dāng)代碼即將從 FBGetClassReferences
方法中返回時(shí)邪乍,使用 lldb 打印 result
中的所有元素:
上述方法成功地從 XXObject
類中獲得了正確的屬性數(shù)組,不過這些數(shù)組中不止包含了強(qiáng)引用纸肉,還有被 weak
標(biāo)記的弱引用:
<__NSArrayM 0x7fdac0f31860>(
[_first, index: 1],
[_second, index: 2],
[_third, index: 3],
[_forth, index: 4],
[_fifth, index: 5],
[_sixth, index: 6]
)
獲取 Ivar Layout
當(dāng)我們?nèi)〕隽?XXObject
中所有的屬性之后溺欧,還需要對(duì)其中的屬性進(jìn)行過濾喊熟;那么我們?nèi)绾闻袛嘁粋€(gè)屬性是強(qiáng)引用還是弱引用呢柏肪?Objective-C 中引入了 Ivar Layout 的概念,對(duì)類中的各種屬性的強(qiáng)弱進(jìn)行描述芥牌。
它是如何工作的呢烦味,我們先繼續(xù)執(zhí)行 FBGetStrongReferencesForClass
方法:
在 ObjC 運(yùn)行時(shí)中的 class_getIvarLayout
可以獲取某一個(gè)類的 Ivar Layout,而 XXObject
的 Ivar Layout 是什么樣的呢壁拉?
(lldb) po fullLayout
"\x01\x12\x11"
Ivar Layout 就是一系列的字符谬俄,每?jī)蓚€(gè)一組,比如 \xmn
弃理,每一組 Ivar Layout 中第一位表示有 m
個(gè)非強(qiáng)屬性溃论,第二位表示接下來有 n
個(gè)強(qiáng)屬性;如果沒有明白痘昌,我們以 XXObject
為例演示一下:
@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 個(gè)非強(qiáng)屬性钥勋,然后有 1 個(gè)強(qiáng)屬性first
- 第二組的
\x12
表示有 1 個(gè)非強(qiáng)屬性second
,然后有 2 個(gè)強(qiáng)屬性third
forth
- 第三組的
\x11
表示有 1 個(gè)非強(qiáng)屬性fifth
, 然后有 1 個(gè)強(qiáng)屬性sixth
在對(duì) Ivar Layout 有一定了解之后辆苔,我們可以繼續(xù)對(duì) FBGetStrongReferencesForClass
分析了算灸,下面要做的就是使用 Ivar Layout 提供的信息過濾其中的所有非強(qiáng)引用,而這就需要兩個(gè)方法的幫助驻啤,首先需要 FBGetMinimumIvarIndex
方法獲取變量索引的最小值:
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;
}
然后執(zhí)行 FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout)
獲取所有強(qiáng)引用的 NSRange
:
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;
}
因?yàn)楦呶槐硎痉菑?qiáng)引用的數(shù)量菲驴,所以我們要加上 upperNibble
,然后 NSMakeRange(currentIndex, lowerNibble)
就是強(qiáng)引用的范圍骑冗;略過 lowerNibble
長(zhǎng)度的索引赊瞬,移動(dòng) layoutDescription
指針,直到所有的 NSRange
都加入到了 interestingIndexes
這一集合中贼涩,就可以返回了森逮。
過濾數(shù)組中的弱引用
在上一階段由于已經(jīng)獲取了強(qiáng)引用的范圍,在這里我們直接使用 NSPredicate
謂詞來進(jìn)行過濾就可以了:
NSArray<id<FBObjectReference>> *filteredIvars =
[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject,
NSDictionary *bindings) {
return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]];
}]];
====
接下來磁携,我們回到文章開始的 - _unfilteredRetainedObjects
方法:
- (NSSet *)allRetainedObjects {
NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache);
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];
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self,
referencedObject,
self.configuration,
namePath);
if (element) {
[retainedObjects addObject:element];
}
}
}
...
}
FBGetObjectStrongReferences
只是返回 id<FBObjectReference>
對(duì)象褒侧,還需要 FBWrapObjectGraphElementWithContext
把它進(jìn)行包裝成 FBObjectiveCGraphElement
:
FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(id object,
FBObjectGraphConfiguration *configuration,
NSArray<NSString *> *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];
}
}
}
最后會(huì)把封裝好的實(shí)例添加到 retainedObjects
數(shù)組中。
- _unfilteredRetainedObjects
同時(shí)也要處理集合類,比如數(shù)組或者字典闷供,但是如果是無縫橋接的 CF 集合烟央,或者是元類,雖然它們可能遵循 NSFastEnumeration
協(xié)議歪脏,但是在這里并不會(huì)對(duì)它們進(jìn)行處理:
- (NSArray *)_unfilteredRetainedObjects {
...
if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) {
return retainedObjects;
}
if (class_isMetaClass(aCls)) {
return nil;
}
...
}
在遍歷內(nèi)容時(shí)疑俭,Mutable 的集合類中的元素可能會(huì)改變,所以會(huì)重試多次以確保集合類中的所有元素都被獲取到了:
- (NSArray *)_unfilteredRetainedObjects {
...
if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) {
NSInteger tries = 10;
for (NSInteger i = 0; i < tries; ++i) {
NSMutableSet *temporaryRetainedObjects = [NSMutableSet new];
@try {
for (id subobject in self.object) {
[temporaryRetainedObjects addObject:FBWrapObjectGraphElement(subobject, self.configuration)];
[temporaryRetainedObjects addObject:FBWrapObjectGraphElement([self.object objectForKey:subobject], self.configuration)];
}
}
@catch (NSException *exception) {
continue;
}
[retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]];
break;
}
}
return retainedObjects;
}
這里將遍歷集合中的元素的代碼放入了 @try
中婿失,如果在遍歷時(shí)插入了其它元素钞艇,就會(huì)拋出異常,然后 continue
重新遍歷集合豪硅,最后返回所有持有的對(duì)象哩照。
最后的過濾部分會(huì)使用 FBObjectGraphConfiguration
中的 filterBlocks
將不需要加入集合中的元素過濾掉:
- (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;
}
總結(jié)
FBRetainCycleDetector
在對(duì)象中查找強(qiáng)引用取決于類的 Ivar Layout,它為我們提供了與屬性引用強(qiáng)弱有關(guān)的信息懒浮,幫助篩選強(qiáng)引用飘弧。
關(guān)注倉庫,及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github