【iOS】OC底层系列六 - block的类型

一、block的类型

block既然有isa指针,那就说明block是一个OC对象,而isa也会指向block所属的对象类型。

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型,同时NSBlock又继承自NSObject(block中的isa本质上就是继承自NSObject的isa)。

  • __NSGlobalBlock__ _NSConcreteGlobalBlock
  • __NSStackBlock__ _NSConcreteStackBlock
  • __NSMallocBlock___NSConcreteMallocBlock

不同类型的block存储的内存区域也不一样。_NSConcreteGlobalBlock存储在数据段(也叫全局区),_NSConcreteMallocBlock存储在堆段中,_NSConcreteStackBlock存放在栈段中。

为了方便研究block的真实类型,我们把ARC关掉。因为ARC会帮助我们处理很多逻辑,导致无法看到block的真实类型。

环境不同会造成block类型有所差异。

3.1. NSGlobalBlock

示例代码:

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
int b = 20;
static int c = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 没有访问任何外部变量
void (^block)(void) = ^{
NSLog(@"this is a block.");
};
NSLog(@"%@", [block class]);
// 输出:__NSGlobalBlock__

// 2. 访问静态局部变量
static int a = 10;
void (^block1)(void) = ^{
NSLog(@"this is a block. a = %d", a);
};
NSLog(@"%@", [block1 class]);
// 输出:__NSGlobalBlock__

// 3. 访问全局变量
void (^block2)(void) = ^{
NSLog(@"this is a block. b = %d", b);
};
NSLog(@"%@", [block2 class]);
// 输出:__NSGlobalBlock__

// 4. 访问静态全局变量
void (^block3)(void) = ^{
NSLog(@"this is a block. c = %d", c);
};
NSLog(@"%@", [block3 class]);
// 输出:__NSGlobalBlock__
}
return 0;
}

上面示例代码中的block有3种情况:没有访问任何外部变量、访问的是静态局部变量、访问的是全局变量。最终显示block的类型是__NSGlobalBlock__

总结:没有访问auto变量的block类型是__NSGlobalBlock__

3.2. NSStackBlock

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 访问局部变量
int a = 10;
void (^block)(void) = ^{
NSLog(@"this is a block. a = %d", a);
};
NSLog(@"%@", [block class]);
// 输出:__NSStackBlock__
}
return 0;
}

注意:上面的代码在MRC环境下显示block的类型是__NSStackBlock__,ARC环境下的block类型是__NSMallocBlock__,因为访问auto变量的block被一个强指针所引用,会自动进行copy操作。

上面示例代码中的block是__NSStackBlock__类型,也就是放在栈段中的。

疑问:栈中的block不会被释放么?看下面的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void (^block)(void);
void test() {
int a = 10;
block = ^{
NSLog(@"a = %d", a);
};
NSLog(@"%@", [block class]);
// 输出:__NSStackBlock__
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
// 输出:a = -272632776
}
return 0;
}

栈中的数据会随着函数调用的结束而释放内存,当执行test函数时,外部的block变量指向test函数中定义的block,而block的执行体本质上是一个被封装的函数地址,而block又被定义在栈中,所以test函数执行结束后,定义的block是会被释放的,外部的block变量指向的就是一块被释放的内存地址。

为了避免栈中的block被释放导致程序错误,我们可以尝试把栈中的block放到堆中,让block成为__NSMallocBlock__类型。

3.3. NSMallocBlock

只要把__NSStackBlock__类型的block进行copy操作,block就会变成__NSMallocBlock__类型(放到堆段)。

修改test函数中的block:

1
2
3
4
5
6
7
8
9
void test() {
int a = 10;
block = [^{
NSLog(@"a = %d", a);
} copy];
NSLog(@"%@", [block class]);
// 输出:__NSMallocBlock__
}
// 调用block输出:a = 10

上面函数中的block进行copy操作后变成了__NSMallocBlock__类型。

注意:如果是MRC情况下进行的copy,需要手动进行释放操作。ARC环境下会自动释放。

二、block的copy

只有__NSStackBlock__类型的block进行copy后会变成__NSMallocBlock__类型,其他类型的block进行copy后基本保持原样不动。

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时(例如,数组排序的方法)
  • block作为GCD API的方法参数时

MRC下block属性的建议写法(只有使用copy修饰符才能让栈上的block被copy到堆上):

1
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法:

1
2
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

虽然在ARC环境下block使用strongcopy进行修饰基本没有区别,但为了统一写法,建议使用copy修饰符。

2.1. 对象类型的auto变量

当block内部访问了对象类型的auto变量时:

  • 如果block是在栈上,将不会对auto变量产生强引用。
  • 如果block被拷贝到堆上,会自动调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
  • 如果block从堆上移除,会自动调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)。

默认引用:

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
typedef void (^DBBlock)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBBlock block;
{
DBPerson *person = [[DBPerson alloc] init];
NSLog(@"1");
block = ^{
NSLog(@"this is a block. person.name = %@", person.name);
};
}
NSLog(@"2");
}
NSLog(@"3");

/*
输出:
1
2
DBPerson dealloc
3
*/
return 0;
}

默认情况下或使用__strong时,block会对person进行强引用,直到block的出了作用域,person才会被释放。主要是因为block被外面的block指针强引用了,导致block暂时不会被释放,直到block所在作用域结束后才会释放block,紧接着person会被释放。

使用__weak

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
typedef void (^DBBlock)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBBlock block;
{
DBPerson *person = [[DBPerson alloc] init];
__weak DBPerson *weakPerson = person;
NSLog(@"1");
block = ^{
NSLog(@"this is a block. person.name = %@", weakPerson.name);
};
}
block();
NSLog(@"2");
}
NSLog(@"3");

/*
输出:
1
DBPerson dealloc
this is a block. person.name = (null)
2
3
*/
return 0;
}

转C++:

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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DBPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DBPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
DBPerson *__weak weakPerson = __cself->weakPerson; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_a9851b_mi_2, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)weakPerson, sel_registerName("name")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;


DBBlock block;
{
DBPerson *person = ((DBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DBPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DBPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_a9851b_mi_0);
__attribute__((objc_ownership(weak))) DBPerson *weakPerson = person;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_a9851b_mi_1);
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, weakPerson, 570425344));
}
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_a9851b_mi_3);
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_a9851b_mi_4);
return 0;
}

新创建的weakPerson指针使用__weak修饰,在block内部引用的person也是使用weak修饰的(弱引用),所以出了person所在作用域后person就会被释放,因为没有任何指针强引用它,此时执行block就会找不到person

注意:如果OC代码含有__weak,在使用clang转换为C++代码时,可能会遇到以下问题:

1
cannot create __weak reference in file using manual reference

解决方案:支持ARC、指定运行时系统版本,比如:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

三、__block修饰符

示例代码:

1
2
3
4
5
int age = 10;
DBBlock block = ^{
NSLog(@"age is %d", age);
};
block();

疑问:可以在block执行体内修改外面变量age的值么?

解答:不可以。首先编译器会报错。其次,在block内部是把age传入当做值传入的,和外面的age不是同一个变量(不是同一个内存地址)。

如果要想在block内部修改age的值,可以把变量修改为静态变量,如static int age = 10,或者把age变为全局变量。因为这样的做法会让block内部的变量和外部变量是同一个内存地址。

但是有时候使用上面的做法会让代码变得难以维护,有其他更好的办法么?有,在变量前面加上修饰符__block

1
2
3
4
5
6
7
__block int age = 10;
DBBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};
block();
// 输出:age is 20

__block可以用于解决block内部无法修改auto变量值的问题。__block不能修饰全局变量、静态变量(static)。编译器会将__block变量包装成一个对象。

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
// age被包装成对象
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; // 指向的是自己
int __flags;
int __size;
int age; // 原来的age值
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕获的是包装后的age
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 修改age的值是先获取__main_block_impl_0中的age对象,然后再获取__forwarding的age值
// 效果等同于 age->age
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_b5d6e0_mi_0, (age->__forwarding->age));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

// 使用__block修饰的age被转成__Block_byref_age_0类型
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

// 后面使用的age都是__Block_byref_age_0类型的age,不是原来定义的age
DBBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pd_jwwkpqcj59x_76sp1yzr_9p80000gp_T_main_b5d6e0_mi_1);
return 0;
}

转成C++后,发现int age被包装成了__Block_byref_age_0 age__Block_byref_age_0是一个对象,内部有isa和age的真正类型。而且结构体__main_block_impl_0内部捕获的变量age是包装后的类型__Block_byref_age_0

结构体__Block_byref_age_0中的__forwarding变量指向的是自己,int age传入的是原来定义的age的值(即代码中的10)。

疑问:使用__block修饰后,后面访问的age是结构体__Block_byref_age_0中的age,还是__main_block_impl_0中的age?

示例代码:

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
__block int age = 10;
DBBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};
NSLog(@"%p", &age);

// 把上面代码转为结构体实现(模拟C++)
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age;
};

int main(int argc, const char * argv[]) {
@autoreleasepool {

__block int age = 10;
DBBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};

struct __main_block_impl_0 *blockImpl = (struct __main_block_impl_0 *)block;

NSLog(@"%p", &age); // 输出:0x7ffeefbff478
NSLog(@"%p", blockImpl->age); // 输出:0x7ffeefbff460
NSLog(@"%p", &(blockImpl->age->age)); // 输出:0x7ffeefbff478
}
return 0;
}

通过对比内存地址可以发现,__block修饰后的age地址和__Block_byref_age_0结构体中的age地址是同一个。

一个__Block_byref_age_0占用28个字节,通过每一个变量的偏移地址也可以计算出age的地址(0x7ffeefbff460 + 24 = 0x7ffeefbff460 + 16 + 8 = 0x7ffeefbff478)。

1
2
3
4
5
6
7
struct __Block_byref_age_0 {
void *__isa; // 指针,占用8个字节
__Block_byref_age_0 *__forwarding; // 指针,占用8个字节
int __flags; // 占用4个字节
int __size; // 占用4个字节
int age; // 占用4个字节
};

请注意下方的代码:

1
2
3
4
NSMutableArray *array = [NSMutableArray array];
DBBlock block = ^{
[array addObject:@"1"];
};

array需要加上__block么?不需要,因为这里只是使用了array指针,并没有修改array(就像只访问age变量,但没有修改age的值)。虽然加上__block也没有问题,但是通过上面的讲解可以看到,能不加__block就不加,因为block内部会重新创建一个对象,并做一些内存管理的操作。