深入理解 Objective-C ☞ Block
栏目: Objective-C · 发布时间: 5年前
内容简介:日常开发中经常会用到 Block,也熟悉各种注意事项,不过对于它的底层实现并没有特别深入地挖掘过,还不能算是真正掌握了,本篇就来探究一下 Block 的底层实现原理。先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。
日常开发中经常会用到 Block,也熟悉各种注意事项,不过对于它的底层实现并没有特别深入地挖掘过,还不能算是真正掌握了,本篇就来探究一下 Block 的底层实现原理。
1.举个 :chestnut:
先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。
typedef void(^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { int age = 30; // 创建 MyBlock blk = ^{ NSLog(@"My age is %d .", age); }; // 执行 blk(); } return 0; }
2.Block的实质
为了探究 Block 的本质,我们需要借助 clang 将含有 Block 语法的源代码转换成 C++ 代码。
2.1 Block 的底层结构
终端执行 $ clang -rewrite-objc main.m
命令,就可以将 main.m
文件编译生成 main.cpp
文件,这里截取了 main.cpp
文件中与 block 相关的代码,并添加了部分注释:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 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)}; // Block 的结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; // 构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // block 的 { } 里边的代码构成的函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age); } // main() 函数 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int age = 30; MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
下面开始一步步讨论源码。从 main() 函数开始,关于自动释放池的代码不在此处讨论,先看一下 block 的创建过程:
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); // 简化后的代码: MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
我们注意到这里实际上有两个函数: __main_block_impl_0()
和 __main_block_func_0()
。
先来看后者,具体代码如下,实际是 block 的 { }
里边的代码构成的函数。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age); }
然后搜索前一个函数的函数名 __main_block_impl_0
,发现它位于下边这个结构体里边:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; // 构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; // 指明该 block 的类型(此处是栈上的 block)。 impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
而这个结构体就是 block 经编译后得到的结构,很明显 __main_block_impl_0() 是它的构造函数,我们发现,这个构造函数里边都是在给 block 的前 3 个元素 (2 个结构体和一个 int age) 赋值。
第一个元素 impl ,它的组成是这样的:
struct __block_impl { void *isa; // 用于说明 block 的类型 int Flags; // 标识位 int Reserved; // 保留字段 void *FuncPtr; // 指针 };
-
FuncPtr
是一个指针,根据名字推断应该是一个函数指针,结合main()
函数中执行构造函数创建 block 的过程可以看出,FuncPtr
指向的是 block 的{ }
里边的代码构成的函数。 -
isa
指明了block 的类型,构造函数中给它赋的值是&_NSConcreteStackBlock
,说明他是栈上的 block。关于 block 的类型,下一小节就会讲到。
第二个元素 Desc
是 __main_block_desc_0
类型的结构体,如下所示:
static struct __main_block_desc_0 { size_t reserved; // 保留字段 size_t Block_size; // block 的大小 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
其中 Block_size
从名字推断应该是结构体的大小,紧随其后定义了一个 __main_block_desc_0
类型的变量 main_block_desc_0_DATA,它的第二个元素值就是当前 block 的大小 `sizeof(struct main_block_impl_0) ,从
main() 函数中执行 block 构造函数的语句可以看出,__main_block_desc_0_DATA 最终赋值给了 block 中的
Desc`,进一步验证了 Block_size 中存放的是 block 的大小。
第三个元素 int age
是 block 捕获的一个 auto 变量,关于捕获变量的机制,后面会详细讨论。
最后回到 main()
函数的最后一行代码:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); // 简化后: blk->FuncPtr(blk);
很明显这是执行 blk 里边的指针 FuncPtr 指向的函数,而且将 blk 自己传了进去,这样,就可以在函数内部访问到 block 捕获的变量,如前文提到的 int age。
至此,本文开头的 block 的底层结构基本介绍完了,看起来比较零散,这里绘制了一张总图做个简单小结:
2.2 Block 的类型
在此,简单说明一下 block 的类型,block 的 3 种类型及其内存分布如下:
那么这 3 种类型的 Block 有什么区别呢,为了搞清楚这个问题,我们需要先回顾一下 4 种常见的变量类型及其代码示例:
- 自动变量(auto 变量)
- 静态局部变量
- 全局变量
- 静态全局变量
// *** 4 中变量的代码示例: // 全局变量 int global_var = 10; // 静态全局变量 static int static_global_var = 20; int main(int argc, const char * argv[]) { @autoreleasepool { // 自动变量(局部变量) int local_var = 30; // <==> auto int local_var = 30; // 静态局部变量 static int local_static_var = 40; MyBlock blk = ^{ NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var); }; blk(); } return 0; }
关于各种 Block 的区别,可以简单汇总成下边的图表:
也就是说:
- 如果 Block 里边访问了 auto 变量,那么他就是栈上的 Block;
- 如果没有访问 auto 变量,就是全局的 Block;
- 如果对栈上的 Block 执行了 copy 操作,就变成了堆上的 Block。
2.3 Block 的 copy
上文提到了对栈上 Block 的 copy 操作,那么为什么需要 copy 呢?原因是:设置在栈上的 Block 如果其所属的作用域结束,该 Block 就会被废弃,为了延长它的生命周期,就需要将其复制到堆上。
既然栈上的 block 经 copy 后会从栈上复制到堆上,那么另外两种 Block 执行 copy 操作又会发生什么呢? 每一种 Block 被 copy 后的结果如下:
ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如满足一下条件之一时:
- block 作为函数返回值时;
- 将 block 赋值给 __strong 指针时;
- block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;
- block 作为 GCD API 的方法参数时。
MRC 环境下,需要手动调用 block 的 copy 操作,才能将栈上的 block 复制到堆上。
3.变量捕获
为了保证 Block 内部能够正常访问外部的变量,block有个变量捕获机制,我们以前边介绍常见变量类型的代码为例,看看 Block 是怎么捕获变量的。
执行 clang -rewrite-objc main.m
之后,转换的 block 的源码如下:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // 捕获的变量 int local_var; int *local_static_var; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
从上边的源码可以看出来,对于这 4 种不同的变量,实际只捕获了 自动变量 local_var
和 静态局部标量 local_static_var
,而且自动变量是捕获了值,静态局部变量捕获的是变量地址。至于为什么这么设计,推测可能的原因如下:
3.1 捕获自动变量
实际开发中,block 捕获到的变量基本都是自动变量(局部变量),理由是:对于全局变量,任何地方都可以访问它,不安全;对于静态局部变量,它会一直存在于内存中,对内存是一种浪费。
对于基本数据类型的自动变量,前边已经讲过了,就是简单的值捕获,接下来我们重点讨论一下对象类型 auto 变量的捕获。
下边是 block 访问外部对象类型 auto 变量的简单实例。
typedef void (^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *obj = [[NSObject alloc] init]; MyBlock blk = ^{ NSLog(@"%@", obj); }; blk(); } return 0; }
执行 clang -rewrite-objc main.m
后,生成的源码中有这 2 点不同:
-
捕获了 NSObject *obj;
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; NSObject *obj; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
-
main_block_desc_0 中增加了两个指针
copy
和dispose
,整个文件里新增了 2 个函数 ` main_block_copy_0()和
__main_block_dispose_0(),结合上下问可以知道,这两个函数地址最终传给了 block 里 Desc 中的
copy和
dispose` 这 2 个指针。
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 }; static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/); }
这两个函数调用时机和作用如下:
-
如果 block 从栈上拷贝到堆上
会调用 block 内部的
copy()
函数,此函数内部会调用_Block_object_assign()
函数,它会根据 auto 变量的修饰符( strong、 weak、__unsafe_unretained)做出相应的操作,形成 强引用 或 弱引用。 -
如果 block 从堆上移除
会调用 block 内部的
dispose()
函数,此函数内部会调用_Block_object_dispose()
函数,它会自动释放引用的 auto 变量(即 release)。
另外,如果 block 一直是在栈上,将不会对 auto 变量产生强引用。
3.2 捕获 __block 变量
前边我们只是在 block 内部 使用
变量,事实上,如果直接 修改
变量的话,比如下边这个例子,就会报错:此变量不可赋值 (错误信息见注释)。
typedef void (^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; MyBlock blk = ^{ age = 20; // Error: Variable is not assignable (missing __block type specifier) NSLog(@"%d", age); }; blk(); } return 0; }
按照错误信息的提示,如果给 int age = 10;
前边加上 __block
,就可以解决 block 内部无法修改 auto 变量的问题,实际操作后,发现果然可以正常输出 age 的新值 20。
3.2.1 __block 变量能够被 block 修改的原因
现在来看看 __block 修饰符到底做了什么,先将上边的代码转成 C++ 源码,下边截取了其中部分关键代码:
// 新出现的结构体 struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __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->__forwarding->age) = 20; NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age)); } int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; MyBlock blk = ((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 *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
先看 main()
函数, __block int age
变成了 __Block_byref_age_0
类型的 age
,也就是说编译器将 __block 修饰的变量包装成了一个新的结构:
struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; };
__Block_byref_age_0
这个结构体里边有一个 int age ,用于存储 age 的值 (10)。还有一个重要成员 __Block_byref_age_0 *__forwarding;
,结合 block 的构造函数,我们知道 __forwarding 指针实际指向了它所在的结构体。
之所以这么做是为了当 block 被拷贝到堆上以后,无论访问栈上的 block 还是 堆上的 block,最终都是访问的堆上的同一个 block(拷贝后,堆上的 forwarding 指向自己所在的 block 变量,栈上的 forwarding 指向堆上的 block 变量),如下图所示。
接下来,看看 block 的结构 __main_block_impl_0
,里边多了一个变量 __Block_byref_age_0 *age;
,即 block 捕获了这个新的结构体 __Block_byref_age_0
的地址,所以 block 里边就可以通过地址访问这个结构体,进而修改里边 int age 的值。
__block 修饰的对象类型的 auto 变量与此类似,差别仅在于新生成的结构体:
从上图可知,__block 修饰的对象类型转换后的结构体里边多了两个函数指针,他们分别指向下面 2 个函数,负责内存管理的相关操作,下边就会讲到。
static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); }
static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); }
3.2.2 被 __block 修饰的对象类型的内存管理
对于 被 __block 修饰的对象类型,内存管理分以下 3 种情况:
-
1.当 __block 变量 在栈上时,不会对指向的对象产生强引用。
-
2.当 __block 变量 被 copy 到堆时,分两种情况:
- ARC 环境下,会调用
__block 变量内部
的 copy 函数,它会调用_Block_object_assign()
函数,此函数会根据所指向对象的修饰符( strong、 weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
- ARC 环境下,会调用
- MRC 环境下,不会形成强引用(retain)。
typedef void (^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { __block NSObject *obj = [[NSObject alloc] init]; MyBlock block = [^{ NSLog(@"%p", obj); } copy]; block(); [obj release]; } return 0; }
如上所示,在 MRC 环境下,对象前加了 __block,不会对 block 形成强引用, 即当执行完 [obj release];
之后,person 就被释放了。
- 3.如果 block 变量从堆上移除,会调用 ` block 变量内部
的
dispose()函数,它会调用
_Block_object_dispose()` 函数,此函数会自动释放指向的对象(release)
3.2.3 对象类型的auto变量 和 __block 变量
-
当block在栈上时,对它们都不会产生强引用
-
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们
-
对于 __block变量(假设变量名叫做a),最终会执行
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
-
对于对象类型的 auto 变量(假设变量名叫做p),最终会执行
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
以上两者最终调用的方法是相同的,只不过最后一个参数有差别,前者是 8(表示引用类型),后者是 3 (表示对象),下面对
_Block_object_dispose()
函数的调用与之类似。
-
-
当 block 从堆上移除时,都会通过 dispose 函数来释放它们
-
对于 __block变量(假设变量名叫做a),最终会执行
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
-
对于对象类型的auto变量(假设变量名叫做p),最终会在执行
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
-
4.循环引用
关于使用 block 可能遇到的循环引用问题,我们分 ARC 和 MRC 两种情况进行讨论。
ARC 环境
在 ARC 环境下,目前大概有 3 种常见的解决循环引用的方式:
- __weak
此方式是最常见也是推荐使用的一种方式,基本原理是,self 对 block 的引用维持强引用,不过将 block 对 self 的引用改成了弱引用。
__weak typeof(self) weakSelf = self; self.block = ^{ printf("%p", weakSelf); };
- __unsafe_unreturned
这种方式与上边的方式类似,不过当 weakSelf 指向的对象销毁后,指针已然指向那块已经被回收的内存,可能发生野指针错误,所以是不安全的。
__unsafe_unretained typeof(self) weakSelf = self; self.block = ^{ printf("%p", weakSelf); };
- __block
__block typeof(self) weakSelf = self; self.block = ^{ printf("%p", weakSelf); weakSelf = nil; }; self.block();
我们知道,当在变量前边加了 block 之后就多了一个 blcok 变量,于是里边的引用关系就变成了:
为了打破这个循环引用的关系,需要在 block 里边将对象置为 nil,而且必须执行 block 才能断开 __block 变量对对象的强引用。
MRC 环境
MRC 环境下解决循环引用的方式与 ARC 环境类似,只是由于 MRC 环境下不可以使用 weak,所以只有 __unsafe_unreturned
和 __block
2 种解决方式。对于 __block
的方式,在MRC中, __block 变量
不会对 weakSelf 产生强引用,也就不需要将其置为 nil 并执行 block 了。
__block typeof(self) weakSelf = self; self.block = ^{ printf("%p", weakSelf); };
以上所述就是小编给大家介绍的《深入理解 Objective-C ☞ Block》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Joy of X
Niall Mansfield / UIT Cambridge Ltd. / 2010-7-1 / USD 14.95
Aimed at those new to the system seeking an overall understanding first, and written in a clear, uncomplicated style, this reprint of the much-cited 1993 classic describes the standard windowing syste......一起来看看 《The Joy of X》 这本书的介绍吧!