0%

从 Block 谈堆栈

在面试的时候,我们会碰到 Block 相关的问题:MRC 下为什么要用 copy 修饰? 为什么 ARC 下就不要这样处理呢?,所以打算把堆栈的内容和 Block 一起讲解。

堆栈存储域

  • 堆的空间需要程序员手动申请分配,它的内存是动态分配的,我们需要手动管理这部分内存,用完了要销毁掉,这部分内存属于dirty memory。简单点,就是 malloc 与 free。
  • 栈的内存是系统编译管理,跟程序员无关,它一般用来存储局部变量等。

OC 中我们创建一个对象,它最终会调用 calloc 方法在堆上创建内存。那有没有对象是创建在栈上的呢?先来看看栈对象的优缺点。

栈对象

优点

  • 快速 在栈上创建对象很快。因为很多东西在编译时就确定了,在运行时,需要做的事情很少,相对而言,在堆上创建对象就比较耗时。
  • 简单 它的声明周期是确定的,对象出栈以后就会被释放,不会发生内存泄露。

缺点

  • 生命周期固定 一旦函数返回,它的栈帧就会被摧毁,那么对象也会被释放。如果这个对象通过方法调用需要传递到别的方法里面去,那被释放掉了,会产生野指针,此时 retain 也不起作用,它不适合引用计数内存管理方法。
  • 大小固定 栈空间有限,创建时的长度是固定好的,没法扩展。

OC 中的栈对象

这就是我们今天的主角了: Block,它就是典型的栈对象。先从几段官方文档中获取相关信息

  • “Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary.” 得知 Blocks 是对象。
  • “As an optimization, block storage starts out on the stack—just like blocks themselves do. “ 和 “The initial allocation is done on the stack, but the runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1.” 得知 Block 对象与一般类的实例对象不同,它默认是分配在栈上的,一般类的实例对象则在堆上分配。

而在 ARC 下,Block 会被拷贝到堆中。所以下面的代码只有在 ARC 下才能正确执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef NSInteger (^blk)(NSInteger);

blk func(NSInteger pram) {
return ^(NSInteger count){return pram * count;};
}

int main(int argc, char * argv[]) {
blk blk1 = func(2);
NSInteger result = blk1(4);
printf("result:%ld", result);
return 0;
}

Block

那什么是 Block 呢?它是带有自动变量(局部变量)的匿名函数。它的语法格式为^ 返回值类型 参数列表 表达式。它的申明可以查看 How Do I Declare A Block in Objective-C?。我们一步步来看看它的实现,先看一个简单的 Block(这里会查看对应编译代码和汇编代码)。

简单 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
int main(int argc, char * argv[]) {
dispatch_block_t blk = ^{printf("Block\n");};
blk();
return 0;
}


// clang -rewrite-objc 后所得
struct __block_impl {
void *isa; // isa指针,关于类与对象的,下次聊 runtime 讲解类和对象的时候再详解
int Flags; // 标志位
int Reserved; // 为以后升级保留的
void *FuncPtr; // 函数指针
};

struct __main_block_impl_0 {
struct __block_impl impl; // Block 实现
struct __main_block_desc_0* Desc; // Block 描述
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { // 构造函数
impl.isa = &_NSConcreteStackBlock; // 栈 Block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // Block 实现函数
printf("Block\n");}

static struct __main_block_desc_0 { // Block 描述结构体
size_t reserved; // 为以后升级保留的
size_t Block_size; // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) { // main 函数
dispatch_block_t blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

代码分析

变换后的源码,Blocks 变成了 C 函数__main_block_func_0,它有一个结构体指针的入参struct __main_block_impl_0 *,相关注释已经写在编译后的代码里面了。相关调用方法为 blk() -> ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk),去掉相关转换部分为 (*blk->FuncPtr)(blk),这就是一个函数指针调用函数啊,而在初始化 &__main_block_impl_0 时赋值给 FuncPtr 就是 __main_block_func_0,所以这里就是调用 __main_block_func_0Block最终还是调用函数,打印 “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
43
44
45
int main(int argc, char * argv[]) {
NSInteger val = 10;
dispatch_block_t blk = ^{printf("Block:%ld\n", (long)val);};
val = 20;
blk();
return 0;
}


// clang -rewrite-objc 后所得
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;
NSInteger val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSInteger val = __cself->val; // bound by copy
printf("Block:%ld\n", (long)val);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) {
NSInteger val = 10;
dispatch_block_t blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 20;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

代码分析

跟上面比,代码有点不同,__main_block_impl_0 多了一个成员变量 val(用来存储 Block 里面所使用的变量)。在 __main_block_impl_0 初始化的时候,将外部的变量值 val 赋值给 __main_block_impl_0val(捕获自动变量的原理),然后调用 Block 的时候获取 __main_block_impl_0val 再打印。

__block 修饰符

如果在Block里面对所捕获的变量进行赋值时,编译时会报错,提示我们加上__block修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
NSInteger val = 10;
dispatch_block_t blk = ^{
val = 30;
printf("Block:%ld\n", (long)val);
};
val = 20;
blk();
return 0;
}

// 报错内容:Variable is not assignable (missing __block type specifier)

记住,只是赋值才会有问题,调用变更对象的方法是没问题。例如,下面的代码就没问题。

1
2
3
4
5
6
7
8
9
10
int main(int argc, char * argv[]) {
NSMutableArray *array = [NSMutableArray array];
dispatch_block_t blk = ^{
NSObject *objc = [[NSObject alloc] init];
[array addObject:objc];
};
blk();
NSLog(@"array:%@", array);
return 0;
}

__block 说明符类似于 staticautoregister 说明符,它们用于指定将变量的值设置在哪个存储域。例如,auto 表示自动变量存储在栈中,static 表示作为静态变量存储在数据区中。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
int main(int argc, char * argv[]) {
__block NSInteger val = 10;
dispatch_block_t blk = ^{
val = 30;
printf("Block inner val:%ld\n", (long)val);
};
val = 20;
blk();
printf("out val:%ld\n", (long)val);
return 0;
}

// 输出结果
Block inner val:30
out val:30

// clang -rewrite-objc 后所得
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __Block_byref_val_0 { // 结构体
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_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 __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref

(val->__forwarding->val) = 30;
printf("Block inner val:%ld\n", (long)(val->__forwarding->val));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);} // 拷贝函数,拷贝到堆上

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);} // 销毁函数,销毁堆上的数据

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, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
dispatch_block_t blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
(val.__forwarding->val) = 20;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
printf("out val:%ld\n", (long)(val.__forwarding->val));
return 0;
}

源码分析

原本的 __block 变量竟然变成了一个结构体 __Block_byref_val_0 实例。这里的赋值操作比较怪异,明明直接 val->val = 30 就行了,为什么还要 val->__forwarding->val) = 30。这个 __forwarding 成员变量是用来指向堆上的结构体实例的,因为这里发生了拷贝行为,将 __block 变量从栈上拷贝到了堆上。Block 有三种类型,

  • _NSConcreteStackBlock 该类的 Block 变量设置在栈上
  • _NSConcreteGlobalBlock 该类的 Block 变量设置在数据区域(.data区)上
  • _NSConcreteMallocBlock 该类的 Block 变量设置在有 malloc 函数分配的内存块(即堆)中

_NSConcreteStackBlock 类型如以下代码所示。

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
dispatch_block_t blk = ^{
printf("Global Block");
};

int main(int argc, char * argv[]) {
blk();
return 0;
}

// clang -rewrite-objc 后所得
struct __blk_block_impl_0 {
struct __block_impl impl;
struct __blk_block_desc_0* Desc;
__blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {

printf("Global Block");
}

static struct __blk_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};
static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
dispatch_block_t blk = ((void (*)())&__global_blk_block_impl_0);

int main(int argc, char * argv[]) {
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

我们现在只有 _NSConcreteMallocBlock 没有看到了,先看一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef NSInteger (^blk)(NSInteger);

blk func(NSInteger pram) {
return ^(NSInteger count){return pram * count;};
}

int main(int argc, char * argv[]) {
blk blk1 = func(2);
NSInteger result = blk1(4);
printf("result:%ld", result);
return 0;
}

// Xcode 运行输出
result:8

但是在终端用 clang 编译的时候会报错,

1
2
/var/folders/hn/g9n1gw8d6p3655k44zsrnh_h0000gn/T/main-a7f5c2.mi:46504:12: error: returning block that lives on the local stack
return ^(NSInteger count){return pram * count;};

报错信息表示 func 函数返回的是在栈上的 Block 对象,函数返回后,它的作用域结束,所以栈上的 Block 也被废弃,这样提示没毛病啊,但是 Xcode 运行确正常啊,说明系统(ARC 情况下)帮我们拷贝到了堆上,因为你把main.m文件编译的时候加上-fno-objc-arc,它就会报错上面一样的错误。那来看看它的汇编代码 Xcode -> Product -> Perform Action -> Assemble"main.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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
	.section	__TEXT,__text,regular,pure_instructions
.ios_version_min 8, 1
.syntax unified
.file 1 "/Users/Admin/Documents/Demo/DatePicker" "/Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m"
.globl _func
.align 1
.code 16 @ @func
.thumb_func _func
_func:
Lfunc_begin0:
.loc 1 17 0 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:17:0
.cfi_startproc
@ BB#0:
push {r7, lr}
mov r7, sp
sub sp, #28
@DEBUG_VALUE: func:pram <- [%SP+24]
str r0, [sp, #24]
.loc 1 18 12 prologue_end @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:12
Ltmp0:
movw r0, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_0+4))
movt r0, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC0_0+4))
LPC0_0:
add r0, pc
ldr r0, [r0]
str r0, [sp]
mov.w r0, #-1073741824
str r0, [sp, #4]
movs r0, #0
str r0, [sp, #8]
movw r0, :lower16:(___func_block_invoke-(LPC0_1+4))
movt r0, :upper16:(___func_block_invoke-(LPC0_1+4))
LPC0_1:
add r0, pc
str r0, [sp, #12]
movw r0, :lower16:(___block_descriptor_tmp-(LPC0_2+4))
movt r0, :upper16:(___block_descriptor_tmp-(LPC0_2+4))
LPC0_2:
add r0, pc
str r0, [sp, #16]
.loc 1 0 0 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:0:0
ldr r0, [sp, #24]
.loc 1 18 12 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:12
str r0, [sp, #20]
mov r0, sp
bl _objc_retainBlock
Ltmp1:
.loc 1 18 5 is_stmt 0 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:5
add sp, #28
pop.w {r7, lr}
b.w _objc_autoreleaseReturnValue
Ltmp2:
Lfunc_end0:
.cfi_endproc

.align 1
.code 16 @ @__func_block_invoke
.thumb_func ___func_block_invoke
___func_block_invoke:
Lfunc_begin1:
.loc 1 18 0 is_stmt 1 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:0
.cfi_startproc
@ BB#0:
sub sp, #12
str r0, [sp, #8]
@DEBUG_VALUE: __func_block_invoke: <- %R0
.loc 1 18 30 prologue_end @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:30
Ltmp3:
mov r2, r0
str r1, [sp, #4]
str r2, [sp]
.loc 1 18 38 is_stmt 0 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:38
Ltmp4:
ldr r0, [r0, #20]
Ltmp5:
.loc 1 18 45 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:45
ldr r1, [sp, #4]
.loc 1 18 43 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:43
muls r0, r1, r0
.loc 1 18 31 @ /Users/Admin/Documents/Demo/DatePicker/DatePicker/main.m:18:31
add sp, #12
bx lr
Ltmp6:
Lfunc_end1:
.cfi_endproc

我们看到了 _objc_retainBlock_objc_autoreleaseReturnValue,在 runtime 源码中,

1
2
3
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}

ARC 帮我们进行了拷贝处理,将 Block 从栈上拷贝到了推上,返回了 autorelease 对象,所以 Xcode 运行才会没毛病。在Objective-C Automatic Reference Counting (ARC) - Blocks 里面也有提及。

1
With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

现在,文章开头的问题解决了吧,但是,为什么我们现在声明 Block 属性的时候还是用 copy 修饰符呢?,算是为了解决历史遗留问题吧,给开发者提个醒吧。在Programming with Objective-C : Working with Blocks 苹果官方文档中,

1
You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior。

参考链接