一. 原理分析
FBRetainCycleDetector
的原理:是基于DFS
算法,把整个对象之间的强引用关系当做图进行处理,查找其中的环,就找到了循环引用。
二. 检测NSObject对象持有的强指针
1. 如何确定对象类型
@encode(type-name)
返回类型的字符串编码,在确定循环引用关系的过程中,只有三种编码字符串存在强引用关系:
判断代码:
- (FBType)_convertEncodingToType:(const char *)typeEncoding {
if (typeEncoding[0] == '{') return FBStructType;
if (typeEncoding[0] == '@') {
if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType;
return FBObjectType;
}
return FBUnknownType;
}
2. 获取Ivar的引用属性
class_getIvarLayout
可以返回一个字符串用来描述成员变量的布局信息。假设存在类:
@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\x12\x11
,以此为例,字符串表示共存在0+1+1+2+1+1
总共6
个成员变量。其中一个\xAB
表示存在A
个非强引用属性,和B
个强引用属性,因此该布局字符串也可表示为:
- 0个非强引用属性,1个强引用属性
- 1个非强引用属性,2个强引用属性
- 1个非强引用属性,1个强引用属性
3. 获取变量布局偏移
ivar_getOffset
可以获取成员变量在类结构中的偏移地址,由于ivar
是指针类型,通过offset/sizeof(void *)
可以获取偏移数量。通过range
的方式可以用来匹配某个成员变量是否属于强引用属性:
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
这一集合中,就可以返回了。
二. 关联属性强引用的获取
FBRetainCycleDetector
在对关联对象进行追踪时,通过fishhook
第三方库hook
了关联对象的两个C
函数,objc_setAssociatedObject
和 objc_removeAssociatedObjects
,然后重新实现了关联对象模块,将通过OBJC_ASSOCIATION_RETAIN
和OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略,保存起来,只追踪强引用的属性。
三. Block引用属性获取
首先需要声明一个类似block
的结构体类型,用于强制类型转换:
struct BlockLiteral {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct BlockDescriptor *descriptor;
// imported variables
};
被block
引用的对象总是基于block
地址偏移整个结构体的size
,并且被持有的对象按照强引用在前,弱引用在后
的顺序排列
这里
block
变量其实只是一个指向结构体的指针,所以大小为8
,结构体的大小为32
struct BlockDescriptor {
unsigned long int reserved; // NULL
unsigned long int size;
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
const char *signature; // IFF (1<<30)
};
block
中存在两个helper
函数协助完成copy
到堆上和释放对象引用的工作,后者会对所有的strong
类型的对象进行一次release
调用,因此可以通过手动调用dispose_helper
的方式来识别block
强引用的对象:
最后就是用于从dispose_helper
的接收类FBBlockStrongRelationDetector
,它的实例在接收release
消息时,并不会真正的释放,只会将标记_strong
为YES
.
- (oneway void)release {
_strong = YES;
}
- (oneway void)trueRelease {
[super release];
}
真正调用trueRelease
的时候才会向对象发送release
消息。
如果block
持有另一个block
对象,FBBlockStrongRelationDetector
也会将自身伪装成一个假的block
防止在接收到关于block
释放的消息时发生crash
.
struct _block_byref_block;
@interface FBBlockStrongRelationDetector : NSObject {
// __block fakery
void *forwarding;
int flags; //refcount;
int size;
void (*byref_keep)(struct _block_byref_block *dst, struct _block_byref_block *src);
void (*byref_dispose)(struct _block_byref_block *);
void *captured[16];
}
1. 获取block强引用对象
static NSIndexSet *_GetBlockStrongLayout(void *block) {
struct BlockLiteral *blockLiteral = block;
/// 如果拥有CPP析构器,说明持有的对象可能没有按照指针大小对齐,很难检测到所有对象
/// 如果没有dispose函数,说明无法retain对象,因此也无法测试强引用了哪些对象
if ((blockLiteral->flags & BLOCK_HAS_CTOR)
|| !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
return nil;
}
/// 通过获取dispose_helper,并且将一个mock的对象数组传入进去,检测哪些mock对象会被release
void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
const size_t ptrSize = sizeof(void *);
const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;
void *obj[elements];
void *detectors[elements];
/// 遍历所有mock对象,如果标记位为YES,说明被release,此时对应位置的变量被block强引用
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}
@autoreleasepool {
dispose_helper(obj);
}
NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
if (detector.isStrong) {
[layout addIndex:i];
}
[detector trueRelease];
}
return layout;
}
四. DFS深度遍历检测是否存在循环引用
采用stack
的方式,对某个对象获取所有被其强引用的对象,然后依次以递归思想将这些对象入栈,如果某次入栈的对象已经存在stack
中,说明该对象在stack
中的位置直到当前为止,存在引用环.
- (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;
}
五. 总结
FBRetainCycleDetector
进行循环引用检测的思路如下:
-
对于正常类的类的成员变量,通过
runtime
的class_getIvarLayout
获取描述该类成员变量的布局信息,然后通过ivar_getOffset
遍历获取成员变量在类结构中的偏移地址,然后获取强引用变量的集合。 -
对于关联对象的成员变量,通过
fishhook
第三方库hook
了关联对象的两个C
函数,objc_setAssociatedObject
和objc_removeAssociatedObjects
,然后重新实现了关联对象模块,将通过OBJC_ASSOCIATION_RETAIN
和OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略进行关联的对象保存起来,只追踪强引用的属性。 -
对于
block
持有的强引用变量的获取,依据block
引用的对象总是基于block
地址偏移整个结构体的size
,并且被持有的对象按照强引用在前,弱引用在后
的顺序排列,因为block
强引用的对象都会进行copy
到堆上和release
对象引用的操作,因此可以通过接收类FBBlockStrongRelationDetector
构造detector
对象,然后用block
的dispose_helper
方法调用,判断如果detector
对象调用release
方法,就说明当前对象是强引用对象,然后获取block
持有的所有强引用变量的集合 -
对所有检测到的强引用变量,利用
DFS(深度优先搜索)
,采用stack
栈的形式,将遍历到的对象依次入栈,如果某次入栈的对象已经存在stack
中,说明该对象在stack
中的位置直到当前为止,存在引用环.
这里只是做了过程的总结,详细分析可以参考以下文章:
如何在 iOS 中解决循环引用的问题
检测 NSObject 对象持有的强指针
如何实现 iOS 中的 Associated Object
iOS 中的 block 是如何持有对象的
网友评论