Block
前前后后看了4、5遍《Objective-C高级编程》的Block模块,对Block相关的内容有一定的理解,为了方便深入理解Block,结合书中的内容按照个人的理解逻辑对Block相关模块进行梳理,对Block的本质和Block截获变量两个模块内容进行深入解析。希望通过阅读本文章能够清楚的了解Block的原理及正确使用Block。
一、Block的本质
1.1 Block结构说明
1.1.1 含Block代码段
下面代码包含设置block并使用。
#include <stdio.h>
int main() {
int a = 10;
void(^blk)(void) = ^{
printf("%d", a);
};
blk();
return 0;
}
1.1.2 LLVM编译后的代码
通过clang -rewrite-objc +文件名查看转化后的内容,去除include相关内容。
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;
__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) {
printf("123");
}
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() {
void(*blk)(void) = ((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;
}
1.1.3 代码段介绍
本小节中,将前文所涉及的代码部分一一介绍,以了解编译器是如何将Block编译成C++语言,进而对Block有更深入的理解。
__main_block_impl_0结构体:
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;
}
};
1. block名称。__main_block_impl_0表示main函数的第一个block,如果main函数中有多个block编译器会以最后的数字进行逐字增加。
2. __main_block_impl_0的初始化。初始化无自动变量的Block,需要传入block所定义的函数、block的描述文件(block的大小)、及可选项flag。通过函数、flags及系统判定的block类别设置__block_impl结构体。
3. 该代码不存在自动变量所以结构体中没有关于变量相关内容,而block持有变量会在第二章节中详细说明。现阶段只需要知道如果存在变量,在初始化阶段也需要将变量传入并设置相关的值,对应的结构体也会出现变化。
__block_impl结构体:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
1. __block_impl结构体,可以理解为block结构体的基类,任何block会有__block_impl结构体。
__main_block_desc_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)};
block的方法:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("123");
}
1. 函数名。__main_block_func_0代表main函数中block的第一个函数。命名规则类似block。
main函数:
int main() {
void(*blk)(void) = ((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;
}
1. __main_block_impl_0的初始化通过__main_block_func_0+__main_block_desc_0_DATA。
2. 通过blk.FuncPtr调用传入的方法。
1.1.4 总结
从上一小节中我们可以知道iOS的block通过编译器在编辑阶段将block编译成对应的结构体,将对应函数及参数在block初始化阶段传入,使block持有函数及相关的变量。
1.2 Block本质
通过block的初始化函数及block的结构体会发现在初始化block时,系统会自动填充impl.isa指针的值,在上文的例子中impl.isa = &_ NSConcreteStateBlock。对runtime熟悉的小伙伴应该对isa指针不陌生 实例对象的isa指针指向父类,父类的isa指针指向元类,元类的isa指向NSObject。而block就是实例对象,它的isa指针指向了_NSConcreteStateBlock,而 _NSConcreteStateBlock的isa指向了其元类最终指向了NSObject,所以block的实质就是对象。
1.2.1 block类型
1.1节例子中block类型为_NSConcreteStateBlock,实际上block一个有3种类型:
_NSConcreteStateBlock: block存在栈中。
_NSConcreteMallocBlock: block存在堆中。
_NSConcreteGlobalBlock: block存在在data数据段中。
block的类型由编译器决定,编译器会根据block的作用域分配block类型。block是全局变量设置如下所示,其类型就为_NSConcreteGlobalBlock。
#include <stdio.h>
void(^blk)(void) = ^{
printf("122");
};
int main() {
blk();
return 0;
}
_NSConcreteMallocBlock类型的block需要手动对栈类型block进行copy才会修改为堆block,具体堆block的用户及与栈block的关系会在下一节变量的自动截获中有详细的说明。
二、Block截获自动变量
本节将介绍block是如何持有变量,对上面代码进行修改。
#include <stdio.h>
int main() {
int a = 10;
void(^blk)(void) = ^{
printf("%d", a);
};
blk();
return 0;
}
2.1 截获自动变量
根据本届开头的代码段进行clang -rewrite-objc+文件名的操作得到跟1.1.2中类似的部分,不同的内容如下所示:
__main_block_impl_0结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
1. 结构体包含a。局部变量a会追加成员变量到block的结构体中,未使用的变量不会被追加到结构体中。
2. 初始化函数。初始化函数增加了int参数。
__main_block_func_0方法:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d", a);
}
1. 取值。通过__main_block_impl_0结构体对象获取结构体中的值。
2. bound by copy。编译器自带的注释“// bound by copy”表示block对它引用的局部变量做了只读拷贝,也就是说block引用的是局部变量的副本。因此获取的自动变量值无法在block进行修改,且外部改变变量的值也不会改变block中变量的值。
2.2 __block修饰的变量
2.1节中介绍了block截获局部变量的原理,然而其对引用的局部变量做的是只读拷贝,因此无法修改截获的局部变量值。为了能够修改截获的自动变量,可以使用__block修饰符修饰变量。
2.2.1 __block导致block结构体变化
main函数修改为:
#include <stdio.h>
int main() {
__block int a = 10;
void(^blk)(void) = ^{
printf("%d", a);
};
blk();
return 0;
}
通过clang编译,再来看看block结构体。
__main_block_impl_0结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
1. __Block_byref_a_0。变量a增加了__block修饰符变为__Block_byref_a_0,关于结构体内容之后会介绍。
2. 初始化函数。在设置a的值是将a.forwording赋值给block的成员变量a。思考如此设计的原因。
__Block_byref_a_0 结构体:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
1. *__forwarding。指向自己的指针,通过__Block_byref_a_0->__forwarding->a的方式获取a的值。
__main_block_desc_0结构体:
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->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
1. __main_block_desc_0。__main_block_desc_0增加了copy和dispose方法。
2. __main_block_copy_0。_Block_object_assign将对象赋值给对象结构体的成员变量中,相当于retain操作。
3. __main_block_dispose_0。_Block_object_dispose释放赋值在结构体的成员变量,相当于release操作。
4. 通过查看main函数,并没有主动调用copy和dispose函数
copy的调用时机:栈上的block复制到堆上。
1. 通过调用block的copy实例方法。
2. block作为返回值。
3. 将block赋值给__strong修饰的block类型的变量。
dispose的调用时机:堆上的block释放。
1. 没有对象持有block调用dispose。
2.2.2 __Block_byref_a_0结构体设计
本节将介绍__Block_byref_a_0结构体中的 _ _forwarding指向自己的指针设计的意义。通过copy可以变量复制到堆上,正常情况下栈上的变量修改不会对堆上的对象有影响,然而这对于使用 _ _block修饰的变量而言是个灾难,也背离了 _ _block设计的初衷,所以引入 _ _forwarding,在变量复制到堆上其指针指向堆上的地址。
如下图所示:
三、更多
3.1 Block的循环引用
在block中使用__strong修饰的对象,当block从栈复制到堆,该对象就会被block所持有,就会造成循环引用。
伪代码:
class V: ViewController {
var i: Int = 0
override func viewDidLoad() {
let block = {
print("输出\(self.i)")
}
block.copy()
block()
}
deinit() {
print("deinit")
}
}
通过对block的拷贝,堆上block持有self对象,而self又持有block,所以造成了循环引用。使用weak修饰符修饰self,使block不持有self即可。