前面我们说了weak和strong的一些作用,自然我们接下来就要聊一下block。Block在我们项目中绝对会用到,如果你没用到,请你到地球外面去好吗。
Block的分类
先来看一段代码
void (^block)(void)=^{
NSLog(@"111");
};
block();
NSLog(@"----:%@",block);
输出结果为:
2021-03-03 13:59:00.239359+0800 RuntimeDemo[21536:8193293] 111
2021-03-03 13:59:00.239400+0800 RuntimeDemo[21536:8193293] ----:<__NSGlobalBlock__: 0x100600038>
NSString *str=@"hello world";
void (^firstblock)(void)=^{
NSLog(@"字符串是:%@",str);
};
firstblock();
NSLog(@"第二个测试字符串:%@",firstblock);
输出结果为:
2021-03-03 14:02:56.687806+0800 RuntimeDemo[21541:8194859] 字符串是:hello world
2021-03-03 14:02:56.687835+0800 RuntimeDemo[21541:8194859] 第二个测试字符串:<__NSMallocBlock__: 0x2832d0bd0>
从上内存的分布上block分为了:
NSGlobalBlock:静态全局
NSMallocBlock:堆block
NSStackBlock: 栈block
Block内部实现
我们使用终端输入clang -rewrite-objc main.m,然后再当前目录下就可以生成一个cpp文件。下面来看一下block内部的实现。
直接滑动到最后,找到了实现的testBlock,上代码。
void testBlock(){
void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}
这一下子,看的有点懵。我们一步一步的来解锁。
首先,是__testBlock_block_impl_0,这是个什么?
往上看,是不是发现了下面的这段代码
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__testBlock_block_impl_0是一个结构体,而在里面又包含了两个结构体__block_impl和_testBlock_block_desc_0*。
_testBlock_block_desc_0
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
来看看它的成员变量的意义:
reserved:Block版本升级所需的预留区空间,在这里为0
Block_size:从单词意思上都清楚,是block的大小
__testBlock_block_desc_0_DATA 是__testBlock_block_desc_0的一个实例。
__block_impl
isa:指向一个类对象 我们上面说的block三种类型,就是这个指针所对应的对象类型
Flags:block 的负载信息(引用计数和类型信息),按位存储
FuncPtr:指向block执行的函数,也是就block大括号里面的东西。
既然明白了这两个结构体,我们在回过头来看最开始testBlock的那一堆东西。
void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
上面代码不就是定义block
((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
这段代码不就是找到FuncPtr,然后执行。
一张图搞定,现在在看上面的函数是不是就明白了。就清楚调用的原理了
总结:block内部的实现就是通过两个结构体__block_impl和__testBlock_block_desc_0来实现的。先去定义block,然后通过__block_impl下的FuncPtr,调用执行。
截获变量值
我们下来执行下面一段代码
void testBlock(){
int testnum=10;
void (^lhrBlock)(void)=^{
NSLog(@"当前输入数值为:%d",testnum);
};
testnum=30;
lhrBlock();
}
按照从上到下的顺序执行,testnum=30在执行block之前,为毛线打印输出的是10.来看它内部是怎么实现的
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
int testnum;
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _testnum, int flags=0) : testnum(_testnum) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
int testnum = __cself->testnum; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n__9j735w697q98_wgfxl0mlrfm0000gq_T_main_41c180_mi_0,testnum);
}
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
void testBlock(){
int testnum=10;
void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, testnum));
testnum=30;
((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}
通过上面的代码发现,在void (lhrBlock)(void)=((void ()())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, testnum));和__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _testnum, int flags=0)中,_testnum是一个数值。我们在定义block之前,int testnum是10.这个数值已经传递到block当中了。之后在去改变testnum和block是半毛钱关系都没有。这就解释了为什么最后打印出来是10.
那我想改变testnum,让block里面也跟着改变。肯定要传入一个它的指针就可以解决。那要怎么做呢。
int testnum=10;
void testBlock(){
//static int testnum=10;
// __block int testnum=10;
void (^lhrBlock)(void)=^{
NSLog(@"当前输入数值为:%d",testnum);
};
testnum=30;
lhrBlock();
}
static
firstblock_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
在看num,这传递的是num的指针。所以在num的数值发生变化的时候,block中的num也是变化的。
全局变量
int num = 10;
struct __firstblock_block_impl_0 {
struct __block_impl impl;
struct __firstblock_block_desc_0* Desc;
__firstblock_block_impl_0(void *fp, struct __firstblock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_1875c6_mi_0,num);
num = 30;
}
num并没有作为一个参数出现在block_func_0中,它直接就是全局变量,就可以直接访问
局部auto变量 捕获到block内部 值传递
局部static变量 捕获到block内部 指针传递
全局变量 捕获到block内部 直接访问
我们重点来看下__block
struct __Block_byref_testnum_0 {
void *__isa;
__Block_byref_testnum_0 *__forwarding;
int __flags;
int __size;
int testnum;
};
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__Block_byref_testnum_0 *testnum; // by ref
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_testnum_0 *_testnum, int flags=0) : testnum(_testnum->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
__Block_byref_testnum_0 *testnum = __cself->testnum; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n__9j735w697q98_wgfxl0mlrfm0000gq_T_main_b918cc_mi_0,(testnum->__forwarding->testnum));
}
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->testnum, (void*)src->testnum, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->testnum, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __testBlock_block_impl_0*, struct __testBlock_block_impl_0*);
void (*dispose)(struct __testBlock_block_impl_0*);
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0), __testBlock_block_copy_0, __testBlock_block_dispose_0};
void testBlock(){
__attribute__((__blocks__(byref))) __Block_byref_testnum_0 testnum = {(void*)0,(__Block_byref_testnum_0 *)&testnum, 0, sizeof(__Block_byref_testnum_0), 10};
void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, (__Block_byref_testnum_0 *)&testnum, 570425344));
(testnum.__forwarding->testnum)=30;
((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}
这么一大堆东西,扎心了。还是慢慢看吧。
__testBlock_block_impl_0多出来一个成员变量 __Block_byref_testnum_0 *testnum;,我们看到经过__block修饰的变量类型变成了结构体__Block_byref_testnum_0,__testBlock_block_impl_0多出来一个成员变量__Block_byref_testnum_0 *_testnum;,block捕获的是__Block_byref_testnum_0类型指针。
我们看到__Block_byref_testnum_0是一个结构体,并且有一个isa,因此我们可以知道它其实就是一个对象。同时还有一个__Block_byref_testnum_0 *类型的__forwarding和testnum,testnum我们能猜到就是用来保存变量的值,__forwarding还在懵逼中。
还多出两个方法__testBlock_block_copy_0和__testBlock_block_dispose_0
__testBlock_block_copy_0中调用的是_Block_object_assign,__block变量结构体实例从栈拷贝到堆
__testBlock_block_dispose_0中调用的是_Block_object_dispose,__block变量结构体实例引用计数为0时调用。
这两后面的block内存管理会用到。
__blockTest_block_desc_0
我们可以看到它多了两个回调函数指针copy和dispose,这两个指针会被赋值为__testBlock_block_copy_0和__testBlock_block_dispose_0。
简单的看了block分类,我们在来看block的循环应用问题
Block循环引用
就像上面的图中展示,block引用b之后,引用计数+1了。只有在A(block)释放掉的时候,B的引用计数才会-1;但是A什么时候释放呢?它是怎么造成循环引用的的?
先来看一个简单的例子
self.lhrblock = ^{
self.str=@"jieddddd";
};
self.lhrblock();
同时这个界面还有
-(void)dealloc{
NSLog(@"销毁了");
}
在退出这个界面的时候,并没有调用dealloc这个方法,没有释放,这是为什么呢?当前界面只有一段代码,只能是它出现问题了。
这是因为block造成了循环引用,lhrblock引用了当前的self,按照我们前面说的,在lhrblock释放的时候,self是不是要realse。但是,lhrblock引用了self,self也引用了block。所以,两边就互相等,一直释放不掉。
生活中遇到这种情况,肯定要有一方去打破它吧。
下面是处理循环引用的方式
使用weak
__weak __typeof(self) weakSelf = self;
self.lhrblock = ^{
weakSelf.str=@"jieddddd";
};
self.lhrblock();
打印出
RuntimeDemo[21593:8213411] 销毁了
这说明调用了dealloc方法,block也被释放了。自然这个相互引用的问题也就不存在了
__block FirstViewController *vc=self;
self.lhrblock = ^{
vc.str=@"jieddddd";
vc=nil;
};
self.lhrblock();
当然我们也可以使用__block来修饰, 要注意的是vc=nil;如果这里不设置为nil,它还是不会释放的。原因我们后续看block底层原理的时候就会明白。
下面我们来看第三种解决方法:
typedef void(^LHRfirstblock)(FirstViewController *vc);
self.lhrblock = ^(FirstViewController *vc) {
vc.str=@"jjjj";
};
self.lhrblock(self);
4477+0800 RuntimeDemo[21605:8220341] 销毁了
既然我们要使用str属性,那我们所幸就直接把VC控制前都作为参数。这样最后block也是可以释放的
下面我们再来看一个多个层级的循环引用
MYTableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"123"];
// __weak __typeof(self) weakSelf = self;
cell.lhrblock = ^(NSString * _Nullable str) {
// [weakSelf testwith:str];
self.str=str;
};
return cell;
cell中的block方法,这么写系统不会报循环引用的错误,那么它循环引用了吗?我们发现dealloc方法是没有调用的,说明在这是发生了循环引用。我个人理解的是self强引用了cell,cell-->block,反过来在block中强引用了self,所以造成了循环引用。
Block的内存管理
我们首先来看看Block从栈中复制到堆,的整个过程变化
我们来详细解读描述一下上面的过程:
1.当block在栈上时,__block也同样是存储在栈上面的,__block她是被栈上的block持有的
2.当block被复制到堆上时,block要调用__block变量,但是它在栈上啊,超出栈上的block的作用范围了,怎么办?先把它copy到栈上不就解决了嘛,所以接下来就是copy函数内部调用_Block_object_assign函数,__block变量的存储就变成堆了,block函数就可以调用了。
3.这样复制到堆上,就引出释放的问题了。当堆上的block被释放了,就会调用block内部的dispose,dispose函数内部会调用_Block_object_dispose,堆上的__block被释放了
1.当多个栈上的block使用栈上的__block变量,__block变量被栈上的多个block函数持有。
2.当block0被复制到堆上是,按照上面讲的,也要把__block复制到堆上,但是现在block1在栈上持有__block,这不是搞事情嘛,假如堆上的__block变量变化,栈上的__block该怎么办呢?原栈上__block变量的__forwarding指向拷贝到堆上之后的__block变量。
3.当block1也被复制到堆上时,堆上的__block被block0和block1持有,引用计数+1.
4 当堆上的block都被释放,__block变量结构体引用计数为0,调用_Block_object_dispose,堆上的__block被释放。
看视频,看其他博客,最后自己理解,总结出了点东西。