前语
这篇文章会有一点长,会从以下几个点进行对 Block 的分析(当然也是基于源码):
- Block 的界说
- Block 底层完成
- Block 的本质
- Block 怎么截获主动变量
-
__block
的原理 - Block 形成的循环引证问题及处理方案
Blocks 的界说
Block 的界说是:带有主动变量(局部变量)的匿名函数。
所以匿名函数便是不带姓名的函数,带有主动变量值表现为 “截获主动变量值”。
比方:
int a = 0;
void (^blk)(void) = ^{
printf("%d", a);
};
a = 1;
blk();
^{ ... }
其实便是一个匿名函数,也便是 Block。
在将 a
赋值为 1
之后再履行 blk
,发现输出是:
0
也便是在界说 blk
时,就现已捕获 a
其时的值,也便是 0
,这便是截获主动变量值。
Blocks 的完成
Block 其实编译之后,便是一段一般的 C 言语代码。而 Block 的本质,便是一个 Objective-C 的目标。
比方下面一段代码:
int main()
{
void (^blk)(void) = ^{
printf(@"Block\n");
};
blk();
}
编译之后的代码为:
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("Block\n");
}
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[]) {
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;
}
大致的结构如下图:
其实也不杂乱,咱们逐一击破。
首先 Blocks 中的函数,在 C 言语中被转化成 __main_block_func_0
,能够看到里面履行的办法便是 printf("Block\n")
,所以其实 Blocks 运用的匿名函数实际上是被转化成了一个简略的 C 函数,它的命名规则是根据 Blocks 所属的函数名(这儿是 main
) 和该 Blocks 语法在该函数中出现的顺序值(这儿是 0)来命名的。
这个函数还有一个参数:struct __main_block_impl_0 *__cself
,__cself
是结构体 __main_block_impl_0
的指针。
去掉结构函数,__main_block_impl_0
的界说如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
两个成员分别对应两个结构体:__block_impl
和 __main_block_desc_0
,这两个结构体咱们后边细说,先来看一下结构函数:
__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;
}
_NSConcreteStackBlock
是栈 Block,这个也放在后边聊,看一下结构函数是怎么调用的,在 main
办法中:
// 去掉转化的部分
struct __main_block_imp_0 tmp =
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
这段代码的意思是,将 __main_block_impl_0
结构体类型的主动变量,即栈上生成的 __main_block_impl_0
结构体实例的指针,赋值给 __main_block_impl_0
结构体指针类型的变量 blk
,以下为这部分代码对应的最初源代码:
void (^blk)(void) = ^{ printf("Block\n"); };
将 Block 语法生成的 Block 赋给 Block 类型变量 blk
,它等同于将 __main_block_impl_0
结构体实例的指针赋给变量 blk
。该代码中的 Block 便是 __main_block_impl_0
结构体类型的主动变量,即栈上生成的 __main_block_impl_0
结构体实例。
下面就来看看 __main_block_impl_0
结构体实例结构参数:
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
第一个参数是由 Block 语法转化的 C 言语函数指针。第二个参数是作为静态全局变量初始化的 __main_block_desc_0
结构体实例指针,以下为 __main_block_desc_0
结构体实例的初始化部分代码:
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
运用 __main_block_impl_0
结构体实例的大小,进行初始化。
假如打开 __main_block_impl_0
中的 __block_impl
:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
然后它会像下面这样进行初始化:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = __main_block_desc_0_DATA;
这儿是将 __main_block_func_0
的函数指针赋值给了成员变量 FuncPtr
。
在最初的代码中,调用 Block 的函数是:
blk();
它被转化成了:
((__block_impl *)((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉转化的部分为:
(*blk->imp1.FuncPtr)(blk);
这便是简略的运用函数指针调用函数。正如咱们前面说到的,由 Block 语法转化的 __main_block_func_0
函数的指针被赋值到了 FuncPtr
中。别的,__main_block_func_0
函数还有一个参数 _cself
,这儿传递的是 blk
,也便是 _cself
指向了 Block 值。
到这儿现已解说清楚了 Block 在底层是怎么完成的。下面看一下咱们前面留下的问题,_NSConcreteStackBlock
是什么。
关于 _NSConcreteStackBlock
isa = &_NSConcreteStackBlock;
将 Block 指针赋给 Block 的结构体成员变量 isa
。
为了了解它,咱们先来阐明一下 Objective-C 中类和目标的本质。
在 Objective-C 用于存储目标的类型是 id
,在 runtime 源代码中的声明如下:
typedef struct objc_object *id;
struct objc_object {
Class isa;
}
Class
也是一个结构体,继承自 objc_object
。
typedef struct objc_class *Class;
struct objc_class : objc_object {
}
对于一个 Objective-C 的类:
@interface MyObject : NSObject
{
int val0;
int val1;
}
@end
该类的目标编译后如下:
typedef struct objc_object MyObject;
struct MyObject_IMPL {
Class isa;
int val0;
int val1;
};
MyObject
类的实例变量 val0
和 val1
被直接声明为目标的结构体成员。Objective-C 中由类生成目标,意味着由类生成该目标的结构体实例。生成的各个目标,其实便是由该类生成的目标的各个结构体实例,然后这些结构体实例经过成员变量 isa
坚持了该类的结构体实例指针。
MyObject_IMPL
能够了解为类的元数据,它会经过类型的办法与类的 objc_class
结构体连接起来,MyObject_IMPL
的内存大小在编译时就确认了,能够看到它包含了成员变量,这也能够解说为什么不能在分类中添加成员变量,因为类生成的结构体在编译时就现已确认了内存,不允许再修正,而分类是在运行时才会动态加载,此刻是不能改动到编译时就现已确认的类的内存的。
这儿所生成的 MyObject_IMPL
持有的 isa
,指向的便是 MyObject
这个类的 object_class
结构体实例,在这个结构体实例中持有了声明的成员变量、办法的称号、办法的完成(函数指针),属性以及父类的指针:
struct objc_class: objc_object {
Class isa; // 继承自 objc_object
Class superclass; // 父类
cache_t cache; // 办法缓存
class_data_bits_t *bits; // 办法完成等
}
回到刚刚的 Block 结构体:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
此 __main_block_impl_0
结构体相当于基于 objc_object
结构体的 Objective-C 类目标的结构体。别的,对其间的成员变量 isa
进行初始化,详细如下:
isa = &_NSConcreteStackBlock;
即 _NSConcreteStackBlock
相当于 objc_class
结构体实例。在将 Block 作为 Objective-C 的目标处理时,关于该类的信息放在 _NSConcreteStackBlock
中。
到这儿,应该就能阐明为什么 Block 便是 Objective-C 的目标了。
截获主动变量值的底层完成
来看下面一段代码:
int main()
{
int a = 0;
int b = 99;
void (^blk)(void) = ^{
printf(@"%d", a);
};
a = 1;
blk();
}
转化后:
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d", a);
}
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[]) {
int a = 0;
int b = 99;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 1;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
Block 语法表达式中运用的主动变量被作为成员变量追加到了 __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
结构体内声明的成员变量类型与主动变量类型完全相同。请注意,Block 语法表达式中没有运用的主动变量不会被追加,变量 b
就没有追加。主动变量捕获只针对 Block 中运用的主动变量。
结合 main
中的调用和 __main_block_impl_0
的界说,能够得到 __main_block_impl_0
结构体实例的初始化如下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_0_DATA;
a = 0;
由此可见,在 __main_block_impl_0
结构体实例(即 Block)中,主动变量值被捕获。
再看 ^{ printf("%d", a); }
办法,该源代码转化为以下函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int a = __cself->a;
printf("%d", a);
}
这儿运用的,是被捕获到 __main_block_impl_0
结构体中的成员变量上的主动变量 a
,也便是 0。
所以,所谓 “截获主动变量值”,意味着在履行 Block 语法时,Block 语法表达式所运用的主动变量值被保存到 Block 的结构体实例(即 Block 本身)中。
__block
主动变量值捕获只能保存履行 Block 语法其时的值,保存后就不能改写该值:
int a = 0;
void (^blk)(void) = ^{
a = 1;
};
blk();
这样会报错:
Variable is not assignable (missing __block type specifier)
假如想在 Block 中修正 a
,就需求在 a
被界说的时分加上 __block
阐明符:
__block int a = 0;
void (^blk)(void) = ^{
a = 1;
};
blk();
这样就没有问题了。
咱们需求处理两个问题:
- 为什么会报错,
- 为什么运用
__block
就能够了
你能够想象之前的变量 int a
是 A,被 Block 截获后的变量 block a
是 B,两个是不同的东西,在 Block 内操作的都是 B,就算你真的能够在 Block 内修正,改的也是 B 而不是 A,可是在运用者看来,它们都是 A,所以编译器在遇见这种行为时,直接报错,不允许修正。
__block
阐明符更准确的表述是 “__block
存储域类阐明符”,C 言语中有以下存储域类阐明符:
- typedef
- extern
- static
- auto
- register
它们用于指定将变量值设置到哪个存储域中。例如,auto
表明作为主动变量存储在栈中,static
表明作为静态变量存储在数据区中。
咱们编译一下运用 __block
后的代码:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 1;
}
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*/);}
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[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
能够看到 a
变成了一个结构体:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
最后的成员变量 a
相当于原主动变量的成员变量。
而赋值的代码:^{ a = 1; }
转化成了:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 1;
}
Block 的 __main_block_impl_0
结构体实例持有指向 __block
变量的 __Block_byref_a_0
结构体实例的指针。
__Block_byref_a_0
结构体实例的成员变量 __forwarding
持有 “指向该实例本身” 的指针。经过成员变量 __forwarding
拜访成员变量 a
。(成员变量 a
是该实例本身持有的变量,它相当于原主动变量。)
别的,__block
变量生成的 __Block_byref_a_0
结构体并不在 __main_block_impl_0
结构体中,这样做是为了在多个 Block 中运用 __block
变量。
也便是 int a
,咱们先称它为 A,被 __block
阐明符润饰之后,直接变成了别的一个变量 C,之后不管在 Block 内运用仍是 Block 外运用的,都是变换后的 C,所以在 Block 内能够修正,因为改的都是同一个东西。
比方:
int main()
{
__block int a = 0;
a = 1;
}
转化后的代码为:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
int main() {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
(a.__forwarding->a) = 1;
return 0;
}
Block 存储域
经过前面的阐明,咱们能够知道Block 会转化为 Block 的结构体类型的主动变量,__block
变量会转化为 __block
变量的结构体类型的主动变量。所谓结构体类型的主动变量,即栈上生成的结构体的实例。
而且也知道 Block 其实便是 Objective-C 中的目标,之前的例子中,咱们生成的 Block 对应的类为 _NSConcreteStackBlock
,在变换后的源码中没有关于它的信息,但有很多与之相似的类:
-
_NSConcreteStackBlock
(目标存放在栈上) -
_NSConcreteGlobalBlock
(与全局变量相同,存放在数据区域(.data
区)) -
_NSConcreteMallocBlock
(存放在由malloc
函数分配的内存块(堆)中)
到目前为止咱们 Block 例子中运用的都是 _NSConcreteStackBlock
类,且都设置在栈上。事实上当然并非都是这样的,比方在记述全局变量的当地运用 Block 语法时,生成的 Block 为 _NSConcreteGlobalBlock
类目标,例如:
void (^blk)(void) = ^{ printf("Global Block\n"); };
int main()
{
}
这段代码也会像之前相同,转化为一个结构体,结构体中的 isa
初始化如下:
impl.isa = &_NSConcreteGlobalBlock;
此 Block 的结构体实例放置在程序的数据区域中。
当:
- 记述全局变量的当地有 Block 语法时
- Block 语法的表达式中不运用应截获的主动变量时
Block 为 _NSConcreteGlobalBlock
目标,除此之外的 Block 语法生成的 Block 为 _NSConcreteStackBlock
类目标,且设置在栈上。
那么堆上的 _NSConcreteMallocStack
在啥时分用呢?
设置在栈上的 Block,假如其所属的变量效果域完毕,则该 __block
变量也会被抛弃:
但 Block 有一个特性,便是它在超出变量效果域的时分仍可运用。
这是经过将 Block 和 __block
变量从栈上复制到堆上来完成的,将在栈上的 Block 复制到堆上,这样即使 Block 语法地点的变量效果域完毕,堆上的 Block 还能够继续存在:
咱们来看一下复制的进程,下面有一段回来 Block 的函数:
typedef int (^blk)(int);
blk_t func(int rate) {
return ^(int count) {
return rate * count;
};
}
该代码为回来装备在栈上的 Block 的函数,程序履行中从该函数回来函数调用方时变量效果域完毕,因而栈上的 Block 也被抛弃,虽然有这样的问题,可是该代码经过编译器转化如下:
blk_t func(int rate)
{
blk_t tmp = &__func_block_impl_0(
__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
查看 objc4 的源码,发现 objc_retainBlock
函数实际上便是 _Block_copy
函数:
/**
* 将经过 Block 语法生成的 Block,
* 即装备在栈上的 Block 结构体实例
* 赋值给相当于 Block 类型的变量 tmp 中
*/
tmp = _Block_copy(tmp);
/**
* _Block_copy 函数
* 将栈上的 Block 复制到堆上。
* 复制后,将堆上的地址作为指针赋值给变量 tmp
*/
return objc_autoreleaseReturnValue(tmp);
/**
* 将堆上的 Block 作为 Objective-C 目标
* 注册到 autoreleasepool 中,然后回来该目标
*/
将 Block 作为函数回来值回来时,编译器会主动生成复制到堆上的代码。
大多数状况下编译器会进行恰当的判别,除此之外的状况下需求手动生成,将 Block 从栈上复制到堆上,此刻需求运用 copy
办法。
编译器不能判别的状况:
- 向办法或函数的参数中传递 Block 时
不过这种状况下还有一些是编译器能够判别的:
- Cocoa 结构的办法且办法名中含有
usingBlock
时 - GCD 的 API
按装备 Block 的存储域,调用 copy
办法:
__block 变量存储域
当 Block 从栈上被复制到堆上时,其间的 __block
变量也会一并复制到堆上。
当 Block 还在栈上时,__block
是作为栈中的一个独自的结构体存在,但 Block 被赋值到堆上时,Block 会去持有 __block
变量。
当多个 Block 运用 __block
时也是相同,在栈上时,__block
变量依然是作为一个独自的结构体存在,当被复制到堆上后,每多一个引证都会添加 __block
的引证计数。
假如装备在堆上的 Block 被抛弃,那么它所运用的 __block
变量也就被开释。
当栈上的 __block
被赋值到堆上时,会将它结构体中的成员变量 __forwarding
的值替换成堆上的 __block
的结构体实例地址,所以无论是在 Block 内仍是 Block 外运用 __block
,也不管是在栈上仍是堆上,都能够正确的拜访同一个 __block
变量。
以下状况,栈上的 Block 会复制到堆上:
- 调用 Block 的
copy
实例办法时 - Block 作为函数回来值回来时
- 将 Block 赋值给附有
__strong
润饰符 id 类型的类或 Block 类型成员变量时 - 在办法名中含有
usingBlock
的 Cocoa 结构办法或 GCD 的 API 中传递 Block 时
Block 的循环引证
假如在 Block 中运用附有 __strong
润饰符的目标类型主动变量,那么当 Block 从栈赋值到堆时,该目标为 Block 所持有,这样容易引起循环引证。
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t _blk;
}
@end
@implementation MyObject
- (instancetype)init {
if (self = [super init]) {
_blk = ^{ NSLog(@"self = %@", self); };
}
return self;
}
@end
int main(int argc, const char * argv[]) {
MyObject *o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
这段代码履行后,MyObject
的 dealloc
办法不会被调用,也便是 MyObject
不会被开释。
因为 MyObject
持有 Block,而在 init
办法中履行的 Block 语法里,运用了附有 __strong
润饰符的 id 类型变量 self
,而且因为 Block 语法赋值给了成员变量 _blk
,会导致 Block 从栈上被赋值到堆,并持有所运用的 self
。self
持有 Block,Block 持有 self
,这便是循环引证。
能够运用 weak
来避免循环引证:
- (instancetype)init {
if (self = [super init]) {
id __weak tmp = self;
_blk = ^{ NSLog(@"self = %@", tmp); };
}
return self;
}
一些面试题
体系的 block 中运用 self 为什么能够不必弱引证?
[UIView animateWithDuration:duration animations:^{
[self.superView layoutIfNeed];
}];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.a = 10;
}];
...
这些体系的 Block 不会形成循环引证的原因是,这些是类办法,Block 不被 self
所持有,而是被其他目标持有,所以不会形成循环引证。
GCD 中的 api 中运用 self 为什么能够不必弱引证?
很简略,因为 self
并不持有 GCD 的东西,只是 GCD 的 Block 持有了 self
罢了,所以不会有循环引证的问题。
为什么 Block 中还需求写一个 strong self?
在避免循环引证的时分运用了 __weak
润饰符,然后再在 Block 内部运用 __strong
去润饰:
- (instancetype)init {
if (self = [super init]) {
id __weak tmp = self;
_blk = ^{
id __strong strongTmp = tmp;
NSLog(@"self = %@", strongTmp);
};
}
return self;
}
这是为了避免在 Block 履行的进程中,self
现已被开释了,这儿对应的是 tmp
,咱们选用 __weak
润饰的 tmp
在开释后会被设置为 nil
,Objective-C 不会履行 nil
所调用的那些办法,程序倒不会崩溃。可是履行的成果可能会和咱们预想的不一致,因为咱们是想在 Block 中去输出关于 self
的一些内容,但假如 self
被开释了,咱们的输出就都是 nil
,相似这种需求 Block 在履行时,所引证的目标不会被开释的状况,就能够运用 __strong
再润饰一次,这儿被 __strong
所润饰后的 strongTmp
,就像一个在 Block 内部界说的主动变量,跟随 Block 的生命周期,在 Block 履行完毕后,strongTmp
也会被开释。