背景
每次看完 狮子书🦁️ 的 Block 部分,过一段时间记忆就会有点模糊...
想达到熟悉源码实现的程度,恐怕还需要反复翻看和一定场景的锻炼 ,而大部分开发日常也碰不到需要源码的场景。
不过 Block 作为 iOS 日常开发高频使用的东西,无论如何还是需要想办法去加深记忆的。
于是尝试做笔记,再加上一些自己的 图解,图像理解起来更直观容易,也许能帮助我们(可能只是菜鸡的我)更好的记忆。
文章主要按下面顺序来做(熟悉使用的开发者,可以直接跳到 Block 实现的部分看):
- Block 语法
- Block 使用
- Block 实现 - Block 如何做到捕获变量
- Block 实现 - Block 为什么不能直接对捕获的自动变量赋值?
- Block 实现 - __Block 说明符的相关实现
- Block 存储域 - NSConcreteStackBlcok / NSConcreteMallocBlock 的关系
- Block 存储域 - 为什么要有堆 Block?
- Block 存储域 -
__Block 变量
如何从栈复制到堆 - Block 存储域 - 为什么
__Block 变量
要通过__forwarding
访问? - Block 存储域 - 堆 Block 截获对象的引用和释放
本文内容基本根据 《Objective-C 高级编程 - iOS 与 OS X 多线程和内存管理》而来,推荐所有 iOS 开发者阅读 📖
Block 语法
对于如何定义一个 Block,首先一定要去理解 Block 的 BN 范式 (Backus-Naur Form):
Block_literal_expression ::= ^ block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
关键在于记忆 block_decl
的定义 ,就能比较理解容易 Block 的语法了。
如果不太明白 BN 范式 的意思,尝试看看 BNF范式(巴科斯范式) 会有助于我们理解,其实就是这副图的关系:
再结合 Block 具体的 4 种写法:
^
返回值类型
参数列表
表达式
^
返回值类型
参数列表表达式
^
返回值类型参数列表
表达式
^
返回值类型 参数列表表达式
上面可以看到, Block 语法中是有省略写法的,对于省略的情况主要遵循 2 点规则:
- 如果省略返回值,那么 return 什么类型就返回什么类型,多个 return 则需要相同类型。
- 如果省略参数列表,即参数类型为
void
可以省略,代表不使用参数。
日常比较多的一种写法是 同时省略,返回值是void,参数也是 void ,可以这么写:
^{ printf("hello"); }
值得注意的是 Block 同时省略返回值和参数时,返回类型也只取决于 return 的类型 ,例如这么写:
NSInteger num = ^{ return 8;}();
NSLog(@"num %@",@(num))
//输出: num 8
掌握了上面的东西,在手写 Block 的能力上估计能有所进步了,虽然大家肯定还是补全用的多 : )
Block 使用
在使用上,是可以直接把 Block 当作一个变量来使用的:
- 自动变量 (局部变量)
- 函数参数
- 静态变量 (静态局部变量)
- 静态全局变量
- 全局变量
创建 Block 作为变量使用的语法:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}
举个例子🌰 :
//将 Block 赋值为 Block 类型变量
int (^blk)(int) = ^ (int count){
return count + 1;
};
int (^blk1)(int) = blk;
int (^blk12);
blk12 = blk1;
typedef 声明 Block 类型变量
Block 在作为函数参数和返回值时,写起来比较冗长:
//Block 作为函数参数
void func(int (^blk_t)(int))
{
//do something
}
//Block 作为函数返回值
int (^func())(int)
{
return ^(int count){ return count + 1;};
}
我们能通过 typedef 来简化 Block 的使用:
typedef int (^blk_t)(int)
//Block 作为函数参数
void func(blk_t blk)
{
//do something
}
//Block 作为函数返回值
blk_t func()
{
return ^(int count){ return count + 1;};
}
Block 的指针类型变量
Block 类型变量可以使用指向 Block 类型变量的指针来访问:
typedef int (^blk_t)(int)
blk_t blk = ^(int count){ return count+1 };
blk_t *blkptr = &blk;
(*blkptr)(10);
Block 截获变量
通过实例看一下什么是 Block 截获变量:
int val = 10;
const char *fmt = "val = %d \n";
void (^block)(void) = ^{
printf(fmt,val);
};
val = 2;
fmt = " the values were changed.val = %d \n";
block();
结果的输出:
val = 10
说明代码中的 val / fmt 在执行 Block 语法时的 瞬间值被截获(被保存),然后在执行时会使用截获的变量。
Block 的实现
截获变量 作为 Block 的重要特性,下面的篇幅,也将回答 3 个截获变量问题:
- 为什么可以截获变量?
- 为什么 Block 内给截获变量赋值,编译器会报错?
- Block 如何实现修改截获的变量
在文件 main.m 的代码:
#include <stdio.h>
int main(int argc, const char * argv[]) {
void(^block)(void) = ^()
{
printf("hello");
};
block();
return 0;
}
通过命令:
clang -rewrite-objc main.m
在转换后的文件 main.cpp
中,找到 main 方法关联的实现:
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("hello");
}
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(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
对于上面代码里的 名称规则
,以 __main_block_impl_0
为例介绍一下:
Block 结构
根据上面代码,整理出来 Block 的结构和对应的关系:
生成一个类型为 __main_block_impl_0
结构体变量 block
,构成也比较简单,由 2 部分组成:
- 类型为
__block_impl
的结构体impl
- 类型为
__main_block_desc_0
的结构体Desc
如图:
这里最重要的就是 impl
了,它是一个 __block_impl 结构体:
Desc
是一个 __main_block_desc_0 结构体:
而 Block 通过构造函数,将 __main_block_func_0
和 __main_block_desc_0_DATA
作为参数传入,来生成结构体实例:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;//flags = 0
impl.FuncPtr = fp;//__main_block_func_0
Desc = desc;//__main_block_desc_0_DATA
}
Block 执行调用
对于 Block 的执行调用为下面的代码:
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
这里需要注意,就是__main_block_impl_0
类型的 block 被强转换为了__block_impl
。
这在C语言是可行的,因为__block_impl
位于__main_block_impl_0
的最顶部,就相当于__block_impl
的变量直接排列在__main_block_impl_0
的顶部。
上面代码去掉多余的转换部分就变成:
(*block->impl.FuncPtr)(block)
其实就是简单使用 函数指针 调用函数 __main_block_func_0
。
再看一下对应函数 __main_block_func_0
的实现定义:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello");
}
函数实现就是 Block 语句内的实现,且 block 本身作为 参数 __cself 进行了传递。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
Block 为什么能截获自动变量值
下面开始来分析 Block 的重点 -- 截获自动变量值。
因为需要基于实际分析,仍然会把所有代码都贴出来...但建议大家不要直接全看,看多了确实头晕.. 等需要对照具体的实现代码去理解,再来看。 重点关注在 Block 截获自动变量值与未截获的 差异。
将这一段代码 :
#include <stdio.h>
int main(int argc, const char * argv[]) {
int val = 10;
const char *fmt = "val = %d \n";
void (^block)(void) = ^{
printf(fmt,val);
};
val = 2;
fmt = " the values were changed.val = %d \n";
block();
return 0;
}
进行转换后,得到:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt,val);
}
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 val = 10;
const char *fmt = "val = %d \n";
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
val = 2;
fmt = " the values were changed.val = %d \n";
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
经过对比,我们可以发现,__main_block_impl_0
的实现已经发生了变化,代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;// 多出来的部分,Block 内要使用的变量 fmt
int val;// 多出来的部分,Block 内要使用的变量 val
//构造函数..
};
上面的代码里,多出来了 Block 内要用到的 2 个变量:
然后通过 __main_block_impl_0
初始化的构造函数,对 fmt
/ val
进行截取和存储:
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
Block 使用截取的自动变量值
因为截取自动变量后的 Block 执行调用
没有变化,仍然是通过函数指针调用函数,并传递 __main_block_impl_0
结构体实例作为参数,我们就不再做分析了。
重点放到新的函数实现上:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt,val);
}
通过代码发现,函数执行时使用的自动变量值,是通过访问 __cself
上捕获的自动变量,再赋值给函数内部的局部变量,用于在执行时使用。
Block 捕获和使用自动变量的流程
总结一下 Block 捕获和使用自动变量的流程:
Block 为什么不能直接对截获的自动变量赋值?
当我们尝试对捕获的自动变量进行 赋值操作,一般都会提示编译错误:
int a = 0 ;
int (^blk)(void) = ^{
a = 9;//❌Variable is not assignable (missing __block type specifier)
return a+1;
};
这是为什么呢?
在截获自动变量值的 __main_block_func_0
实现里,能看到系统自动加上的注释: bound by copy。Block 对它引用的局部变量做了只读拷贝,也就是说block引用的是局部变量的副本。
自动变量 val 虽然被捕获进来了,但是 Block仅仅捕获了val的值,并没有捕获val的内存地址。在 __main_block_func_0
中修改自动变量的值,依旧不能改写 Block 外面的自动变量值。
基于上面原因,Block 内的修改无法影响外部变量的值。
所以在编译层面检测出被截获的自动变量的赋值操作时,就会报编译错误。
另外会提示错误的还有这种情况:
const char text[] = "hello";
void (^blk)(void) = ^{
//错误提示:Cannot refer to declaration with an array type inside block
NSLog(@"num %c ",text[2]);
};
Block 不能直接对 C 语言数组的截获,要使用指针:
const char *text = "hello";
void (^blk)(void) = ^{
NSLog(@"num %c ",text[2]);
};
Block 如何做到修改自动变量值
想要在 Block 中做到修改变量,在函数内可以使用:
- 静态全局变量
- 全局变量
因为 Block 的匿名函数部分的实现还是变换成 C 语言函数,在其中访问 静态全局变量/全局变量 并没有任何改变,可以直接使用。
除此之外,可以用两种方式在 Block 修改变量,一种是 静态变量,一种是利用 __block 说明符 修饰。
静态变量在 Block 中的实现
在 Block 中想修改变量值,除了 静态全局变量/全局变量
之外,使用静态变量也能办到修改变量值。
#include <stdio.h>
int main(int argc, const char * argv[]) {
static int val = 10;//原来为:int val = 10;
const char *fmt = "val = %d \n";
void (^block)(void) = ^{
val = 3; // 对变量进行修改
printf(fmt,val);
};
val = 2;
fmt = " the values were changed.val = %d \n";
block();
return 0;
}
只是这里捕获的变量稍微有些不同,由于源码和上面最开始的差不多,就不再全部贴上来了,省略了部分代码。
struct __main_block_impl_0 {
...
int *val;//原来为 int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int *_val, int flags=0) : fmt(_fmt), val(_val) {
...
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
...
int *val = __cself->val; // bound by copy,原来为 int val = __cself->val;
(*val) = 3;//修改变量
printf(fmt,(*val));// 原来为 printf(fmt,val);
}
int main(int argc, const char * argv[]) {
static int val = 10;
...
//原来为直接传递 val ,使用 static 声明后用 &val 传递
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val, fmt));
...
return 0;
}
之前我们说到 Block 不能直接修改捕获的自动变量原因,就是因为只是捕获了值,并没有保存指针,所以无法修改外部的变量。
采用静态变量之后,Block 在捕获的时候传递了指针地址。修改的时候也是使用指针变量。
所以使用静态变量可以做到在 Block 内修改变量的值。
__block 说明符的实现
除了静态变量,使用 __block 修饰也可以在 Block 内修改变量的值:
int main(int argc, const char * argv[]) {
__block int val = 10;
const char *fmt = "val = %d \n";
void (^block)(void) = ^{
val = 3;
printf(fmt,val);
};
val = 2;
fmt = " the values were changed.val = %d \n";
block();
return 0;
}
经过转换,代码如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), val(_val->__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_val_0 *val = __cself->val; // bound by ref
const char *fmt = __cself->fmt; // bound by copy
(val->__forwarding->val) = 3;
printf(fmt,(val->__forwarding->val));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
const char *fmt = "val = %d \n";
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
(val.__forwarding->val) = 2;
fmt = " the values were changed.val = %d \n";
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
__Block 变量使用的结构体
看到一下子这么多代码,有点头晕,实际上最核心的就在于 __Block_byref_val_0
结构体的增加与使用。
整理代码,我们发现被 __block
修饰的 val
, 类型从 int
变成了 __Block_byref_val_0
结构体:
生成 __Block_byref_val_0
结构体变量 val
:
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,// isa
(__Block_byref_val_0 *)&val, //forwarding,指向自身的指针
0, //flags
sizeof(__Block_byref_val_0),//size
10//val,相当于原来的自动变量
};
Block 进行捕获,则传递 val 指针作为参数:
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
__Block 变量的访问和修改
Block 内对 val 做访问和修改:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
const char *fmt = __cself->fmt; // bound by copy
(val->__forwarding->val) = 3;
printf(fmt,(val->__forwarding->val));
}
通过 __cself->val
取出来变量,并且注释变为了bound by ref
,说明是引用传递。
(val->__forwarding->val)
很容易看出是通过访问 val 结构体指针来做修改的:
不过确实存在疑问:
__block 变量的实现,为什么要通过
(val->__forwarding->val)
来访问和修改 ?
直接 val->val
应该也是可以的,通过 __forwarding
要多访问一步。
这里就涉及到 Block 的存储域问题了。
Block 存储域
一般 Block 根据存储位置分为3种:_NSConcreteStackBlock
,_NSConcreteMallocBlock
,_NSConcreteGlobalBlock
。
- _NSConcreteGlobalBlock 没有捕获变量,或者只用到全局变量/全局静态/静态变量的 Block,生命周期从创建到应用程序结束。与全局变量一样,GlobalBlock 类对象设置在程序的数据区域(.data区)
- _NSConcreteStackBlock 有捕获外部变量,但没有被强引用的 Block 。生命周期由系统控制。StackBlock 类对象设置在栈上。
- _NSConcreteMallocBlock 有被强引用或者 copy 修饰的属性的 Block 会被复制一份到堆中成为 MallocBlock,生命周期由程序员控制。MallocBlock 类对象设置在由 malloc 函数分配到内存块(即堆)中。
NSConcreteStackBlock 和 NSConcreteMallocBlock 的关系
NSConcreteMallocBlock 是由 NSConcreteStackBlock 复制而来。
栈 Block 通过什么复制到堆 Block
复制 Block 使用 objc_retainBlock
,而 objc_retainBlock
实际上就是 Block_copy
函数。
一般来说,编译器会自动判断。
为什么要有 NSConcreteMallocBlock ?
NSConcreteMallocBlock 的使用,是为解决这么一种情况:
设置在栈上的 Block,即 StackBlock,当 StackBlock 所属的变量作用域结束,该 Block 和栈上的 __block 变量也会随之被废弃。
StackBlock 作为栈对象的好处是内存分配快,有确切的生命周期,因为函数执行完就会自动销毁。但也因此无法在函数之外再使用它。
Blocks 于是提供了将 Block
/__block 变量
从栈上复制到堆上来解决这个问题,即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在和被使用。
将栈 Block 复制的到堆上面创建堆 Block 对象,就可以通过引用计数来管理它。所以 NSConcreteMallocBlock 超出变量作用域还能存在。
而在 ARC 下,大多数情况编译器会恰当的判断,自动生成将 Block 从栈上复制到堆上的代码。
概念说起来可能不具体,下面通过 MRC / ARC 的两种情况来看一下 NSConcreteStackBlock 废弃的情况 。
MRC 下 NSConcreteStackBlock 废弃的情况
要理解为什么需要 MallocBlock ,就要理解 NSConcreteStackBlock 和栈上的 __block 变量被废弃的情况。
因为上面说到了 ARC 下会做对 Block 一些自动的处理,现在来做一个对比,如果在 MRC 下不处理 Block,和 ARC 下处理 Block 的结果。
对下面代码分别在 ARC/MRC 下来进行测试:
typedef int (^blk_t)(int);
blk_t func(int rate) {
blk_t blk = ^(int count){
return rate * count;
};
return blk;
}
int main(int argc, const char * argv[]) {
blk_t blk = func(3);
int total = blk(6);
printf("total = %d",total);
return 0;
}
先在 ARC 下运行 ,下面的图里可以看到在 func 函数返回的为 NSConcreteMallocBlock :
运行完成的结果如图:
可以看到在 ARC 下,main 函数里的 blk 为 NSConcreteMallocBlock ,total 的结果为18。正常。
然后使用 -fno-objc-arc
对文件做处理,变成 MRC 运行:
从上面的图里看到在 func 函数返回的为 NSConcreteStackBlock 。
运行完成的结果如图:
我们发现 blk 的类型 isa 变成了 0x0
,确实属于 NSConcreteStackBlock 随着变量作用域结束而被废弃的情况。 total 得到也是一个奇怪的值。
解决方式就是把 StackBlock 变成 MallockBlock ,所以 ARC 下,编译器自动判断后帮我们做了一次转换。
ARC 下 NSConcreteStackBlock 废弃的情况
大多数情况下 ARC 下会自动复制到堆上,但也存在例外,这时需要 手动copy 将 Block 复制到堆上。
需要手动 copy 的场景
有一种情况需要我们手动 copy ,是编译器不能判断的状况:
- 向方法或函数传递 Block
下面是一个 Block 作为 参数传递 的例子:
id getBlockArray()
{
NSString *val = @"test";
NSArray *blockList =[[NSArray alloc] initWithObjects:
^{NSLog(@"block0 %@",val);},
^{NSLog(@"block1 %@",val);},
^{NSLog(@"block2 %@",val);},
nil];
return blockList;
}
int main(int argc, const char * argv[]) {
NSArray *array = getBlockArray();
blk_t blk = (blk_t)[array objectAtIndex:1];
blk();
return 0;
}
将代码在 ARC 下运行,发现数组里除了第 0 个是 NSMallocBlock ,后面的都是 NSStackBlock :
接下来在 main 取数组里的 Block ,会发现崩溃了,原因是: StackBlock 随着函数作用域的结束而跟着被废弃了。
怎么解决 StackBlock 废弃呢?
那就是把 Block 复制到堆上,变成 MallocBlock 之后,即使超出了函数的作用域也可以被访问和使用。
对应到这段代码,就是代码手动调用 copy :
NSArray *blockList =[[NSArray alloc] initWithObjects:
^{NSLog(@"block0 %@",val);},
[^{NSLog(@"block1 %@",val);} copy],
[^{NSLog(@"block2 %@",val);} copy],
nil];
这也验证了需要 NSConcreteMallocBlock 在超出函数作用域时使用的原因,否则当 StackBlock 废弃,再去访问就会发生异常。
三种 Block 的 copy 效果
另外这里直接总结一下三种 Block 调用 copy 的效果:
Block 类型 | 存储域 | 复制的效果 |
---|---|---|
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
__block 变量从栈上复制到堆上
当 Block 被复制到堆上,对应使用的 __Block 变量
都是会一起复制到堆上的。
需要注意的是多个 Block 持有同 1 个 Block 变量
的情况是如何复制的。
例如:
栈上的 block1和 block2 同时都用到了栈上的 __block 变量
。
block1 复制到堆上,__block 变量
也跟着被复制到了堆上,并且堆上的__block 变量
被堆上的 block1 持有。如图:
接着 block2 也复制到堆上,__block 变量
此时不会再复制 1 份,被复制到堆上的 block2 持有 __block 变量
,增加 __block 变量
的引用计数。__block 变量
同时被堆上的 block1 和 block2 一起所持有。
__block 结构体使用 forwarding 成员变量的原因
之前提到了,使用 __block 变量 的修改变量值没直接用 var->var
的方式,而是使用 var->__forwarding->var
绕了一下来做访问。
例如下面的情况:
{
__block int val = 0;
void (^blk) (void) = [^{++val;} copy];
++val;
blk()
NSLog(@"%d",val);
}
代码里 2 处的 ++val
最终同样都用 val.__forwarding->val
来做修改。
重点在于,实际上这 2 处是不同的 __block 变量
类型:
-
Block 语法内的,因为被 copy 了,所以 Block 执行时是
堆上的 __block 变量
。 -
Block 语法外的,仍旧还是
栈上的 __block 变量
。
当栈上的 Block 被复制到堆上,此时可以 同时 访问栈上 __block 变量
和 堆上__block变量
。
如果直接使用 val->val
来访问,那么此时 2 个不同的 __block 变量
,指向的是栈上和堆上不同的地址,显然会产生 错误的结果。
关键的解决方式,就是使用 __forwarding
来做指向:
如图,有了 __forwarding
来做指向的话,那么在 栈上的__block 变量
,最终也会指向 堆上的__block 变量
,来保证访问到的都是同一个变量,即此时访问栈 __block 变量
实际访问的是堆 __block 变量
。
截获对象的使用和废弃
被 MallocBlock 所截获的对象(这里指 OC 对象),就算没有使用 __block 修饰,由于被堆上的 Block 持有,也会延长对象的生命周期,使该对象能够超出其变量作用域而存在。等到堆上的 Block 释放才会调用 dispose 函数去释放对象。
例如下面的情况:
blk_t blk ;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld",[array count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
输出:
array count = 1
array count = 2
array count = 3
array
按照道理应该是随着变量作用域结束就会被废弃了,但是调用 blk([NSObject new])
发现 array
仍旧存在,且运行正常。
原因就是 Block 在堆上 ,对象因为被堆上的 Block 持有了,所以还能超出其变量作用域存在。
copy / dispose 函数
截获对象的一个关键点,就是用 copy 函数和 dispose 函数来进行引用和释放:
- 栈上的 Block 复制到堆时,会调用 copy 函数
- 堆上的 Block 被废弃时,会调用 dispose 函数
截获对象和截获 __block 变量时,copy / dispose 函数的内部实现是不一样的。
截获 __block 变量:
// __block 变量使用 BLOCK_FIELD_IS_BYREF (值为8)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
截获对象:
// 对象使用 BLOCK_FIELD_IS_OBJECT (值为3)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
栈上 Block 复制到堆
在下列情况,栈上的 Block 会自动复制到堆:
- 调用Block 的 copy 实例方法
- Block 作为函数返回值返回时
- 将 Block 赋值给附有 __strong 修饰符 id 类型的类或Block 类型成员变量时
- 在方法名含有有 usingBlock 的 cocoa 框架方法或 GCD 的 API 传递 Block 时 (会在方法内部调用 copy)
总结
这次将 Block 的相关知识梳理了一下,主要针对 Block 的实现,__block 变量
的实现, Block/__block 变量
怎么从栈复制到堆,以及回答为什么需要堆 Block,为什么 __block 变量
的结构体要通过 __forwarding
访问等问题。
在整理和画图的同时,相比只读一遍文字和代码,记忆更加深刻了。建议大家也可以在读书时画图整理关系。
如有错误,欢迎指正 !