Block 底层原理(一)
block
的本质是对象
、函数
、结构体
。
一、block 定义
- block:带有自动变量的匿名函数。
-
匿名函数:没有函数名的函数,一对
{}
包裹的内容是匿名函数的作用域。 -
自动变量:栈上声明一个变量既不是静态变量,又不是全局变量,是不可以在栈内声明的匿名函数中使用的。但是在block中却可以,虽然使用block不用声明类,但是block提供了类似OC的类一样,可以通过成员变量来保护作用域外变量值的方法,那些再block的一对
{}
里面使用到,但却是在{}
作用域外声明的变量,就是block接货的自动变量。
block表达式语法
^ 返回值类型(参数列表){表达式}
1、无参数,无返回值
// 1、无参数,无返回值
void(^block)(void) = ^ {
NSLog(@"block");
};
block();
2、有参数,无返回值
// 2、有参数,无返回值
void(^block)(int, NSString*) = ^(int a, NSString *name){
NSLog(@"%@ -- %d", name, a);
};
block(10, @"block");
3、有参数,有返回值
// 3、有参数,有返回值
int(^block)(int, int) = ^(int a, int b){
NSLog(@"block -- %d", a+b);
return a+b;
};
block(10, 20);
4、无参数,有返回值
// 4、无参数,有返回值
int(^block)(void) = ^{
NSLog(@"block -- 40");
return 40;
};
block();
5、在开发中我们还可以用typedef
定义block
typedef int(^block)(int, NSString*);
可以这样使用:
block Myblock = ^(int a, NSString *name){
NSLog(@"%@ -- %d", name, a);
return 40;
};
NSLog(@"%d", Myblock(10, @"block"));
也可以定义成属性:
@property (nonatomic, copy) block Myblock;
二、block的分类
-
block主要分为三类:
image.png
① 全局block:_NSConcreteGlobalBlock
;存储在全局内存中,相当于单例。
② 栈block:_NSConcreteStackBlock
;存储在栈内存中,超出其作用域则马上被销毁。
③ 堆block:_NSConcreteMallocBlock
;存储在堆内存中,是一个带引用计数的对象,需要自行管理其内存。
这三种block各自的存储区域如下图:
简而言之,存储在栈中的block
就是栈块,存储在堆区的就是堆块,既不在栈区也不在堆区的就是全局块 -
当我们遇到一个
block
,怎么去判定这个block
的存储位置呢?
(1)block
不访问外部变量(包括栈和堆中的变量)
此时block
既不在栈中,也不在堆中,在代码段中。ARC
和MRC
下都是如此。
此时为全局block
:_NSConcreteGlobalBlock
void(^block)(void) = ^{
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSGlobalBlock__: 0x100004030>
(2)block
访问外部变量
MRC
环境下:访问外部变量的block
默认是存储在栈
中的。
ARC
环境下:访问外部变量的block
默认是存储在堆
中的(实际是放在栈
区,然后ARC
情况下又自动拷贝到堆
区),自动释放。
- MRC 环境:
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSStackBlock__: 0x7ffeefbff3e8>
- ARC 环境下
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSMallocBlock__: 0x1040508b0>
- 在ARC环境下我们怎么获取
栈block
呢?
我们可以这样做:
int a = 10;
void(^ __weak block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSStackBlock__: 0x7ffeefbff3e8>
此时我们通过__weak
不进行强持有,block
就还是栈区的block。
- ARC环境下,访问外部变量的
block
为什么要自动从栈区拷贝到堆区呢?
因为:栈上的block
,如果其所属的变量作用域结束,该block
就会被废弃,如同一般的自动变量。当然,block
中的__block
变量也同时会被废弃。
image.png
为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把block
复制到堆中,延长其生命周期。开启ARC
时,大多数情况下编译器会恰当的进行判断是否有必要将block
从栈复制到堆,如果有,自动生成将block
从栈复制到堆的代码。block
的复制操作执行的是Copy实例方法。block
只要调用了Copy方法,栈块就会变成堆块。
image.png
eg:
typedef int(^myblock)(int);
myblock func(int a) {
return ^(int b) {
return a * b;
};
}
上面的代码中,函数返回的block
是配置在栈上的,所以返回返回函数调用方法时,block
变量作用域就被释放了,block
也会被释放。但是,在ARC
环境下是有效的,这种情况编译器会自动完成复制。
在非ARC
情况下则需要开发者调用Copy方法手动复制。
将block从栈区复制到堆区非常想好CPU,所以当block设置在栈上也能使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。
block
的复制操作,执行的是Copy实例方法。不同类型的block
使用的Copy方法的效果如下:
block的类型 | 副本源的配置存储区域 | 复制效果 |
---|---|---|
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteStackBlock | 栈区 | 从栈区复制到堆区 |
_NSConcreteMallocBlock | 堆区 | 引用计数增加 |
根据表格我们知道,block
在堆区Copy会造成引用计数增加,这与其它OC对象是一样的。虽然block
在栈中也是以对象的身份存在,但是栈区没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。
三、block 底层分析
int a = 10;
void(^ block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
使用 clang
将OC代码转换成C++文件,查看block
的方法。
- 在命令行输入下面的指令(XXX.m就是要编译的文件,需在当前文件夹下面执行)
clang -rewrite-objc XXX.m
-
执行网上面的指令之后,当前文件夹中会多一个
XXX.cpp
的文件。此时在命令行输入open XXX.cpp
或者 直接打开文件 -
打开XXX.cpp文件,在文件底部我们可以看到
main
函数被编译之后的样式:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_c75271_mi_1, block);
}
return 0;
}
我们从main
函数中提取一下block
void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
/*简化一下,去除强制转换*/
void(* block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); ///构造函数
可以看到构造函数名为__main_block_impl_0
下面我们再寻找一下__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;
}
};
- 可以看到
__main_block_impl_0
是一个结构体
。同时我们也可以说block
是一个__main_block_impl_0
类型的对象,这也是为什么block
能够%@
打印的原因
1、block自动捕获外部变量
- block自动捕获的外部变量,在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(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 值拷贝
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_ab7cb4_mi_0, a);
}
可以看到,编译器会自动生成一个同名的变量。
__main_block_func_0
中a
是值拷贝。
因此,在block
内存会生成一个,内容一样的同名变量,此时如果在函数体内进行a++
的操作,则编译器就不清楚该去修改哪个变量。所以block
自动捕获的变量,在函数体内部是不允许修改的。
- 那么我们要修改外部变量要怎么办呢?
1、__block
修饰外部变量。
2、将变量定义成全局变量
3、将变量用参数的形式,传入block
里面。
第2种和第3种方式,想必大家都非常的熟悉,在这里就不再赘述。下面我们来看一下第1种方式,底层究竟做了些什么。
__block 原理
- 现在我们对
a
进行__block
编译,之后我们就可以在block
内部对a
进行修改。
__block int a = 10;
void(^ block)(void) = ^{
a++;
NSLog(@"%d", a);
};
block();
下面我们再通过clang
来观察一下,底层代码有了什么变化。
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++
(a->__forwarding->a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_b21337_mi_0, (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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(* block)(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 *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 首先我们看到,在
main
函数里面,此时a
变成了成了一个__Block_byref_a_0
类型的对象。 - 同时在
__main_block_func_0
函数中,由之前的值拷贝(bound by copy
) 变成了现在的指针拷贝(bound by ref
)
*在main
函数中传入的a
是一个对象,同时在__main_block_func_0
函数内部,对a
进行指针拷贝;则此时创建的对象a
和传入的对象a
指向同一片内存空间。
总结:
__block
修饰外界变量的时候:
1、外界变量
会生成__Block_byref_a_0
结构体
2、结构体用来保存原始变量的指针
和值
(可以在上面编译后的代码中找到)
3、将变量生成的结构体对象的指针地址
传递给block
,然后在block
内部就可以对外界变量
进行修改了。
接下来,在给大家看一个东西:
- 在上面的C++代码中,
__main_block_func_0
函数中,大家会注意到执行a++
的是这段代码(a->__forwarding->a)++;
,那么这个__forwarding
又是什么呢?
接下来我们先看一下__Block_byref_a_0
结构体长什么样子:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
可以看到,__forwarding
是一个指向自己本身的指针(自身为结构体)。
那么,在Copy操作之后,既然__block
变量也被Copy到堆区
上了,那么访问该变量是访问栈
上的还是堆
上的呢?这个时候我们就要来看一下,在Copy过程中__forwarding
的变化了:
可以看到,通过
__forwarding
,无论是在block
中,还是block
外访问__block
变量,也不管该变量是在栈
上或是在堆
上,都能顺利的访问同一个__block
变量。注意:这里与上面的结论并不矛盾。大家要主要到
局部变量a
被__block
修饰之后,会变成__Block_byref_a_0
结构体对象。所以无论是在栈区
还是在堆区
,只要__forwarding
指向的地址一样,那么就可以在block
内部修改外界变量。这里大家要仔细观察一下__Block_byref_a_0
结构体
例题:
int a = 10;
int *p = &a;
NSLog(@"开始 a == %d, s == %p", a, &a);
NSLog(@"开始 p == %d, s == %p", *p, p);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
(*p) = 100;
NSLog(@"异步 a == %d, s == %p", a, &a);
NSLog(@"异步 p == %d, s == %p", *p, p);
});
///打印结果
开始 a == 10, s == 0x7ffeebb2a0ac
开始 p == 10, s == 0x7ffeebb2a0ac
异步 a == 10, s == 0x600003a26c68
异步 p == 0, s == 0x7ffeebb2a0ac
image
- 这里有一点要注意
我们上面用的是异步,所以最后的*p
打印的不是100
,而是0
。
这就是因为异步,不用等待,viewDidLoad
直接执行完毕,*p
对应的内存空间被释放。
如果在dispatch_async
后面写上其他的一些函数,输出的*p
可能是任意值:
image.png
当然这也是一个释放时机
的问题,如果有很多业务要处理,可能打印*p
的时候,对应的内存地址还没有释放。
-
a
的最后输出值是10
这里就不多说,block
捕获局部变量的拷贝上面有讲。