Objective-C Blocks

Blocks
阅读完《Objective-C高级编程:iOS与OS X多线程和内存管理》,对Objective-C Blocks进行总结。

什么是 Blocks

Blocks 是带有自动变量(局部变量)的匿名函数

Blocks 模式

Block 语法

^ 返回值类型 (参数列表) {表达式}

若省略返回值类型,返回值类型根据表达式 return 的类型确定:
^ (参数列表) {表达式}

若不使用参数,参数列表也可省略:
^ {表达式}

Block 类型变量

C 语言函数指针:
int (*funcptr)(int)

声明 Block:
int (^blk)(int)

也可使用 typedef:
typedef int (^blk_t)(int)

截获自动变量值

1
2
3
4
5
6
int val = 10;
void (^blk)(void) = ^{
NSLog(@"val = %d",val);
};
val = 2;
blk();

结果为:

1
val = 10

自动变量截获保存后,无法进行修改,以下代码会产生告警:

1
2
3
4
void (^blk)(void) = ^{
val = 1;
NSLog(@"val = %d",val);
};

PS: 不能截获 C 语言的数组变量,若想在 Block 中使用 C 语言的数组,可以截获数组的指针。

Block变量

若要在 block 中修改,或保持最新值,需要添加 __block 说明符:

1
2
3
4
5
6
__block int val = 10;
void (^blk)(void) = ^{
NSLog(@"val = %d",val);
};
val = 2;
blk();

结果为:

1
val = 2

使用截获的值

如下,array 实际上截获的类似 C 语言的指针,无法直接进行赋值,但是可以操作该对象。

1
2
3
4
5
NSMutableArray *array = [NSMutableArray array];
void (^blk)(void) = ^{
[array addObject:@(1)];
};
blk();

Blocks的实现

Blocks的实质

可以使用 Clang 将含有 Block 的源码转换为 C++ 源码
clang -rewrite-objc xxx.m

1
2
3
4
5
6
7
8
void test() {
int val = 10;
void (^blk)(void) = ^{
int tmp = val;
};
val = 2;
blk();
}

转换后:

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

static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};

struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int val; //截获的值
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _val, int flags=0) : val(_val) { //构造函数
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

//__cself为__test_block_impl_0指针,Objective-C的self、C++的this实现方式均类似
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy

int tmp = val;

}

void test() {
int val = 10;
void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, val)); //初始化传入函数指针、要截获的值
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); //函数第一个参数为blk指针
}

isa指针相关知识可参考Objective-C Runtime

__block

传递内存地址指针到 Block

看 __block 之前我们先看一下其他几种可以被 block 内调用的变量:全局变量、静态全局变量、静态局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int global_val = 1;
static int static_global_val = 2;

void test() {
static int static_val = 3;
void (^blk)(void) = ^{
static int static_block_val = 4;
int tmp = global_val;
tmp = static_global_val;
tmp = static_val;
tmp = static_block_val;
};
blk();
}

转换后如下,比较特殊的是局部静态变量,截获的是其指针并存入 __cself->static_val

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int global_val = 1;
static int static_global_val = 2;


struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int *static_val;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy

static int static_block_val = 4;
int tmp = global_val;
tmp = static_global_val;
tmp = (*static_val);
tmp = static_block_val;
}

那为什么自动变量不用指针访问的方法呢?因为自动变量存放在栈中,变量作用域结束的同时,自动变量被废弃,Block 将不能用指针访问到原来的自动变量。

__block 改变存储区域

变量使用 __block:

1
2
3
4
5
6
7
8
void test() {
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
};
val = 2;
blk();
}

转换后:

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
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref

(val->__forwarding->val) = 1;
}
static void __test_block_copy_0(struct __test_block_impl_0*dst, struct __test_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __test_block_dispose_0(struct __test_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __test_block_impl_0*, struct __test_block_impl_0*);
void (*dispose)(struct __test_block_impl_0*);
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0), __test_block_copy_0, __test_block_dispose_0};
void test() {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
(val.__forwarding->val) = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

加了 __block 的变量变成了结构体__Block_byref_val_0__test_block_impl_0结构体实例持有指向 __block 变量的__Block_byref_val_0结构体实例指针,__Block_byref_val_0__forwarding持有指向该实例自身的指针,通过__forwarding访问成员变量 val。

Block存储区域

Block的类:

存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 数据区域(.data)
_NSConcreteMallocBlock

存储分布:

Block 为 _NSConcreteGlobalBlock 类对象的情况:

  • Block 为全局变量
  • Block 内表达式不截获任何变量

_NSConcreteMallocBlock 存在的原因:

  • Block 超出变量作用域可存在的原因
  • __block 变量用结构体成员变量 __forwarding 的原因

Block超出变量作用域时,栈上的__block变量和Block也被废弃:

复制到堆上解决该问题:

复制到堆上的 Block 将 _NSConcreteMallocBlock 类对象写入 isa,__forwarding 可以实现无论 __block 变量配置在栈还是堆上都能拿正确访问:

1
ipml.isa = & _NSConcreteMallocBlock;

大多数情况下编译器会自动生成复制到堆上的代码,其他情况需要手动调用 copy:

  • 向方法或函数参数传递 Block

但如果方法或函数内部进行了适当复制则不需要手动复制:

  • ~~Cocoa 框架方法中含有 usingBlock ~~
  • GCD

经过测试没有上述问题了,参考 StackOverflow:

The need for copying blocks even under ARC arose from a compiler bug in clang (which has been fixed long ago) and was just a work-around since Apple couldn’t wait for this bug to be fixed first before releasing the next Xcode version.

__block变量存储区域

当 Block 从栈复制到堆时,使用的 __block 变量也从栈复制到堆。复制的 Block 持有 __block 变量,若多个 Block 则增加引用计数:

复制后 __forwarding 指针变化如下:

截获对象

1
2
3
4
5
6
7
void test() {
NSMutableArray *array = [NSMutableArray array];
void (^blk)(void) = ^{
[array addObject:@(1)];
};
blk();
}
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
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
NSMutableArray *array; //结构体保存了对象的指针
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
NSMutableArray *array = __cself->array; // bound by copy

((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), (1)));
}
static void __test_block_copy_0(struct __test_block_impl_0*dst, struct __test_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);} //通过copy持有array对象

static void __test_block_dispose_0(struct __test_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);} //通过dispose 释放array对象,通过BLOCK_FIELD_IS_OBJECT 区分是对象还是__block变量

static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __test_block_impl_0*, struct __test_block_impl_0*);
void (*dispose)(struct __test_block_impl_0*);
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0), __test_block_copy_0, __test_block_dispose_0};
void test() {
NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, array, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

可以看到与截获 __block 变量类似

Block循环引用

循环引用最常见的代码:

1
2
3
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
1
2
3
4
5
6
7
8
9
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // capturing a strong reference to self
// creates a strong reference cycle
};
}
...
@end

self 持有 Block,Block 持有 self:

通过 __weak 来避免循环引用:

1
2
3
4
5
6
7
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // capture the weak reference
// to avoid the reference cycle
}
}

通过 __block 来避免循环引用,必须要执行 Block 才能避免循环引用,感觉这种用法意义不大:

1
2
3
4
5
6
7
- (void)configureBlock {
XYZBlockKeeper * __block tmpSelf = self;
self.block = ^{
[tmpSelf doSomething]; // capture the weak reference
tmpSelf = nil; // to avoid the reference cycle
}
}
Cotin Yang wechat
欢迎订阅我的微信公众号 CotinDev
小小地鼓励一下吧~😘