一、Block的概念
1、是什么Block
Block是带有局部变量的匿名函数。
iOS4引入,是对c语言的扩充功能,先来理解一下局部变量和匿名函数的含义
1.1.局部变量
那带有局部变量又是什么意思? 先理解一下c语言的都有哪些变量
- 自动变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
每个变量的作用域不同。
c
int aa = 2; //全局变量
static int bb = 3; //静态全局变量
int main(int argc, const char * argv[]) { //argc 函数的参数
int a = 1; //auto修饰局部变量
static int b = 1; //静态局部变量
return 0;
}
| 变量 | 作用域 | 存储位置 | 生命周期 | 关键特性 |
|---|---|---|---|---|
| 全局 aa | 整个程序(文件间) | 全局数据区.data | 程序全程 | 外部链接,多文件可访问 |
| 静态全局 bb | 仅当前源文件 | 全局数据区.data | 程序全程 | 内部链接,仅本文件可见 |
| 局部 a | main 函数内 | 栈区 | main 执行期间 | 自动销毁,每次调用重新初始化 |
| 静态局部 b | main 函数内 | 全局数据区.data | 程序全程 | 仅初始化一次,保留值 |
| argc/argv | main 函数内 | 栈区 | main 执行期间 | 函数参数,栈上分配 |
1.2 匿名函数
所谓匿名函数,就是不带有函数名称的函数。
而在c语言中是不允许函数不带有名称的。
先理解一下c语言的函数定义:
c
int func(int count); //声明了名为func的函数,参数为int类型的count,返回值为int类型
调用func
c
int result = func(3);
使用函数指针funcPtr直接调用函数func,这种也是需要函数名才能通过函数指针调用。
c
int (*funPtr)(int) = &fun;
int result = (*funcPtr)(3);
二、Block的语法
2.1.Block的定义
官方文档:Blocks Programming Topics
c
//官方实例
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};

其中myBlock是声明的快对象,返回类型为int,myBlock 快对象有一个int类型的参数,myBlock的主体部分为 return num * multiplier;
上面表达式的特点:
- 没有函数名 (匿名函数)
- 带有
^,插入记号,便于查找。
Block的表达式: ^返回值类型(参数列表){表达式}
c
^int (int count){ return count +1 };
Block可以省略如返回值,参数列表,如果用不到的话
-
省略返回值类型
c//省略返回值类型:^(参数列表){表达式}; ^(int count){return count+1}; -
省参数列表
c^int (void){ return 1}; ^int {return 1}; -
省略返回值类型、参数列表:
c^{ }; //最简洁的block
2.2.Block类型的变量
通过Block语法将Block赋值为Block类型的变量
c
int (^blk) (int) = ^(int count) { return count+1};
此时的表达式和c语言的指针函数表达式对比
c
int (*funPtr)(int) = &fun; //指针函数
block的变量声明就是把声明指针函数类型的* 变为^
-
在函数参数中使用 Block类型的变量
c//作为函数参数的block变量 void func(int (^blk)(int)){ } -
作为函数的返回值
cint (^blk1(void))(int){ return ^(int count){ return count+1 ; }; } //作为函数返回值时,需要注意: // 1. 调用 blk1 函数,并且是无参函数,它返回一个Block int (^myBlock)(int) = blk1(); // 2. 调用(执行)这个返回的Block,并传入整数参数 5 int result = myBlock(5); NSLog(@"结果是: %d", result); // 输出:结果是: 6上述当block作为参数或返回值时,可以通过
typedef声明类型,来简化,如下ctypedef int ^(Blk_t)(int); //声明一个Blk_t类型的block Blk_t blck = ^{ } //作为函数参数和返回值就可以简化为 void func(Blk_t blck){ } //作为函数返回值时可以简化为 Blk_t func1(){ }
2.3. 截获自动变量值
c
void test1(void){
//默认为auto修饰局部变量
int a = 1;
void (^bck1)(void)=^(){
NSLog(@"访问量局部变量a:%d",a);
};
a = 2;
NSLog(@"访问量局部变量a1:%d",a);
bck1();
}
//访问量局部变量a1:2
//访问量局部变量a:1
block访问局部变量 auto修饰,此时block截获了变量a的当前的瞬间值,底层为值传递,所以block内部不能直接赋值修改,block外侧修改了局部变量,block内部变量值不会修改。
如果在block内部尝试修改局部变量会报错

报错信息:变量不可赋值(缺少__block类型说明符)
Variable is not assignable (missing __block type specifier)
2.4. __block 说明符
__block说明符更准确的描述方式为"__block存储域类说明符" __block storage-class-specifier,c语言的存储域类说明符右以下几种:
- typedef
- extern
- static
- auto
- register
__block类似于static、auto、register等说明符。用于指定将变量值设置到哪个存储域中,例如auto作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。
如果非要在block内部修改局部变量,就需要再局部变量前通过__block修饰
c
void test1(void){
//默认为auto修饰局部变量
__block int a = 1;
void (^bck1)(void)=^(){
NSLog(@"访问量局部变量修改前a:%d",a);
a = 3;
NSLog(@"访问量局部变量修改后a:%d",a);
};
a = 2;
NSLog(@"访问量局部变量a1:%d",a);
bck1();
}
//访问量局部变量a1:2
//访问量局部变量修改前a:2
//访问量局部变量修改后a:3
2.5.截获的自动变量
block截获自动变量会报错,那截获OC对象呢,比如NSMutableArray还会报错吗?
c
//截获可变数组,
void test11(void){
//默认为auto修饰局部变量
NSMutableArray *array = [NSMutableArray array];
void (^bck1)(void)=^(){
[array addObject:[[NSObject alloc]init]];
NSLog(@"访问量局部变量array.count:%lu",(unsigned long)array.count);
};
NSLog(@"访问量局部变量array1.count:%lu",(unsigned long)array.count);
bck1();
}
//访问量局部变量array1.count:0
//访问量局部变量array.count:1
此时block截获的变量值是NSMutableArray类对象,及NSMutableArray类对象的结构体实例指针,因此,对变量值进行addObject操作,是没有影响的,如果在block内部,要对array进行赋值时是不行的。依然需要用__block修饰

使用c语言的数组时必须小心使用其指针,下面这个例子,看似并没有像截获的自动变量text赋值,但还是编译不通过,报错信息:Cannot refer to declaration with an array type inside block(不能引用块内数组类型的声明)

需要把text声明为指针来解决
c
void test111(void){
//使用c语言的数组时必须小心使用其指针,
const char *text = "hello";
void (^bck1)(void)=^(){
//截获自动变量的方法并没有实现对c语言数组的截获,此时需要用指针来解决该问题
NSLog(@"截获的局部变量:%c",text[2]);
};
bck1();
}
三、Block的底层实现
3.1、Block的实质
一开始讲了Blocks是带有局部变量 的匿名函数,但是Block实质究竟是什么,类型、 变量、还是其他什么?
先说结论:Block其实是一个对象。因为它的结构体里有isa指针。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m

执行后,会生成一个main.cpp文件

-
转换前的oc代码
c++//为了简化生成的c++代码,把原来oc代码的main方法传参(int argc, const char * argv[])省略。以及 int main(void) { void (^donyBck)(void)=^(){ //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看 NSLog(@"The block prints logs internally"); }; //donyBck的调用 donyBck(); return 0; } -
转换后的C++源码
c++//包含block实际函数指针的结构体 struct __block_impl { void *isa; //有isa int Flags; int Reserved; //今后升级所需区域大小 void *FuncPtr; //函数指针 }; // static __NSConstantStringImpl __NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"The block prints logs internally",32}; //block结构体 struct __main_block_impl_0 { struct __block_impl impl; //Block的实际指针, struct __main_block_desc_0* Desc; //block构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上 impl.Flags = flags; //block标志位 impl.FuncPtr = fp; //Block执行的函数指针 Desc = desc; //Block描述信息,Block大小等元信息 } }; //Block内部函数调用 /* void (^donyBck)(void)=^(){ //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看 NSLog(@"The block prints logs internally"); }; */ static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0); } //Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量 static struct __main_block_desc_0 { size_t reserved; //成员1 :保留字段 size_t Block_size; //成员2:Block实例的内存大小 } __main_block_desc_0_DATA = { //初始化变量 0, //给Reserved赋值 sizeof(struct __main_block_impl_0) //给Block------size赋值 }; //main函数 int main(void) { //1.构造block对象,并将其强制转换为无参无返回值的函数指针。 void (*donyBck)(void)=((void (*)())&__main_block_impl_0( (void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr &__main_block_desc_0_DATA //block的描述信息(版本,大小等) )); //2.调用block的核心逻辑(donyBck的调用) // ((void (*))(donyBck):将函数指针转回Block结构体指针 //->FuncPtr :取出Block的执行函数指针 //最后调用该函数,并传入Block自身作为参数,(block的隐式self) ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck); return 0; }
3.2 、源码分析
3.2.1 Block结构体-__main_block_impl_0
先看看 __main_block_impl_0 结构体
c++
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl; //Block的实际指针,
struct __main_block_desc_0* Desc;
//block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
impl.Flags = flags; //block标志位
impl.FuncPtr = fp; //Block执行的函数指针
Desc = desc; //Block描述信息,Block大小等元信息
}
};
从源码里可以看出,__main_block_impl_0结构体,包含三部分
-
成员变量 impl,是结构体
__block_impl -
成员变量 Desc指针,是结构体
__main_block_desc_0 -
__main_block_impl_0 构造函数
分别分析一下这三部分
3.2.1.1 struct __block_impl impl
c++
//包含block实际函数指针的结构体
struct __block_impl {
void *isa; //有isa
int Flags; //标志位
int Reserved; //今后升级所需区域大小
void *FuncPtr; //函数指针
};
- isa指针,保存Block结构体的实例指针
- Flags 标志位
- Reserved 后续版本升级所需区域大小
- FuncPtr 函数指针,指向了Block的主体部分,也及时对应oc代码里的
{ NSLog(@"The block prints logs internally");};。
3.1.1里的__main_block_impl_0 里的impl 是__block_impl结构体,而__block_impl 包含了Block实际函数指针 FuncPtr。
总结:impl 主要就是包含了Block的函数指针 FuncPtr
3.2.1.2 struct __main_block_desc_0* Desc
c++
//Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
static struct __main_block_desc_0 {
size_t reserved; //成员1 :保留字段
size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
0, //给Reserved赋值
sizeof(struct __main_block_impl_0) //给Block------size赋值
};
- reserved 版本升级后所需的区域大小
- Block_size Block的大小。
总结:__main_block_desc_0是block的描述信息,也就是附加信息
3.2.1.3 __main_block_impl_0构造函数
c++
//block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
impl.Flags = flags; //block标志位
impl.FuncPtr = fp; //Block执行的函数指针
Desc = desc; //Block描述信息,Block大小等元信息
}
传三个参数:
- void *fp :主要赋值给impl的FuncPtr ,Block执行的函数指针
- struct __main_block_desc_0 *desc, Block的描述信息
- int flags=0 ,默认值为0,标志位。
总结:构造函数主要用来初始化__block_impl的成员变量,以及把描述信息赋值给Desc
3.2.1.4 在main方法里的过程
关于:__main_block_impl_0 基本概念就了解完了,那在main方法里,__main_block_impl_0 又是怎么赋值的呢
c++
//原函数
void (^donyBck)(void)=^(){
//这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
NSLog(@"The block prints logs internally");
};
//转换c++后
//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
&__main_block_desc_0_DATA //block的描述信息(版本,大小等)
));
可以看出通过 __main_block_impl_0构造函数,生成 __main_block_impl_0结构体(Block结构体)的实例指针,并赋值给donyBck
然后对 __main_block_impl_0构造函数传了两个参数
-
__main_block_func_0c++//Block内部函数调用 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //对应的就是原oc NSLog(@"The block prints logs internally"); NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0); }可以看出 对应的是oc block的主体部分也就是
{NSLog(@"The block prints logs internally"); };这里的参数
__cself就是指向Block的值的指针变量,相当于oc的self;🎯这里画个重点:从这里可以看出,block内部,把block的
^{}执行函数{}在Block定义完后,当成一个参数类型为__main_block_func_0传入到__main_block_impl_0->__block_impl->FuncPtr里了。 -
__main_block_desc_0_DATA__main_block_desc_0_DATA 是包含了Block的描述信息,
c++static struct __main_block_desc_0{ size_t reserved; //成员1 :保留字段 size_t Block_size; //成员2:Block实例的内存大小 } __main_block_desc_0_DATA = { //初始化变量 0, //给Reserved赋值 sizeof(struct __main_block_impl_0) //给Block------size赋值 };
3.2.2 总结
至此Block的内部原理就浮出水面了。
Block内部是由 __main_block_impl_0结构体组成的,内部isa指针,指向所属类的结构体的实例指针,_NSConcreteStackBlock相当于Block的结构体实例,对象impl.isa = &_NSConcreteStackBlock ,将Block结构体的指针赋值给impl的成员变量isa ,相当于Block结构体成员变量保存了Block结构体的指针,和OC的对象处理方式是一致的。
所以Block的实质就是对象,和NSObject一样,都是对象。

3.3 Block截获局部变量和特殊区域变量
3.3.1 Block截获局部变量的实质
在2.3里我们知道了Block可以截获局部变量,那背后Block是怎么截获的,为什么不能在block内部直接修改截获的局部变量呢?
先说结论:Block截获的局部变量是值传递的方式传入Block结构体中,并保存为Block的成员变量。因此当外部局部变量值发生修改后,Block内部对应的成员变量的值并没有发生改变。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
-
oc代码
cint main(void) { int a = 2; void (^donyBck)(void)=^(){ NSLog(@"The block Capture local variables:%d",a); }; a = 4; //donyBck的调用 donyBck(); NSLog(@"The block prints local variables:%d",a); return 0; } //The block Capture local variables:2 //The block prints local variables:4 -
转c++源代码
c++//block结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; //截获的局部变量,在block内部变成了成员变量 __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; } }; //block执行函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_1,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(void) { int a = 2; //block定义 void (*donyBck)(void)=((void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); a = 4; //block 执行方法 ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)( (__block_impl *)donyBck ); //打印 NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_2,a); return 0; }- 从源码可以看出,结构体
__main_block_impl_0多了一个成员变量a,这个变量就是Block捕获的局部变量 - 方法
__main_block_func_0看出int a = __cself->a在block内部访问这个变量a时,通过值传递的方式,而不是指针传递,这也就说明了a是block的内部变量,外部修改a,Block内部捕获的a是不会发生变化的。
- 从源码可以看出,结构体
3.3.2 使用__block说明符更改局部变量
那使用__block来修饰局部变量后,就能让Block内部来修改这个变量,那背后__block 又做了什么呢?
先说结论:通过 __block修饰后,使这个局部变量在block内部通过指针传递,所以修饰后的局部变量,在block内部可以修改了。
为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
-
oc代码
cint main(void) { __block int a = 2; void (^donyBck)(void)=^(){ a = 3; NSLog(@"The block Capture local variables:%d",a); }; a = 4; //donyBck的调用 donyBck(); NSLog(@"The block prints local variables:%d",a); return 0; } -
c++代码
c++//__block修饰的 struct __Block_byref_a_0 { void *__isa; //isa指针 __Block_byref_a_0 *__forwarding; //传入变量的地址 int __flags; //标志位 int __size; //结构体大小 int a; //存放变量a的实际的值,相当与原局部变量的成员变量。 }; //block内部 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_a_0 *a; // by ref //加入__Block修饰后,这里的a是__Block_byref_a_0类型了 __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; //栈block impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //block执行函数 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) = 3; NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a)); } //新增了 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 }; //main函数 int main(void) { //__block修饰的局部变量a __attribute__((__blocks__(byref))) __Block_byref_a_0 a = { (void*)0, (__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 2}; void (*donyBck)(void)=((void (*)())&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); //修改局部变量a的值 (a.__forwarding->a) = 4; //block调用 ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck); //block打印 NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_4,(a.__forwarding->a)); return 0; }可以看出局部变量a加上
__block后,c++代码里新增了__Block_byref_a_0、__main_block_copy_0、__main_block_dispose_0- 从结构体
__main_block_impl_0可以看出原来oc的 被__block修饰后的局部变量a,在结构体__main_block_impl_0内部变成了__Block_byref_a_0 *a,也就是说Block内部的结构体__main_block_impl_0实例持有指向__block变量的__Block_byref_a_0结构体实例指针。 __main_block_copy_0和__main_block_dispose_0留在后面在说。
- 从结构体
这里有点绕,我们用白话文理解一下
可以把整个过程想象成寄送一个易碎品(变量a):
| 角色 | 对应代码 | 比喻说明 |
|---|---|---|
| 易碎品本身 | 局部变量 int a = 2 |
比如一个玻璃杯。 |
| 加固包装盒 | __Block_byref_a_0结构体 |
一个专门用来固定玻璃杯的防震包装盒。 |
| 快递单/指针 | __Block_byref_a_0 *a(Block结构体里的成员) |
一张写着包装盒地址的快递单。 |
| 整个Block | __main_block_impl_0结构体实例 |
快递仓库。 |
🎯 整个过程是这样的:
- 打包: 当你用
__block修饰变量a时,编译器会自动创建一个"包装盒"(__Block_byref_a_0结构体),然后把你的变量a(玻璃杯)放进这个盒子里。 - 填写快递单: Block(快递仓库)想要操作这个玻璃杯,但它并不直接把玻璃杯拿进来(那样就成了它的私有物品,无法和外界共享了)。Instead,它只拿了一张写着"包装盒地址"的快递单 (即指针
__Block_byref_a_0 *a)。 - 共享修改: 当Block内部或外部代码需要修改
a的值时,它们会凭着这张"快递单"找到同一个"包装盒",然后打开盒子修改里面的玻璃杯。因为大家访问的是同一个盒子里的东西,所以任何一方的修改,另一方都能立刻看到
白话文总结:被__block修饰后的变量a,在Block内并没有直接把变量装在自己口袋里,而是记下了变量所在包装盒的地址。通过这个共享的地址,Block和外部代码就能共同修改同一个变量了。
在看看__Block_byref_a_0 结构体
c++
//__block修饰的
struct __Block_byref_a_0 {
void *__isa; //isa指针
__Block_byref_a_0 *__forwarding; //传入变量的地址
int __flags; //标志位
int __size; //结构体大小
int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
};
在看一下在main()中原oc代码为
c
__block int a = 2;
转为c++为
c
//__block修饰的局部变量a
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
2};
从赋值里可以看出在main()被__block修饰后的变量a,底层赋值给__Block_byref_a_0结构体时,传入的值如下
-
isa传入空
-
__forwarding传入了局部变量a的本身地址 -
__flags :分配了0
-
sizeof:结构体的大小
-
a值赋值为2.

总结一下:到此知道了__forwarding就是局部变量a的本身地址,可以通过 __forwarding指针来访问局部变量。同时也能对其修改了。
c
//block执行函数
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) = 3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
}
可以看出 (a->__forwarding->a) = 3;,是通过指针取值的方式来改变局部变量的值,
🎯总结:
从而解释了通过__block修饰后的变量,在Block内部通过指针传递的方式修改局部变量
另外__block的 __Block_byref_a_0 结构体并不在Block的 __main_block_impl_0结构体中,这样做是为了多个Block同时使用__block变量。
-
继续看一下OC代码
cint main(void) { //__block被多个Block使用 __block int aa = 2; void (^donyBck)(void)=^(){ aa = 3; NSLog(@"The block Capture local variables:%d",aa); }; void (^donyBck1)(void)=^(){ aa = 4; NSLog(@"The block Capture local variables1:%d",aa); }; } -
转换为c++
c++//__block修饰后 __Block_byref_aa_0 aa = { 0, &aa, 0, sizeof(__Block_byref_aa_0), 2}; //donyBck donyBck)=&__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_aa_0 *)&aa, 570425344)); //donyBck1 donyBck1)=&__main_block_impl_1( __main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_aa_0 *)&aa, 570425344));
可以看出donyBck和donyBck1 都是用了__Block_byref_aa_0结构体的实例aa的指针。反过来一个block中使用多个 __block也是可以的。
3.3.3 更改特殊区域变量值
除了通过__block修饰局部变量外,其他变量如静态局部变量、静态全局变量、全局变量能否在block内部进行修改?
为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
-
oc代码
cint global_a = 2; static int static_global_a = 3; int main(void) { static int static_a = 4; void (^donyBck)(void)=^(){ global_a = 1; static_global_a = 2; static_a = 3; NSLog(@"The block Capture global_a:%d ,static_global_a:%d,static_a:%d",global_a,static_global_a,static_a); }; //donyBck的调用 donyBck(); return 0; } -
C++代码
c++//全局变量 int global_a = 2; //静态全局变量 static int static_global_a = 3; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_a; //静态局部变量 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_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 *static_a = __cself->static_a; // bound by copy global_a = 1; static_global_a = 2; (*static_a) = 3; NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_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(void) { static int static_a = 4; void (*donyBck)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_a)); ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck); return 0; }从
__main_block_impl_0可以看出,静态局部变量static_a以指针形式添加添加为成员变量,而静态全局变量static_global_a和全局变量global_a并没有添加到__main_block_impl_0内部。c++//全局变量 int global_a = 2; //静态全局变量 static int static_global_a = 3; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_a; //静态局部变量 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };再从
__main_block_func_0,可以看出全局变量global_a和全局静态变量static_global_a是在block内部访问直接访问的,而静态局部变量static_a是通过指针传递的方式进行访问和赋值的。c++static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *static_a = __cself->static_a; // bound by copy global_a = 1; //全局变量 static_global_a = 2; //全局静态变量 (*static_a) = 3; //局部静态变量 NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a)); }
3.4 Block的存储域
在3.2 中的Block实质源码分析里,看出impl.isa = &_NSConcreteStackBlock; //栈block,可以看出该Block存储在栈区,那Block还可以存在哪个区呢?
先说结论:Block分别可以存储在_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock。
| 类 | 设置对象的存储域 |
|---|---|
| _NSConcreteGlobalBlock | 数据区域(.data区) |
| _NSConcreteStackBlock | 栈 |
| _NSConcreteMallocBlock | 堆 |

3.4.1 _NSConcreteGlobalBlock
为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟
进入main.m的文件夹下,执行:clang -rewrite-objc main.m
-
oc代码
c//全局block void (^donyBck)(void) =^(){ NSLog(@"global block"); }; int main(void) { donyBck(); return 0; } -
c++源码
c++struct __donyBck_block_impl_0 { struct __block_impl impl; struct __donyBck_block_desc_0* Desc; __donyBck_block_impl_0(void *fp, struct __donyBck_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteGlobalBlock; //全局block impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
通过源码可以看出 impl.isa = &_NSConcreteGlobalBlock,说明该Block为 _NSConcreteGlobalBlock类型。
这里需要注意使用全局block时,因为本身已经在全局区域,所以不会捕获自动变量(局部变量),存储在数据区域
3.4.2 _NSConcreteStackBlock
除了全局block外,其他基本都存储在栈上,也就是StackBlock。
在NSConcreteStackBlock类的block,存储在栈区,如果所属的变量作用域结束,该Block就会被废弃。由于 __block变量也配置在栈上,同样的所属变量作用域结束后,该 __block变量同样也被废弃。

3.4.3 _NSConcreteMallocBlock
既然在栈上的Block在变量作用域结束后就立即被废弃,那如果不想废弃怎么办?
Block提供了 【复制copy】操作,可以将Block对象和 __block变量从栈区复制到堆区上,当Block从栈区复制到堆区后,即时变量作用域结束时,堆区上的Block和 __block还可以继续使用。

此时的在堆区的Block就是_NSConcreteMallocBlock 对象,Block结构体成员变量isa赋值为 imp.isa = &_NSConcreteMallocBlock;
而此时被__block修饰的变量用结构体成员变量 __forwarding可以实现无论__block变量配置在堆上还是在栈上,都能够正确访问__block变量
3.5 Block的自动拷贝和手动拷贝
3.5.1 Block的自动拷贝
在ARC下,大多数情况下,编译器会自动进行判断,自动生成将Block从栈上复制到堆上的代码
-
将Block作为函数返回值返回时,会自动拷贝
-
向方法或函数的参数中传递Block时:(以下两种情况内部底层实现了copy操作,其他都需要手动拷贝)
- Cocoa框架的方法且方法名中含有usingBlock等 如
NSArray类的enumerateObjectsUsingBlock方法 - GCD的API时如
dispatch_async函数
c//在MRC下,initWithObjects后面的Block,编译器不会主动给Block添加copy操作,所以Block还存在栈上,所以会报错。 //在ARC下,编译器主动添加了copy操作,此时的block被复制到堆上了。 @implementation SDPerson - (id) getBlockArray{ int val = 10; void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);}; void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);}; NSLog(@"blk01:%@",blk0); NSLog(@"blk02:%@",blk1); /* blk01:<__NSStackBlock__: 0x7ff7bfeff0d8> blk02:<__NSStackBlock__: 0x7ff7bfeff0a8> */ //array 的initWithObjects 纯容器存储AIP,框架内部不会自动copy return [[NSArray alloc]initWithObjects:blk0,blk1, nil]; } @end
报错原因是:在执行完
getBlockArray后栈上的Block被废弃,MRC 无任何自动优化,initWithObjects:仅存栈 Block 指针 → 方法返回栈帧销毁 → 执行 Block 访问野内存 → 崩溃。此时我们需要手动复制下即可。修改一下getBlockArray,即可
c- (id) getBlockArray{ int val = 10; void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);}; void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);}; blk0 = [blk0 copy]; blk1 = [blk1 copy]; NSLog(@"blk01:%@",blk0); NSLog(@"blk02:%@",blk1); //blk01:<__NSMallocBlock__: 0x600000c00540> //blk02:<__NSMallocBlock__: 0x600000c00570> return [[NSArray alloc]initWithObjects:blk0,blk1, nil]; } - Cocoa框架的方法且方法名中含有usingBlock等 如
3.5.2 Block的手动拷贝
所有需要让 Block「脱离原栈帧存活」的场景,都必须手动调用[block copy]
关于Block不同类的拷贝效果总结如下
| block类 | 副本源的存储域 | 复制效果 |
|---|---|---|
| _NSConcreteStackBlock | 栈区 | 从栈拷贝到堆区 |
| _NSConcreteGlobalBlock | 程序的数据区域 | 不做改变 |
| _NSConcreteMallocBlock | 堆区 | 引用计数增加 |
不管Block配置在何处,用copy复制不会引起任何问题,在不确定时,调用copy方法即可
3.6 __block变量存储域
在使用 __block变量的Block从栈复制到堆上,__block变量也受到了如下影响
| __block变量的配置存储区域 | Block从栈上复制到堆上时的影响 |
|---|---|
| 堆区 | 从栈复制到堆区,并被Block所持有 |
| 栈区 | 被Block所持有 |
和OC引用计数方式内存管理完全相同。
__block修饰的变量被Block所持有,如果Block废弃,持有的__block变量也跟着废弃
3.7 截获对象
在Block语法中使用局部变量array来添加元素。
理论上在变量作用域的同时,变量array被废弃,因此赋值给变量array的NSMutableArray类对象必定释放并废弃,但上述代码在main方法里的内{}外仍然可以执行,并打印日志。 这意味着array在Block的执行部分超出其变量作用域而存在。
先说结论:Block从栈复制到堆,归结为__Block_copy函数被调用,使__strong修饰的自动变量对象和__block修饰的变量,被堆上的Block所持有,所以可以超出其变量作用域而存在。
那我们转为c++代码再探个究竟
-
OC
ctypedef int (^Blk_t)(id obj); Blk_t donyBck; int main(void) { { id array = [NSMutableArray array]; donyBck = [^(id obj){ [array addObject:obj]; NSLog(@"access local variables array.count:%lu",[array count]); } copy]; } donyBck([[NSObject alloc]init]); donyBck([[NSObject alloc]init]); donyBck([[NSObject alloc]init]); return 0; } /* access local variables array.count:1 access local variables array.count:2 access local variables array.count:3 */ -
C++
c++typedef int (*Blk_t)(id obj); Blk_t donyBck; //Block用的结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; id array; //这里array被Block强引用 __strong __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //block执行方法 static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id array = __cself->array; // bound by copy //添加方法 ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj); //打印日志 NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_b8c0c1_mi_8,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count"))); } //copy static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);} //废弃 static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);} //block描述 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}; //main方法 int main(void) { { id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array")); donyBck = (Blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy")); } //Block执行 ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))); ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))); ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))); return 0; }通过源码可以看出 array被Block截获,并成为
__strong修饰成员变量,这里虽然没有显示__strong,默认就是强引用
c
//Block用的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array; //这里array被Block强引用 __strong
};
那Block捕获的array是在什么时候进行初始化和废弃的呢?
我们可以从__main_block_desc_0可以看出结构体新增了copy和dispose,以及对应__main_block_copy_0和__main_block_dispose_0,这两个结构体在3.3.2里__block时也遇到了。 不过__block修饰的变量和捕获的对象有一点点区别:
| 对象 | BLOCK_FIELD_IS_OBJECT |
|---|---|
__block变量 |
BLOCK_FIELD_IS_BYREF |
仅仅主要用来区分是对象还是__block变量
c++
//block描述
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};
//copy
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
//_Block_object_assign对使Block内部对array持有
_Block_object_assign(
(void*)&dst->array,
(void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/
);
}
//废弃
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose(
(void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/
);
}
-
从
__main_block_copy_0可以看出,内部通过_Block_object_assign方法对array持有,_Block_object_assign相当于retain,将对象赋值在对象类型结构体成员变量中。 -
__main_block_dispose_0使用_Block_object_dispose函数,相当于release,释放赋值在Block用结构体成员变量的array中的对象。 -
因此
_Block_object_assign和__main_block_dispose_0指针赋值在__main_block_desc_0的copy和dispose中。但是在源代码里没有看到这些函数以及指针被调用,那他们的调用时机在什么时候?

那什么时候栈上的Block会复制到堆上呢?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有
__strong修饰符id类型或Block类型成员变量时 - 在方法名中含有
usingBlock的Cocoa框架方法或GCD的API中传递Block时。
array默认为__strong修饰,那如果改为__weak呢?
c
typedef void (^Blk_t)(id obj);
Blk_t donyBck;
int main(void) {
{
id array = [NSMutableArray array];
id __weak array2 = array;
donyBck = [^(id obj){
[array2 addObject:obj];
NSLog(@"access local variables array.count:%lu",[array2 count]);
} copy];
}
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
donyBck([[NSObject alloc]init]);
return 0;
}
/**
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
*/
这是因为array在变量作用域结束后同时被释放、废弃。nil被赋值给__weak修饰的array2。
那如果__block和__weak同时修饰呢?
c
__block id __weak array2 = array;
结果一样:
c
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
即使被附加了__block说明符,__strong修饰符的变量array也会在变量作用域结束的同时被释放掉,nil被赋值给附有__weak的变量array2中。
另外被__unsafe_unretained修饰符的变量只不过与指针相同,所以不管在Block中使用还是附加到__block变量中,也不会想 __strong或__weak那样进行处理,使用__unsafe_unretained修饰符需要注意不能通过悬垂指针访问已被废弃的对象。
__autoreleasing修饰符也不能和__block同时使用
3.8 Block循环引用
我们知道Block内部使用__strong修饰符的对象类型的自动变量,那当Block从栈复制到堆的时候,该对象就会被Block所持有
那么如果这个对象还同时持有Block的话,就容易发生循环引用。正所谓你中有我,我中有你

-
示例1:
c// 文件SDPerson.m typedef void (^blk_t)(void); @interface SDPerson() { blk_t blk_; } @end @implementation SDPerson - (instancetype)init { self = [super init]; if (self) { blk_ = ^{ NSLog(@"self = %@",self); }; } return self; } - (void)dealloc{ NSLog(@"dealloc"); }c//文件main.m #import "SDPerson.h" int main(void) { SDPerson *person = [[SDPerson alloc]init]; NSLog(@"%@",person); return 0; }最终执行结果 <SDPerson: 0x60000020d340> SDPerson的dealloc没有执行,发生了Block的循环引用
具体分析:
SDPerson内部
blk_t持有了self,而self也同时持有作为成员变量的blk_t另外编译器也会有提示

如果Block内部不使用self,还会造成循环引用吗?
-
示例2:
ctypedef void (^blk_t)(void); @interface SDPerson() { blk_t blk_; id obj_; } @end @implementation SDPerson - (instancetype)init { self = [super init]; if (self) { blk_ = ^{ NSLog(@"obj_ = %@",obj_); }; } return self; }答案:会
分析一下:表面上看
obj_没有使用self,但是它是self的成员变量,因此Block想持有obj_,就必须引用self,所以同样造成循环引用。那如果这个属性使用weak修饰符呢
-
实例3 :
ctypedef void (^blk_t)(void); @interface SDPerson() @property (nonatomic, weak) NSArray *array; @end - (instancetype)init { self = [super init]; if (self) { blk_ = ^{ NSLog(@"obj_ = %@",_array); }; } return self; }答案:还是会循环引用,因为循环引用是self和block之间的事情,这个被block持有的成员变量是strong或weak没有关系,即使是基本类型assign也是一样的。
那如何解决这样的循环引用呢?
3.8.1 __weak修饰符
为了避免循环引用,可以通过__weak修饰符,来打破互相持有
c
- (instancetype)init
{
self = [super init];
if (self) {
//使用__weak修饰符,使block内部为弱引用关系
id __weak tmp = self;
// id __unsafe_unretained tmp = self;
blk_ = ^{
NSLog(@"self = %@",tmp);
};
}
return self;
}
常见用 __weak typeof(self) weakSelf = self; 来进行弱引用self
c
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
[weakSelf doSomething];
};
上述存存在一个问题,当block执行过程中,weakSelf可能被释放,导致后续操作无效
这里需要再block内部进行强化弱引用,使用__strong在局部作用域内临时强引用弱引用对象,确保在执行期间对象存活。__strong在Block内部栈上创建局部强指针,不会造成循环引用。
c
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
[strongSelf doAnotherThing];
}
};
除了__weak typeof(self) weakSelf = self; 这里还可以参考第三方开源库ReactiveObjC这样的简洁写法 @weakify(self); 作用是一样的,都是对self进行弱引用。
c
@weakify(self);
self.myBlock = ^{
@strongify(self);
if (self) {
[self doSomething];
[self doAnotherThing];
}
};
除了__weak修饰符外,还可以使用__unsafe_unretained,如id __unsafe_unretained tmp = self; 效果是一样的,都是使对象为弱引用,那两者有什么区别?更推荐使用__weak
两者的区别在于当所指向的对象被释放时,如何处理指针
__weak(安全)当对象释放后,所有指向它的__weak变量会被运行时自动设置为nil,意味着后续在访问这个指针,就像nil发送消息一样,在OC中是安全的,不会导致程序崩溃 。__unsafe_unretained(不安全)当对象释放后,__unsafe_unretained指针不会自动置空,仍然保存着对象被释放前的那个内存地址,也就是变成了"悬垂指针"或"野指针",如果此时访问了这个指针,就会发生BAD_ACCESS

3.8.2 __block修饰符
除了__weak外,还可以使用 __block解决block循环引用问题
c
typedef void (^blk_t)(void);
@interface SDPerson()
{
blk_t blk_;
}
@end
@implementation SDPerson
- (instancetype)init
{
self = [super init];
if (self) {
__block id tmp = self;
blk_ = ^{
NSLog(@"obj_ = %@",tmp);
tmp = nil;
};
}
return self;
}
//执行block
- (void)execBlock{
blk_();
}
c
int main(void) {
SDPerson *person = [[SDPerson alloc]init];
[person execBlock];
NSLog(@"%@",person);
return 0;
}
//执行结果
//obj_ = <SDPerson: 0x600000c08690>
//<SDPerson: 0x600000c08690>
//dealloc
如果Block不执行execBlock,依然会存在循环引用
此时-SDPerson持有Block,Block持有__block变量,__block持有SDPerson类对象。三者互相持有,导致引用循环

如何解决?
就是执行Block execBlock方法,Block内部,会把tmp置为nil 。并执行block execBlock方法,因此__block持有类对象的强引用就失效了,
c
blk_ = ^{
NSLog(@"obj_ = %@",tmp);
tmp = nil;
};
//并执行block execBlock方法

所以__block需要执行Block来解决循环引用,基于此特点,可以通过__block控制对象的持有时间。
这里需要区分,这里利用__block解决循环引用,不是因为__block本身直接解决的,而是利用了__block的**「可写特性」+ 手动执行 tmp = nil**,并执行block的execBlock方法,使block内部tmp=nil生效,从而主动打破了循环链。
在实际开发过程中,需要具体根据实际情况,来使用__weak还是__block.
四、常见面试题
4.1 说一下什么是Block?
Block是带有局部变量的匿名函数,本质是一个对象,内部有isa指针,内部是由结构体**__main_block_impl_0->__block_impl**组成,Block的执行函数通过在__block_impl->FuncPtr函数指针,找到封装的函数,并将block地址作为参数传给这个函数进行执行。Block捕获的变量,存入__main_block_impl_0结构体内,并通过block地址拿到捕获变量的值。
c++
//包含block实际函数指针的结构体
struct __block_impl {
void *isa; //有isa
int Flags;
int Reserved; //今后升级所需区域大小
void *FuncPtr; //函数指针
};
//block结构体
struct __main_block_impl_0 {
struct __block_impl impl; //Block的实际指针,
struct __main_block_desc_0* Desc;
};
4.2 Block 有几种类型?分别是什么?
有三种类型,分别为 _NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock
定义在全局的block,为全局block,存储在数据区的全局区里,因为本身就是全局,所以不会访问局部变量,因此不需要捕获局部变量。
一般用到的是栈block,但是栈上的block是临时的,在它的作用域结束后就被销毁,为了延长生命周期,在arc下系统会默认会copy到堆上,来延迟生命周期,这样可以在定义它的作用域外部使用。mrc下,需要手动进行copy
4.3 Block 自动截取变量
Block外部的变量,可以被block捕获到内部进行使用,这里需要注意的是变量类型
-
全局变量/静态全局变量 ,block不需要捕获,因为全局变量和静态全局变量数据存储在全局数据区,Block内部直接使用
-
局部静态变量 捕获变量地址,所以外部变量修改后,通过地址访问到变量的值,也会跟着修改。
-
静态变量 捕获变量的值,是通过值传递 的方式捕获到block内部,并且捕获的是变量的当前瞬时值,所以外部修改了变量,block内部的变量值不会发生改变,如果需要修改,需要通过
__block来修饰,然后通过指针引用传递的方式在内部使用。4.3.1 为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗
不行,和局部变量的生命周期有关系,因为局部变量在出大括号后就会被释放掉,这事我们在大括号外部调用这个Block,此时局部变量已经被释放了,block内部通过变量的指针访问变量,就会抛出异常。而静态局部变量的生命周期是和整个程序的生命周期一样,也就是在整个程序运行过程中不会释放,所以可以通过指针地址访问。
4.3.2既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?
这是因为静态局部变量作用域只在大括号内,出了括号,它虽然存在,但外面已经访问不了,这时通过block执行函数只能通过捕获的方式。
4.3.3 静态局部变量一直都不会被释放,会导致内存泄漏吗?
不会,静态局部变量是语言设计的特性,行为可预测,是一种特殊的局部变量,具有局部作用域,存储在数据区
.data(初始化)或.bss(未初始化),程序在运行期间只被分配一次内存(且占内存有限),生命周期有编译器自动管理,启动时初始化,结束时销毁,不会导致运行时的内存泄漏首先明确内存泄漏的概念:程序在运行过程中,不断分配内存而没有适当的释放,导致内存逐渐减少的情况
4.4Block 处理循环引用
如果Block内部捕获了外部的strong(强引用)类型的引用对象,那么这个对象有强引用block,就会形成循环引用,会导致内存泄漏,因为参与循环引用的对象和block无法正常释放,长期下去会导致性能问题。
这时,就需要通过__weak关键字,进行对强引用对象进行弱引用,来打破你中有我,我中有你。
4.4.1 __weak typeof(self) weakSelf = self; 和@weakify(self);有什么区别
都是用来弱引用self,避免循环引用,__weak typeof(self) weakSelf = self;
@weakify(self) 是宏定义,预编译阶段展开就是 __weak typeof(self) weakSelf = self;,设计初衷就是为了更简洁更优雅。
4.5 Block 的内存管理
首先围绕Block的三个核心点
- Block自身的【三大存储区域】 [请查看4.2
Block有几种类型?分别是什么?](#请查看4.2 Block 有几种类型?分别是什么? "#4.2BlockType") - ARC / MRC下Block【栈->堆的拷贝规则】请查看4.7.为什么需要使用copy关键字
- Block捕获变量时的持有 / 引用规则 请查看4.3block自动截取变量
- 以及
__block相关的使用 请查看4.6__block
4.6__block 的解释以及在 ARC 和 MRC 下有什么不同
默认情况下,Block捕获的外部自动变量(局部变量)是值捕获,在Block内部是无法修改的。
__block是修饰符,主要用于解决上述问题,block捕获的局部变量,在block内部,可以进行修改。
原理:被__block修饰后的局部变量,编译器会把这个变量包装成一个结构体对象,底层其实一个结构体__Block_byref_a_0,内部有一个__forwarding指针,和当前变量的值等成员,无论Block和__block变量本身被复制到栈上还是堆上,都可以通过这个指针访问和修改值,这样就使从值传递变成了引用传递
在ARC和MRC下的不同
- 在ARC下
被__block修饰的对象是强引用,需要注意循环引用,常用的解决方案为使用__weak弱引用。
- 在MRC下
对__block修饰对象,不会对对象进行retain,避免循环引用
4.7为什么需要使用copy关键字
Block默认创建在栈上,为了延迟生命周期,需要copy到堆上。
在arc之前,手动管理内存,为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上,通过copy关键字操作,如下
c
@property (copy, nonatomic) void (^block)(void);
在ARC之后,虽然编译器会自动进行copy操作,把block复制到堆上,为了和MRC下保持一致,避免在不同内存管理环境下切换的混淆,在ARC之后,还是推荐用copy关键字
4.7.1 那用strong可以修饰block吗?
在ARC下可以,在MRC下不可以。
4.7Dispatch_block_t这个有没有用过?解释一下?
dispatch_block_t是GCD中的一个类型定义,代表无参数,也没有返回值的代码里。
基本定义 typedef void (^dispatch_block_t)(void);
常见的使用场景:将任务放入队列后立即返回,不阻塞当前线程
c
dispatch_async(queue,^{
NSLog(@"在后台执行任务");
})
五、参考文献
- 参考文档:苹果官方文档Block
- iOS 开发:『Blocks』详尽总结 (二)底层原理
- block相关知识以及常见面试题
- 《Objective-C 高级编程》干货三部曲(二):Blocks篇
- 书籍:『Objective-C 高级编程 iOS 与OS X 多线程和内存管理』