block

block的实现原理,对象模型

数据结构定义

从苹果的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
15
struct 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
2
3
4
5
6
7
8
9
10
11
12
- (instancetype)init {

self = [super init];
if (self) {
void(^test)(void) = ^(){

NSLog(@"11");
};
test();
}
return self;
}

执行clang,摘出主要代码如下。

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
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};

//这种就是之前说的 为什么尽量不要用block的命名,在执行clang之后,struct名是由类名+方法名的方式拼接成。
struct __test__init_block_impl_0 {
struct __block_impl impl;
struct __test__init_block_desc_0* Desc;
__test__init_block_impl_0(void *fp, struct __test__init_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //函数指针,可以从下面调用block传入的参数看出,这里指向了 __test__init_block_func_0
Desc = desc;
}
};

static void __test__init_block_func_0(struct __test__init_block_impl_0 *__cself) {
//真正的NSLog在这里
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ng_8ncwcfkj69n6sh2hxlsfzz300000gn_T_test_0cd5eb_mi_0);
}

//示例A 中的descriptor。
static struct __test__init_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test__init_block_desc_0_DATA = { 0, sizeof(struct __test__init_block_impl_0)};

//对应OC的重写初始化方法
static instancetype _I_test_init(test * self, SEL _cmd) {

self = ((test *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("test"))}, sel_registerName("init"));
if (self) {
//__test__init_block_impl_0的第一个参数*fp,其值为__test__init_block_func_0。
void(*test)(void) = ((void (*)())&__test__init_block_impl_0((void *)__test__init_block_func_0, &__test__init_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}
return self;
}

根据注释我们可以看出上面的结构跟示例A 是基本对的上,invoke会特殊些,不过也是有迹可循,即为impl.FuncPtr。

第二次clang(以下称“第二次clang结果”),捕获外部变量

下面我们在block外面声明一个变量a,在block内部访问。OC 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

- (instancetype)init {

self = [super init];
if (self) {
NSString *a = @"222";
void(^test)(void) = ^(){

NSLog(@"a=%@",a);
};
test();
}
return self;
}

执行clang,摘出主要代码如下。

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
52
// __block_impl 结构不变这里就不在重复放代码。
struct __test__init_block_impl_0 {
struct __block_impl impl;
struct __test__init_block_desc_0* Desc;
//我们可以发现在该结构体内d,多了一个变量“a”;
NSString *a;
//这里的构造方法也多了一个参数“a”;
__test__init_block_impl_0(void *fp, struct __test__init_block_desc_0 *desc, NSString *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test__init_block_func_0(struct __test__init_block_impl_0 *__cself) {
//这里的__cself 实际就是上面的结构体,__cself—>a,即结构体内a的值。
NSString *a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ng_8ncwcfkj69n6sh2hxlsfzz300000gn_T_test_8d5c07_mi_1,a);
}

//不同点,和上面“第一次clang结果”相比,多了两个方法:block_copy_0和block_dispose_0
//下面方法参数为__test__init_block_impl_0结构体,
//内部调用的是_Block_object_assign方法,且操作了参数结构体内的“a”变量,如何操作下面会仔细说明。
static void __test__init_block_copy_0(struct __test__init_block_impl_0*dst, struct __test__init_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

//不同点。
//下面方法参数同为__test__init_block_impl_0。
//方法内部调用的是 _Block_object_dispose,同样操作了参数结构体内的“a”变量,如何操作下面会仔细说明
static void __test__init_block_dispose_0(struct __test__init_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __test__init_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __test__init_block_impl_0*, struct __test__init_block_impl_0*);
void (*dispose)(struct __test__init_block_impl_0*);
} __test__init_block_desc_0_DATA = { 0, sizeof(struct __test__init_block_impl_0), __test__init_block_copy_0, __test__init_block_dispose_0};

static instancetype _I_test_init(test * self, SEL _cmd) {

self = ((test *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("test"))}, sel_registerName("init"));
if (self) {
NSString *a = (NSString *)&__NSConstantStringImpl__var_folders_ng_8ncwcfkj69n6sh2hxlsfzz300000gn_T_test_8d5c07_mi_0;
//这里把“a”作为block构造方法的其中一个参数传入。(block如果捕获外部变量);
void(*test)(void) = ((void (*)())&__test__init_block_impl_0((void *)__test__init_block_func_0, &__test__init_block_desc_0_DATA, a, 570425344));
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}
return self;
}

先来说下多出来的这个方法:_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
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
52
53
54
55
56
57
58

//对比之前多出来的结构体,对应之前的“a”变量
struct __Block_byref_a_0 {
void *__isa; //熟悉的isa指针
__Block_byref_a_0 *__forwarding; //指向”a“地址的指针
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSNumber *a; //"a"的值
};

struct __test__init_block_impl_0 {
struct __block_impl impl;
struct __test__init_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref 对应“第二次clang”中的变量”a“
__test__init_block_impl_0(void *fp, struct __test__init_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test__init_block_func_0(struct __test__init_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 333);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ng_8ncwcfkj69n6sh2hxlsfzz300000gn_T_test_b1d4ff_mi_0,(a->__forwarding->a));
}

//对应“第二次clang”中的copy方法
static void __test__init_block_copy_0(struct __test__init_block_impl_0*dst, struct __test__init_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

//对应“第二次clang”中的dispose方法
static void __test__init_block_dispose_0(struct __test__init_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __test__init_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __test__init_block_impl_0*, struct __test__init_block_impl_0*);
void (*dispose)(struct __test__init_block_impl_0*);
} __test__init_block_desc_0_DATA = { 0, sizeof(struct __test__init_block_impl_0), __test__init_block_copy_0, __test__init_block_dispose_0};

static instancetype _I_test_init(test * self, SEL _cmd) {

self = ((test *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("test"))}, sel_registerName("init"));
if (self) {

//构造Block_byref_a_0结构体,第二参数传入的”a“的地址(对应”__forwarding“参数),最后一个参数传入”a“的值
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 33554432, sizeof(__Block_byref_a_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 22)};

void(*test)(void) = ((void (*)())&__test__init_block_impl_0((void *)__test__init_block_func_0, &__test__init_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}
return self;
}

加上“__block”关键字之后

  1. 源码中新增加了一个名为“__Block_byref_a_0”的结构体,用来保存我们要捕获并且修改的变量“a”。
  2. “__Block_byref_a_0”结构体带有“isa”指针,变量“a”的值及地址。
  3. 同样增加的copy和dispose函数,用于“__Block_byref_a_0”结构体的内存管理。

循环引用问题

1.为什么会产生循环引用?
答:当block内部捕获的变量类型为对象时。会增加“copy”和“dispose”来管理变量内存,当变量本身被“strong”修饰时,copy内部调用assign会再次进行强引用:引用计数+1,此时如果变量对block有强引用关系(如block为变量的属性等)便会导致循环引用。
2.为何加上“__weak”之后可以避免循环引用?
答:在copy内部调用assign时,如果此时变量的修饰类型为“weak”,则assign函数则不会进行“+1”的行为,所以不会产生循环引用。