0%

获取OC对象的所有强引用对象(三)

上一篇文章 介绍了如何获取 NSTimer(FBObjectiveCNSCFTimer)的所有强引用对象。本文继续介绍如何获取一种特殊OC对象 – Block 的所有强引用对象。

FBRetainCycleDetector 的源码中,Block 对应的类为 FBObjectiveCBlock

FBRetainCycleDetector 源码地址: https://github.com/facebook/FBRetainCycleDetector

Block 基础

OC 的 Block 其实也是一个结构体,结构如下:

1
2
3
4
5
6
7
8
struct BlockLiteral {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct BlockDescriptor *descriptor;
// imported variables
};

其中,isa 这个字段大家应该都很熟悉,和 objc_class 结构体里的 isa 一样,都是指向具体的类。Block 结构体对应的类可能是以下三种中的任意一种,大家可以自行了解,本文不再继续展开介绍。

  • __NSGlobalBlock
  • __NSMallocBlock
  • __NSStackBlock

FBRetainCycleDetector 源码中 Block 对象对应的类为 FBObjectiveCBlock

1
2
3
4
5
/**
Object Graph element representing block.
*/
@interface FBObjectiveCBlock : FBObjectiveCGraphElement
@end

如何判断一个对象是 Block

如果是普通的OC对象,我们可以直接用 [objc class] 获取其类型。 Block 对象能不能也通过这种方式来判断呢?当然是可以的,Block 对象是一种特殊的 OC 对象 ,再怎么特殊,它也是一个OC对象,普通OC对象能做的事情,它也能做。

问题的关键是,OC Runtime 并没有直接暴露出所谓的 Block 类 。上面提到过,一个 Block 可能是 __NSGlobalBlock__NSMallocBlock__NSStackBlock 中的任意一种,但是这三个类都没有在OCRuntime 中直接暴露。

Block 的运行时类型

所以,我们首先要找到一个 Block 对象在运行时到底是什么类。

FBRetainCycleDetector 通过创建一个 testBlock 并不断向上查找其 superclass ,来获取 Block 对象在运行时的 “基类”。

1
2
3
4
5
6
7
8
9
10
11
12
13
static Class _BlockClass() {
static dispatch_once_t onceToken;
static Class blockClass;
dispatch_once(&onceToken, ^{
void (^testBlock)() = [^{} copy];
blockClass = [testBlock class];
while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
blockClass = class_getSuperclass(blockClass);
}
[testBlock release];
});
return blockClass;
}

注意: _BlockClass 方法在查找 superclass 时,如果找到了 NSObject,就直接终止查找过程。否则找到的基类就都成了 NSObect

实际运行起来,大家会发现这个方法的返回值为 NSBlock。其声明如下:

1
2
@interface NSBlock : NSObject <NSCopying>
@end

这再次印证了我们上面说过的,“Block 对象是一种特殊的OC对象”。Block 对象都继承自 NSBlockNSBlock 又是 NSObject 的子类。

Block 类型判断

1
2
3
4
5
6
BOOL FBObjectIsBlock(void *object) {
Class blockClass = _BlockClass();

Class candidate = object_getClass((__bridge id)object);
return [candidate isSubclassOfClass:blockClass];
}

这段代码就很简单了,直接判断当前对象的 Class 是不是 NSBlock 类的 subclass 即可。

获取 Block 的强引用对象

再回头看 Block 结构体的最后,有一行注释:

// imported variables

1
2
3
4
5
6
7
8
struct BlockLiteral {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct BlockDescriptor *descriptor;
// imported variables
};

Block 引用的对象会存放在 Block 结构体的最下方,这里似乎是一个突破口,直接遍历 imported variables 是不是就可以拿到所有的强引用对象了呢?

普通OC对象的所有强引用对象 一文中,我们提到引用对象分为 强引用对象弱引用对象 ,对于 Block 来说也是一样。

对于普通OC对象,我们可以通过 class_getIvarLayout 来区分强弱引用对象,但是 Block 对象并没有直接提供 class_getIvarLayout 方法,所以我们需要换一种方式获取 ivarLayout

dispose_helper

继续看 Block 结构体,其有一个 BlockDescriptor 结构体,结构如下:

1
2
3
4
5
6
7
8
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)
};

其中,size 字段表示 Block 持有的所有对象的大小,通过这个字段我们能拿到 Block 持有的对象总数(包括强引用和弱引用)。

在这个结构体中还有两个函数指针, copy_helperdispose_helpercopy_helper 用于 Block 的拷贝,而 dispose_helper 用于 Block 的析构。
Block 在析构时会调用 dispose_helper ,对所有强引用对象发送 release 消息,以销毁强引用对象。弱引用对象此时自然不会收到 release 消息。

FBRetainCycleDetector 正是利用这个原理,来获取 Block 的所有强引用对象。

发送/接收 release 消息

Block 在析构时会调用 dispose_helper ,对所有强引用对象发送 release 消息。那么我们只需要手动触发一次 dispose_helper 方法,看看哪些对象收到了 release 消息,这些对象就是被强引用的对象了。

问题来了,怎么知道哪个对象执行了 release 方法呢?难道要把所有对象的 release 方法都 hook 掉?这样肯定是不现实的。而且,我们只是想知道哪些对象被强引用了,并不想把这些对象真正销毁掉。如果真销毁掉了,整个 Block 肯定乱成一锅粥了😂。

因此,我们要创建一个 虚假的对象 ,用来接收 release 消息。在 FBRetainCycleDetector 的源码中,FBBlockStrongRelationDetector 就是这个 虚假的对象

1
2
3
4
5
6
7
8
9
@implementation FBBlockStrongRelationDetector
- (oneway void)release {
_strong = YES;
}
- (oneway void)trueRelease {
[super release];
}
// ...
@end

FBBlockStrongRelationDetector 对象在接收到 release 消息时,只是将 _strong 标记为 YES,真正的销毁是在 trueRelease 方法中完成的。

收集强引用对象

通过上面的分析,我们已经能够:

  • 通过解析 Block 结构体数据和 BlockDescriptor 结构体的 size 字段,获取到所有引用对象列表。
  • 通过手动执行 BlockDescriptor 结构体的 dispose_helper ,获取所有强引用对象在对象列表中的位置。

那最后一步就很明显,也很简单了:

  • 收集所有强引用对象。

其实,查找 Block 强引用对象和查找普通OC对象的强引用对象,在思想上还是非常相似的。都需要先获得所有对象列表,再筛选出强引用对象。只不过普通OC对象筛选强引用对象非常方便,可以直接获取到 ivarLayout ,Block 对象要麻烦很多。

源码解读

FBObjectiveCBlock 类中提供了如何获取 Block 对象的所有强引用对象的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (NSSet *)allRetainedObjects {
// 1. 添加作为普通OC对象的所有强引用对象,见 *FBObjectiveCObject.m*
NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy];

// Grab a strong reference to the object, otherwise it can crash while doing
// nasty stuff on deallocation
__attribute__((objc_precise_lifetime)) id anObject = self.object;

// 2. 获取所有 Block 强引用的对象
void *blockObjectReference = (__bridge void *)anObject;
NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference);

for (id object in allRetainedReferences) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration);
if (element) {
[results addObject:element];
}
}
return [NSSet setWithArray:results];
}

其中, FBGetBlockStrongReferences 方法的源码在 FBBlockStrongLayout.m 中。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 有删减
NSArray *FBGetBlockStrongReferences(void *block) {
// ...
NSMutableArray *results = [NSMutableArray new];
// 确定引用对象中哪些为强引用
void **blockReference = block;
NSIndexSet *strongLayout = _GetBlockStrongLayout(block);
[strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
void **reference = &blockReference[idx];
if (reference && (*reference)) {
id object = (id)(*reference);
if (object) {
[results addObject:object];
}
}
}];
return [results autorelease];
}

static NSIndexSet *_GetBlockStrongLayout(void *block) {
struct BlockLiteral *blockLiteral = block;
// ...
// 获取 dispose_helper 函数指针
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;

// 创建“虚假的对象”,用于接收 release 消息
void *obj[elements];
void *detectors[elements];
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}
// 执行 dispose_helper 方法,触发强引用对象所在的索引位置的 FBBlockStrongRelationDetector 执行 release
@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];
}
// Destroy detectors
[detector trueRelease];
}
return layout;
}

总结

FBRetainCycleDetector 将OC对象划分为3类,分别为:

  • FBObjectiveCObject
  • FBObjectiveCNSCFTimer
  • FBObjectiveCBlock

本系列的三篇文章分别介绍了对这3类对象如何获取其所有强引用对象。汇总起来,就是 FBRetainCycleDetector 查找OC对象的所有强引用对象的基本原理。知道每个OC对象的所有强引用对象后,就能够生成每一个对象的引用关系图,继续按图索骥,就能够检测到是否存在循环引用链。

FBRetainCycleDetector 是一个非常优秀的开源项目,尤其适合用来学习 OC Runtime 知识,了解 OC 对象在运行时的结构,顺便掌握一些 “黑科技” ,强烈建议大家阅读其源码。

前两篇文章链接:

参考