Block函数有三种:
第一种:全局block
void (^block)(void) = ^{
NSLog(@"block!");
};
NSLog(@"%@",block);
打印结果:<__NSGlobalBlock__: 0x10d94f088>
第二种:堆区block
int a = 10;
void (^block)(void) = ^{
NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
打印结果:<__NSMallocBlock__: 0x6000020eb0c0>
第三种:栈区block,栈区block在iOS14后,越来越少,因此需要使用__weak
使其不在强持有。
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
<__NSStackBlock__: 0x7ffeeba41478>
全局访问外界变量强引用变成堆区,弱引用变成栈区。
既然是block,那就存在循环引用问题,那就先要了解循环引用的概念,按照正常的流程来说,例如A持有B,B的引用计数加1,而当A发送dealloc信号之后,B的引用计数需要减1变为0,那么dealloc才会正常被调用;而循环引用就是A持有B,B也持有A,构成了相互持有,那么在释放的时候,谁也释放不了对方,就造成了循环引用问题。
那么如何解决循环引用问题呢?
来看一段代码:
typedef void(^WXBlock)(void);
@interface ViewController ()
@property (nonatomic, copy) WXBlock block;
@property (nonatomic, copy) NSString *name;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循环引用
self.name = @"Block";
self.block = ^(void) {
NSLog(@"%@",self.name);
};
self.block();
}
在上面一段代码中,肯定是会造成循环引用的,因为self引用了block,而bloc也引用了self;类似于self -> block ->self
;
那么解决循环引用,相信很多人都知道是用__weak
;它加入了一张弱引用表,增加__weak typeof(self) weakSelf = self;
这一行实现弱引用,就类似于self -> block ->weakSelf -> self
;
那么weakSelf
持有强引用对象self
,引用计数是不会增加的,因此weakSelf
持有的self
在weakSelf生命周期结束之后,也就进行释放了。
下面是执行的结果:
那么这种方式来解决循环引用是会存在某些问题的,例如修改部分代码,异步延迟两秒执行:
self.block = ^(void) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
});
};
那么在延迟两秒执行后,还没来得及调用,self
就被释放了,因此self
的生命周期是不足以得到保证的。
那么我们又可以在block函数内部对weakSelf
进行强引用,就可以解决这个问题。
增加代码__strong typeof(self) strongSelf = weakSelf;
打印结果为:
这样的强引用对象是在block函数调用结束之后,就会进行释放。
那么使用__weak
解决循环引用就需要weak
和strong
结合使用。
完整代码:
__weak typeof(self) weakSelf = self;
self.block = ^(void) {
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
__weak
只是解决循环引用的方式之一,他是自动释放,下面介绍第二种解决方式,手动释放,看代码:
__block ViewController *vc = self;
self.block = ^(void) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
使用__block
对ViewController赋值self,通过在输出之后,手动将vc
置为nil。类似于self->block-> vc=nil ->self
;vc被block捕获,无法自动释放,那么手动释放,就解决了释放这一问题。
接下来介绍第三种解决循环引用问题,那就是通过参数来解决问题:
看代码
typedef void(^WXBlock)(ViewController *);
self.block = ^(ViewController *vc) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
在了解了block的使用之后,下面来看一下block的底层原理,首先通过xcrun
来看一下block的cpp是如何实现的:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("Block - ");
};
block();
return 0;
}
上面的c代码通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
转换为:
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;
}
__main_block_impl_0
是一个结构体:
也就是说,block的本质就是对象结构体,以函数作为参数传入进来;
由impl.FuncPtr = fp;
可以知道block是需要具体函数实现的;
而*__cself
作为匿名参数,因此可以获取block内部的代码,并执行。
那么如果有外界参数时,block又是如何实现的呢?
通过转换之后得到了下面的代码:
int a = 11;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
可以看到,a的值在编译时,就自动生成了相应的变量,而在__main_block_func_0
的方法中,就通过了一种赋值拷贝的方式赋值给a,但是里面的a和外面的a是不一样的。
那么对里面的a进行加加,在转换后为:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
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;
可以看到a是进行了指针拷贝,也就是说两个变量a同时指向同一片内存空间。
总结:
block本质一个对象结构体,匿名函数,block自动捕获外界变量生成同一个属性来保存,block调用block()是因为函数申明,需要有具体的函数实现;而__block的原理就是生成相应的结构体,保存原始变量进行指针拷贝,传递指针地址给block。
那到现在为止,其实还没有探索到一些核心的底层原理,还不清楚,__NSGlobalBlock__
,__NSStackBlock__
,__NSMallocBlock__
在内存地址中是如何变化的,以及关于block调用的问题。
下面我们探索一下运行时的block。
首先创建工程,代码很简单:
利用真机进行调试,打开汇编模式;
就出现了如下图所示的代码,这边它执行了一个objc_retainBlock
的跳转;
接下来我们手动添加objc_retainBlock
的断点:
执行下一步之后,就会进入到objc_retainBlock
的汇编,按住control+step into
,就进入到了一个_Block_copy
的汇编当中:
到这里,就可以清楚的知道block所在的动态库在libsystem_blocks.dylib
,因此可以在苹果官网下载所需要的源码。
在上面试过转化的cpp文件存在Block_layout
,在libsystem_blocks.dylib
就有这个结构,它是一个结构体,里面还有一个isa,block在底层真正的类型就是Block_layout
,在源码中,很多方法的参数都有Block_layout
:
下面来研究一下block的全局,堆区和栈区地址的变化,将除了26行的断点留下,其他断点去掉,重新执行程序,通过控制台读取寄存器信息:
下图是__NSGlobalBlock__
内存信息:
下面尝试一下捕获外界变量,声明一个a,在block中打印出来,重新执行程序:
在执行程序之后,它并没有跳转到objc_retainBlock
中来,打印的x0信息不对,在objc_retainBlock出打下断点,这时候读x0信息,就是栈区block了,__NSStackBlock__
:
__NSMallocBlock__
是从__NSStackBlock__
拷贝过去的,那么意味着预编译的时候是__NSStackBlock__
,然后让block copy操作,当进入了_Block_copy
汇编代码中,在最后一行有一个ret
的返回操作,在此处打断点:
如下图所示,在经过_Block_copy
返回之后,block的内存地址发生了变化,从0x000000016f837728
变到0x0000000282e036c0
,而__NSStackBlock__
也变成了__NSMallocBlock__
。
下图是_Block_copy
的底层源码实现,内部实现了为什么从__NSStackBlock__
转换成__NSMallocBlock__
:
总结:在block捕获外界变量时,会从__NSStackBlock__
经过_Block_copy
处理变成__NSMallocBlock__
。
下面来看一下block的签名,在block_layout
的结构体当中,有很多属性,其中就存在Block_descriptor_1
类型的descriptor
,而Block_descriptor_2
和Block_descriptor_3
都是可选类型,表示不是所有block都存在它们的一些属性;
而在它们是如何辨别是否需要属性呢?
看上图,主要是通过枚举值类型和进行地址平移来获得所需要的属性:
看下图的Block_descriptor的源码实现:
那现在去获取block的签名:
执行程序,将程序卡在_Block_copy
执行完之后,读取寄存器x0的信息:
最终获取的__NSMallocBlock__
的地址是0x0000000281e7c4e0
,而在查看block_layout
结构之后,通过x/4gx
获取它的信息,其中第一个是isa的值,而第4个就是descriptor
:
那我们清楚,descriptor的类型有1,2,3,其中2和3都是可选类型的,并不清楚它们是否存在,因此需要一个一个去尝试,首先打印第四个地址的内存情况,通过上面给的枚举值属性左移的位数,来查看地址是否有值,经过一翻查询,Block_descriptor_2
是没有的,而Block_descriptor_3
就存在值,在打印第三个地址之后,得到了它的签名:
打印签名信息: