数据结构定义
从苹果的llvm开源代码中,我们可以看到block的数据结构定义如下:示例A。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// imported variables
};
通过上面的代码,我们可以知道,一个block对象实际是由6个部分组成,我们姑且那上面的示例代码成为
- isa指针:OC对象都有该指针,用于实现对象相关的功能。
- flags:用于按bit位表示一些block的附加信息。
- reserved:保留变量。
- invoke:函数指针,指向具体的block实现的函数调用地址,相当于clang编译器下的impl,命名不一样而已!
- descriptor:表示该block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。
- variables:capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
该数据结构和clang分析出来的结构实际上是一样的,仅仅是结构体的嵌套方式不一样。比如,下面两个结构体的嵌套方式不一样,但是在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct SampleA {
int a;
int b;
int c;
};
struct SampleB{
int a;
struct Part1 {
int b;
};
struct Part2{
int c;
};
}
用clang分析block实现
新建工程,新建类test(这里类名和变量名最好不要用带block关键字的,因为后面clang后,变量名会自动填充一些关键字)。test.m中代码如下
第一次clang结果(以下称“第一次clang结果”),不捕获外部变量
1 | - (instancetype)init { |
执行clang,摘出主要代码如下。
1 | struct __block_impl { |
根据注释我们可以看出上面的结构跟示例A 是基本对的上,invoke会特殊些,不过也是有迹可循,即为impl.FuncPtr。
第二次clang(以下称“第二次clang结果”),捕获外部变量
下面我们在block外面声明一个变量a,在block内部访问。OC 代码如下:
1 |
|
执行clang,摘出主要代码如下。
1 | // __block_impl 结构不变这里就不在重复放代码。 |
先来说下多出来的这个方法:_xx_block_copy_0和 _xx_block_dispose_0,为了方便阅读,下面会把__test_init简写为 _xx_
当block中捕获对象类型的变量时,我们发现block结构体_xx_block_impl_0的描述结构体_xx_block_desc_0中多了两个参数copy和dispose函数(非对象类型如:int等类型时这不会出现这两个函数)copy和dispose函数中传入的都是_xx_block_impl_0结构体本身。
_Block_object_assign函数调用时机及作用:
当block进行copy操作的时候就会自动调用_xx_block_desc_0内部的_xx_block_copy_0函数,_xx_block_copy_0函数内部会调用_Block_object_assign函数。
_Block_object_assign函数会自动根据_xx_block_impl_0结构体内部的“a”是什么类型的指针,对“a”对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对“a”进行引用计数器的操作,如果_xx_block_impl_0结构体内“a”指针是 __strong 类型,则为强引用,引用计数+1,如果_xx_block_impl_0结构体内“a”指针是 __weak 类型,则为弱引用,引用计数不变。
_Block_object_dispose函数调用时机及作用:
当block从堆中移除时就会自动调用_xx_block_desc_0中的_xx_block_dispose_0函数,_xx_block_dispose_0函数内部会调用_Block_object_dispose函数。
_Block_object_dispose会对“a”对象做释放操作,类似于release,也就是断开对“a”对象的引用,而“a”究竟是否被释放还是取决于“a”对象自己的引用计数。
第三次clang(加入__block,修饰的变量捕获)
下面我们来修改代码 加上关键字__block,OC:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (instancetype)init {
self = [super init];
if (self) {
__block NSNumber *a = @22;
//__block NSString *a = @"22"; 原来是NSString,但是clang会报错,暂时没找到原因。
void(^test)(void) = ^(){
NSLog(@"a=%ld",a);
};
test();
}
return self;
}
clang之后的代码
1 |
|
加上“__block”关键字之后
- 源码中新增加了一个名为“__Block_byref_a_0”的结构体,用来保存我们要捕获并且修改的变量“a”。
- “__Block_byref_a_0”结构体带有“isa”指针,变量“a”的值及地址。
- 同样增加的copy和dispose函数,用于“__Block_byref_a_0”结构体的内存管理。
循环引用问题
1.为什么会产生循环引用?
答:当block内部捕获的变量类型为对象时。会增加“copy”和“dispose”来管理变量内存,当变量本身被“strong”修饰时,copy内部调用assign会再次进行强引用:引用计数+1,此时如果变量对block有强引用关系(如block为变量的属性等)便会导致循环引用。
2.为何加上“__weak”之后可以避免循环引用?
答:在copy内部调用assign时,如果此时变量的修饰类型为“weak”,则assign函数则不会进行“+1”的行为,所以不会产生循环引用。