Block
中文翻译成块,我的理解是对函数或者方法实现的一个封装,当然也有捕获外部变量的能力,只有当块被调用时,内部的方法实现才会执行。那块的本质是什么呢?对象?那又怎么去证明呢。灵光一闪,想到了下面一张图
根据上面这张图,我们是不是可以打印Block的SuperClass,看看到底是什么呢?
void(^block)(void) = ^(void){
NSLog(@"Block");
};
block();
NSLog(@"%@",[block superclass]);
NSLog(@"%@",[[block superclass] superclass]);
NSLog(@"%@",[[[block superclass] superclass] superclass]);
2020-04-08 11:13:24.560052+0800 ObjcStudy[27895:510541] __NSGlobalBlock
2020-04-08 11:13:24.560199+0800 ObjcStudy[27895:510541] NSBlock
2020-04-08 11:13:24.560309+0800 ObjcStudy[27895:510541] NSObject
完美,这也验证了Block就是对象的猜想,在上面的输出中,我们发现了__NSGlobalBlock,这是不是Block的类型呢?有了这个想法,可以继续的往下探索Block的类型,同样,通过NSLog来输出Block到底有什么
void(^block1)(void) = ^(void){};
block1();
int a = 100;
void(^block2)(void) = ^(void){
NSLog(@"%d",a);
};
block2();
NSLog(@"%@",block1);
NSLog(@"%@",block2);
NSLog(@"%@",^(void){NSLog(@"%d",a);});
2020-04-08 11:14:55.371336+0800 ObjcStudy[27895:510541] <__NSGlobalBlock__: 0x106384060>
2020-04-08 11:14:55.371479+0800 ObjcStudy[27895:510541] <__NSMallocBlock__: 0x6000000c01e0>
2020-04-08 11:14:55.371623+0800 ObjcStudy[27895:510541] <__NSStackBlock__: 0x7ffee987c130>
发现有以下三种类型的Block
- NSGlobalBlock (程序的数据区域(.data区))
- NSMallocBlock(堆)
- NSStackBlock(栈)
问题变的有点意思了,在手敲代码的时候,发现Block内部可以捕获a的值,但是无法更改a的值
提示说需要__block来修饰a变量,为什么呢?想看看Block内部构造,到底做了些什么骚操作,我们知道Objective-C的代码底层都转换成了C++的代码,那我们就通过clang将.m .c文件转换成.cpp文件,看看到底搞了哪些事情,先建个main.c文件,写一个简单block如下:
#include <stdio.h>
int main(){
void(^block)(void) = ^(void){};
block();
return 0;
}
通过clang转换成.cpp文件
clang -rewrite-objc main.c -o main.cpp
转换完成后发现,短短几行的代码,变成了几百行。。。浏览了下发现有个函数与我们写的main函数是一一对应的
int main(){
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;
}
把强转类型都删了,看起来会舒服点
// 对应的代码是 void(^block)(void) = ^(void){};
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
// 对应的代码是 block();
(block)->FuncPtr)(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;
}
};
// __main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
// __block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
通过上面的代码我们可以得出block的工作流程
- __main_block_impl_0()生成结构体函数
- 参数为__main_block_func_0 也就是block的{}中的内容
- __main_block_func_0赋值给了__block_impl类型的FuncPtr
- (block)->FuncPtr 也就是__main_block_func_0 也就是运行了block的{}里面的内容
大概知道了流程后,测试下捕获外部变量,看看Block又是怎么操作的
int a = 10;
void(^block)(void) = ^(void){
NSLog(@"%d",a);
};
a++;
block();
2020-04-08 15:29:06.322437+0800 ObjcStudy[28744:583891] 10
输出结果居然是10!!!而不是11,同样clang下
int main(){
int a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a++;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 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
printf("%d", a);
}
可以看出__main_block_impl_0对变量a是值得获取,捕获的是a的瞬时值,所以在Block之后对a做的任何操作,都不会使Block内部保存的a有所改变,上面Xcode有报错,提示需要在变量前添加__block,那加上又会出现什么效果呢?
#include <stdio.h>
int main(){
__block int a = 10;
void(^block)(void) = ^(void){
printf("%d", a);
};
a++;
block();
return 0;
}
继续clang,发现__block 对变量a搞事情了,出现了新的结构体__Block_byref_a_0,并将a转换成了此结构体类型的变量!!!
int main(){
__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));
(a.__forwarding->a)++;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
// __Block_byref_a_0
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
// __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;
}
};
// __main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("%d", (a->__forwarding->a));
}
可以看出被__block 修饰的a,生成的结构体(__Block_byref_a_0)保存了对a的地址引用(__Block_byref_a_0 *)&a,在捕获或者使用的时候,都是通过引用地址来访问变量a的值,当然block内部也可以给外部变量a赋值等一系列操作。这里的__forwarding很有意思,不管是block内部还是外部对变量a的访问或者操作,都是通过__forwarding访问的同一个__block对象。
在日常的Block使用过程中,经常会遇到循环引用,比如下面的代码:
typedef void(^Block)(void);
@interface ViewController ()
@property (nonatomic, copy) Block block;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
NSLog(@"%@",self);
};
self.block();
}
self持有block,block又持有self,这样就导致了循环引用,使得当前ViewController不会被释放掉,引起内存泄露。假如把block或者self置为nil,是不是就能打破这种循环引用呢?block内部是无法直接对self操作了,这就需要用到__block
__block UIViewController *vc = self;
self.block = ^{
NSLog(@"%@",vc);
vc = nil;
};
通过实验,__block确实可以打破这种循环引用,但是必须得确保block被调用,否则循环引用依然存在!!!
日常的开发中,经常会用__weak来避免循环引用,当然block内部对外部变量需要长时间占有的话,还会使用__strong,避免外部变量被提前释放
__weak typeof(self)weakSelf = self;
self.block = ^{
NSLog(@"%@",weakSelf);
};