理解Block(下)

前言

本文将会从内存结构上分析 OC 中的 Block

环境

macOS Sierra 10.12.3
Xcode 8.2.1

关于 Block 的实现

Block的实现 中的 Block 的结构声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
};

关于 ABI

在计算机软件中,应用程序二进制接口(ABI)是两个程序模块之间的接口,其中一个程序模块通常是机器代码级别的库或操作系统。 ABI确定如何调用函数以及在系统调用的情况下将二进制格式信息从一个程序组件传递到下一个程序组件或操作系统的详细信息。

  1. Block : 结构中有一个 isa 指针,初始化时指向 _NSConcreteStackBlock_NSConcreteGlobalBlock 的地址, 用于实现对象相关的功能。
  2. flags : 用于按 bit 位表示一些 block 的附加信息
  3. reserved : 保留位
  4. void (*invoke)(void* ,...) : 接收可变参数的函数指针,指向 block 实现的函数调用 (invoke) 地址。
  5. descriptor : Block_descriptor_1 结构体的指针,表示该 block 的附加描述信息,主要是 size 大小,以及 copydispose 函数的指针。
  6. imported variables : capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

实践

实践一 : 新建 Xcode 命令行工程

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^blockDemo)() = ^() {
NSLog(@"Hello world");
};

blockDemo();
}
return 0;
}

设置断点,编译并运行,按指令级别单步调试,可得到

从上面的结果可知, Block 结构确实如上面所描述的, isa 指针指向的是 __NSGlobalBlock__

将工程的二进制文件放到 Hopper ,查看 __main_block_invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                     ___main_block_invoke:
0000000100000f10 push rbp ; Objective C Block defined at 0x100001060, XREF=0x100001070
0000000100000f11 mov rbp, rsp
0000000100000f14 sub rsp, 0x10
0000000100000f18 lea rax, qword [ds:cfstring_Hello_world] ; @"Hello world"
0000000100000f1f mov qword [ss:rbp+var_8], rdi
0000000100000f23 mov qword [ss:rbp+var_10], rdi
0000000100000f27 mov rdi, rax ; argument "format" for method imp___stubs__NSLog
0000000100000f2a mov al, 0x0
0000000100000f2c call imp___stubs__NSLog
0000000100000f31 add rsp, 0x10
0000000100000f35 pop rbp
0000000100000f36 ret
; endp
0000000100000f37 db 0x90 ; '.'

反汇编得到

1
2
3
4
void ___main_block_invoke(void * _block) {
NSLog(@"Hello world");
return;
}

修改 Xcode 工程,用 Block 去捕获参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int a = 1;
NSString *testString = @"2";
void (^blockDemo)() = ^() {
NSLog(@"Hello world,%d,%@",a,testString);
};

blockDemo();
}
return 0;
}

同样操作得到如下截图

如果取消 ARC 环境, isa 指针会指向 __NSStackBlock__,在此不再截图说明。

小结一

  1. 验证了 Blockinvoke 函数指针指向 block 实现的函数地址。
  2. 验证了 imported variables 确实保存了从外部 captureBlock 中的变量

问题一

Blockisa 指针指向的对象和 Block的实现 描述的有出入

实践二 : clang 重写工程的 main.m 文件

找到 main.m 文件,在终端中用 clang -rewrite-objc path/to/main.m 生成对应的 C++ 代码。

参考本文后的引用链接 3 摘录出与 Block 相关的实现部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
#endif
#endif
#define __block
#define __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
29
30

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_8067ac_mi_0);
}

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, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

void (*blockDemo)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

((void (*)(__block_impl *))((__block_impl *)blockDemo)->FuncPtr)((__block_impl *)blockDemo);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

main 函数的执行流程

  1. 函数 __main_block_impl_0__main_block_func_0__main_block_desc_0_DATA 作参数传入来完成结构体 __main_block_impl_0 的初始化
  2. 函数 __main_block_func_0 在这里即为 NSLog(@"Hello world"),将其地址传递给结构体 __main_block_impl_0 中的 __block_impl 结构体 impl 的函数指针 FuncPtr
  3. 参数 __main_block_desc_0 传递到结构体 __main_block_impl_0 中的 __main_block_desc_0 结构体的 Desc
  4. blockDemo 函数指针引用了前面结构体初始化中获得的 __main_block_impl_0 结构体
  5. blockDemo 函数指针引用的结构体强转为 __block_impl * 类型,因为结构体 __main_block_impl_0 的内存结构中最前面就是 __block_impl 结构体的 impl,因此在这里强转类型借用一下面向对象的概念就是子类强转成父类指针,是没有问题的。当然面向过程早于面向对象,面向对象为什么能实现父类指针强转成子类,反倒是应该要依赖两者兼容的内存结构。
  6. 执行第 5 步生成的 __block_impl 的结构体的 FuncPtr 函数,前面部分的 ((void (*)(__block_impl *)) 表明这个函数是无返回值,需要一个 __block_impl 的参数,后面部分的 ((__block_impl *)blockDemo) 说明要执行 FuncPtr 函数,并把 blockDemo 强转为 (__block_impl * ) 作为参数传递到该函数中

小结二

从上面的实践中,我们可以得知平时创建的 Block 包含了 struct __block_impl 和类似 struct __main_block_desc_0 这与 Block实现 中描述的 Block 的结构是等价的。

关于问题一

  • 在实践一中用 po 输出 Blockisa 对象为类似 __NSStackBlock__ 这种的类型的
  • 在实践二中用 clang 重写 OC 代码,Blockisa 指针指向 _NSConcreteStackBlock / _NSConcreteStackBlock

哪个才是正确的呢?

思考之后,我觉得是这样的:

从内存角度上看,isa 中保存的就是一个内存地址。我们用 po 来输出时,就是告诉 lldb 把我传入的这个内存地址认为是一个对象的起始地址,以一个对象的角度去看它,因为 AppleBlock 进行了相应的封装,自然输出对应的 Block 对象类型。然后在 clang 翻译成 C++ 后,是以 _NSConcreteStackBlock 这种变量地址的角度来看待它。简而言之,两者都没有问题,只是角度不同而已。

实践三 : 捕获外部变量的 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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
NSString *testString;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSString *_testString, int flags=0) : a(_a), testString(_testString) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSString *testString = __cself->testString; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_6832f1_mi_1,a,testString);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->testString, (void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->testString, 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;

int a = 1;
NSString *testString = (NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_6832f1_mi_0;
void (*blockDemo)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, testString, 570425344));

((void (*)(__block_impl *))((__block_impl *)blockDemo)->FuncPtr)((__block_impl *)blockDemo);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

小结三

main 函数的执行流程

这次与实践二主要差别在

  • 结构体 __main_block_impl_0 的多了两个 atestString 属性接收外界传入的参数。
  • 结构体 __main_block_desc_0 多了 copydispose 两个函数
  • 多了 __main_block_copy_0__main_block_dispose_0 两个静态方法

查看 Block-private.h

1
2
3
4
5
6
7
8
9
10
11
// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};

因为 a 不是对象,所以是以传值的方式进行传递过程。而 testString 是对象,会在函数 __main_block_copy_0 中以参数 3 去调用函数 _Block_object_assign,在函数 __main_block_dispose_0 中类似。

实践四 : __Block 修饰捕获的变量

插曲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int a = 1;
NSString *testString = @"2";
__block int b = 2;
__block NSString *testString2 = @"3";
void (^blockDemo)() = ^() {
NSLog(@"Hello world,%d,%d,%@,%@",a,b,testString,testString2);
};
blockDemo();
}

return 0;
}

上面这段代码用 clang -rewrite-objc 会报 clang: error: unable to execute command: Segmentation fault: 11 ,段错误,一般是访问了非法地址造成的。很多编程语言为了避免字符串被重复创建,一般都会有个字符串常量缓存池。而 OC 中也不例外,用字面量创建的字符串是放在数据段中的。

the string will be stored in a area of memory called data segment. This area never changes after the application is launched

这个区域的数据在应用启动后不会被改变,而用 __block 来修饰,很明显是想在 block 中进行修改,所以才会报段错误。

回归

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int a = 1;
NSString *testString = @"2";
__block int b = 2;
__block NSString *testString2 = [[NSString alloc] initWithString:@"3"];
void (^blockDemo)() = ^() {
NSLog(@"Hello world,%d,%d,%@,%@",a,b,testString,testString2);
};
blockDemo();
}

return 0;
}

rewrite 后的结果

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
struct __Block_byref_b_0 {
void *__isa;
__Block_byref_b_0 *__forwarding;
int __flags;
int __size;
int b;
};
struct __Block_byref_testString2_1 {
void *__isa;
__Block_byref_testString2_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSString *testString2;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
NSString *testString;
__Block_byref_b_0 *b; // by ref
__Block_byref_testString2_1 *testString2; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSString *_testString, __Block_byref_b_0 *_b, __Block_byref_testString2_1 *_testString2, int flags=0) : a(_a), testString(_testString), b(_b->__forwarding), testString2(_testString2->__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_b_0 *b = __cself->b; // bound by ref
__Block_byref_testString2_1 *testString2 = __cself->testString2; // bound by ref
int a = __cself->a; // bound by copy
NSString *testString = __cself->testString; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_894ba5_mi_2,a,(b->__forwarding->b),testString,(testString2->__forwarding->testString2));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->testString, (void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->testString2, (void*)src->testString2, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->testString2, 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, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

int a = 1;
NSString *testString = (NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_894ba5_mi_0;
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};
__attribute__((__blocks__(byref))) __Block_byref_testString2_1 testString2 = {(void*)0,(__Block_byref_testString2_1 *)&testString2, 33554432, sizeof(__Block_byref_testString2_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_lk_znn1qp4925j412wt4t_qkplc0000gn_T_main_894ba5_mi_1)};
void (*blockDemo)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, testString, (__Block_byref_b_0 *)&b, (__Block_byref_testString2_1 *)&testString2, 570425344));
((void (*)(__block_impl *))((__block_impl *)blockDemo)->FuncPtr)((__block_impl *)blockDemo);
}

return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

小结四

__block 修饰的外部变量被 Block 捕获时,会创建一个以 __Block_byref_变量名_捕获顺序
为变量名的结构体

1
2
3
4
5
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};

void (*blockDemo)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, testString, (__Block_byref_b_0 *)&b, (__Block_byref_testString2_1 *)&testString2, 570425344));

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSString *_testString, __Block_byref_b_0 *_b, __Block_byref_testString2_1 *_testString2, int flags=0) : a(_a), testString(_testString), b(_b->__forwarding), testString2(_testString2->__forwarding) {...}

根据上面的代码,可以得出结构体 __Block_byref_xxx_xx 中的 __forwarding 就是指向这个结构体的地址,同时在结构体的初始化方法中传递的是变量地址的引用,并将这个引用传递给结构体 __main_block_impl_0 中对应的外部变量,因此用 __block 修饰的外部变量是以传址的方式被 Block 捕获的。

OC 的内存管理是基于引用计数的,拷贝到 Block 中的外部变量被结构体 __Block_byref_xxx_xx__forwarding 所引用了,因此需要考虑维护对象的引用计数。

总结

最后,以我的话来总结下 Block 具体是什么?

Block 是一个结构体指针,初始化方法中接收捕获的外部变量,其中有 __block 修饰则传址,没有则传值, 结构体中的 FuncPtr 属性以函数指针的形式指向一个预定义的匿名函数,在调用时,会把 Block 作为参数传递到预定义的匿名函数中并执行。

That's all

参考

  1. Block Implementation Specification
  2. Wiki - Application binary interface
  3. 谈Objective-C block的实现
  4. iOS-Runtime-Headers
  5. a-look-inside-blocks-episode-3-block-copy
  6. How Objective-C handles the memory of immutable strings
阅读:iOS App签名的原理 理解HTTPS协议
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×