前言 本文将会从内存结构上分析 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; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; unsigned long int size; void (*copy_helper)(void *dst, void *src); void (*dispose_helper)(void *src); const char *signature; } *descriptor; };
关于 ABI
在计算机软件中,应用程序二进制接口(ABI
)是两个程序模块之间的接口,其中一个程序模块通常是机器代码级别的库或操作系统。 ABI
确定如何调用函数以及在系统调用的情况下将二进制格式信息从一个程序组件传递到下一个程序组件或操作系统的详细信息。
Block
: 结构中有一个 isa
指针,初始化时指向 _NSConcreteStackBlock
或 _NSConcreteGlobalBlock
的地址, 用于实现对象相关的功能。
flags
: 用于按 bit
位表示一些 block
的附加信息
reserved
: 保留位
void (*invoke)(void* ,...)
: 接收可变参数的函数指针,指向 block
实现的函数调用 (invoke
) 地址。
descriptor
: Block_descriptor_1
结构体的指针,表示该 block
的附加描述信息,主要是 size
大小,以及 copy
和 dispose
函数的指针。
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 { 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: 0000000100000 f10 push rbp ; Objective C Block defined at 0x100001060 , XREF=0x100001070 0000000100000 f11 mov rbp, rsp0000000100000 f14 sub rsp, 0x10 0000000100000 f18 lea rax, qword [ds:cfstring_Hello_world] ; @"Hello world" 0000000100000 f1f mov qword [ss:rbp+var_8], rdi0000000100000 f23 mov qword [ss:rbp+var_10], rdi0000000100000 f27 mov rdi, rax ; argument "format" for method imp___stubs__NSLog0000000100000 f2a mov al, 0x0 0000000100000 f2c call imp___stubs__NSLog0000000100000 f31 add rsp, 0x10 0000000100000 f35 pop rbp0000000100000 f36 ret ; endp 0000000100000 f37 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 { int a = 1 ; NSString *testString = @"2" ; void (^blockDemo)() = ^() { NSLog (@"Hello world,%d,%@" ,a,testString); }; blockDemo(); } return 0 ; }
同样操作得到如下截图
如果取消 ARC
环境, isa
指针会指向 __NSStackBlock__
,在此不再截图说明。
小结一
验证了 Block
中 invoke
函数指针指向 block
实现的函数地址。
验证了 imported variables
确实保存了从外部 capture
到 Block
中的变量
问题一 Block
中 isa
指针指向的对象和 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; }; #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[]) { { __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
函数的执行流程
函数 __main_block_impl_0
以 __main_block_func_0
与 __main_block_desc_0_DATA
作参数传入来完成结构体 __main_block_impl_0
的初始化
函数 __main_block_func_0
在这里即为 NSLog(@"Hello world")
,将其地址传递给结构体 __main_block_impl_0
中的 __block_impl
结构体 impl
的函数指针 FuncPtr
参数 __main_block_desc_0
传递到结构体 __main_block_impl_0
中的 __main_block_desc_0
结构体的 Desc
blockDemo
函数指针引用了前面结构体初始化中获得的 __main_block_impl_0
结构体
blockDemo
函数指针引用的结构体强转为 __block_impl *
类型,因为结构体 __main_block_impl_0
的内存结构中最前面就是 __block_impl
结构体的 impl
,因此在这里强转类型借用一下面向对象的概念就是子类强转成父类指针,是没有问题的。当然面向过程早于面向对象,面向对象为什么能实现父类指针强转成子类,反倒是应该要依赖两者兼容的内存结构。
执行第 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
输出 Block
的 isa
对象为类似 __NSStackBlock__
这种的类型的
在实践二中用 clang
重写 OC
代码,Block
的 isa
指针指向 _NSConcreteStackBlock / _NSConcreteStackBlock
哪个才是正确的呢?
思考之后,我觉得是这样的:
从内存角度上看,isa
中保存的就是一个内存地址。我们用 po
来输出时,就是告诉 lldb
把我传入的这个内存地址认为是一个对象的起始地址,以一个对象的角度去看它,因为 Apple
对 Block
进行了相应的封装,自然输出对应的 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; NSString *testString = __cself->testString; 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 );}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void *)src->testString, 3 );}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[]) { { __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
的多了两个 a
和 testString
属性接收外界传入的参数。
结构体 __main_block_desc_0
多了 copy
和 dispose
两个函数
多了 __main_block_copy_0
和 __main_block_dispose_0
两个静态方法
查看 Block-private.h
1 2 3 4 5 6 7 8 9 10 11 enum { BLOCK_FIELD_IS_OBJECT = 3 , BLOCK_FIELD_IS_BLOCK = 7 , BLOCK_FIELD_IS_BYREF = 8 , BLOCK_FIELD_IS_WEAK = 16 , BLOCK_BYREF_CALLER = 128 , };
因为 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 { 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 { 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; __Block_byref_testString2_1 *testString2; __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; __Block_byref_testString2_1 *testString2 = __cself->testString2; int a = __cself->a; NSString *testString = __cself->testString; 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_object_assign((void *)&dst->testString, (void *)src->testString, 3 );_Block_object_assign((void *)&dst->testString2, (void *)src->testString2, 8 );}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void *)src->b, 8 );_Block_object_dispose((void *)src->testString, 3 );_Block_object_dispose((void *)src->testString2, 8 );}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[]) { { __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
参考
Block Implementation Specification
Wiki - Application binary interface
谈Objective-C block的实现
iOS-Runtime-Headers
a-look-inside-blocks-episode-3-block-copy
How Objective-C handles the memory of immutable strings