iOS底层-Block底层原理

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生命周期结束之后,也就进行释放了。
下面是执行的结果:

iShot2020-11-15 11.41.51.png

那么这种方式来解决循环引用是会存在某些问题的,例如修改部分代码,异步延迟两秒执行:

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的生命周期是不足以得到保证的。

iShot2020-11-15 11.42.18.png

那么我们又可以在block函数内部对weakSelf进行强引用,就可以解决这个问题。
增加代码__strong typeof(self) strongSelf = weakSelf;
打印结果为:

iShot2020-11-15 12.02.53.png

这样的强引用对象是在block函数调用结束之后,就会进行释放。
那么使用__weak解决循环引用就需要weakstrong结合使用。
完整代码:

__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是一个结构体:

iShot2020-11-15 12.22.38.png

iShot2020-11-15 12.31.06.png

也就是说,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;
iShot2020-11-15 12.40.52.png

可以看到,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;
iShot2020-11-15 12.48.32.png

可以看到a是进行了指针拷贝,也就是说两个变量a同时指向同一片内存空间。

总结:
block本质一个对象结构体,匿名函数,block自动捕获外界变量生成同一个属性来保存,block调用block()是因为函数申明,需要有具体的函数实现;而__block的原理就是生成相应的结构体,保存原始变量进行指针拷贝,传递指针地址给block。

那到现在为止,其实还没有探索到一些核心的底层原理,还不清楚,__NSGlobalBlock____NSStackBlock____NSMallocBlock__在内存地址中是如何变化的,以及关于block调用的问题。
下面我们探索一下运行时的block。

首先创建工程,代码很简单:

iShot2020-11-16 15.53.53.png

利用真机进行调试,打开汇编模式;
就出现了如下图所示的代码,这边它执行了一个objc_retainBlock的跳转;

iShot2020-11-16 15.59.43.png

接下来我们手动添加objc_retainBlock的断点:

iShot2020-11-16 16.05.11.png

执行下一步之后,就会进入到objc_retainBlock的汇编,按住control+step into,就进入到了一个_Block_copy的汇编当中:

iShot2020-11-16 16.06.47.png

到这里,就可以清楚的知道block所在的动态库在libsystem_blocks.dylib,因此可以在苹果官网下载所需要的源码。

在上面试过转化的cpp文件存在Block_layout,在libsystem_blocks.dylib就有这个结构,它是一个结构体,里面还有一个isa,block在底层真正的类型就是Block_layout,在源码中,很多方法的参数都有Block_layout

iShot2020-11-16 16.09.39.png

下面来研究一下block的全局,堆区和栈区地址的变化,将除了26行的断点留下,其他断点去掉,重新执行程序,通过控制台读取寄存器信息:
下图是__NSGlobalBlock__内存信息:

iShot2020-11-16 16.19.39.png

下面尝试一下捕获外界变量,声明一个a,在block中打印出来,重新执行程序:

在执行程序之后,它并没有跳转到objc_retainBlock中来,打印的x0信息不对,在objc_retainBlock出打下断点,这时候读x0信息,就是栈区block了,__NSStackBlock__

iShot2020-11-16 16.22.54.png

__NSMallocBlock__是从__NSStackBlock__拷贝过去的,那么意味着预编译的时候是__NSStackBlock__,然后让block copy操作,当进入了_Block_copy汇编代码中,在最后一行有一个ret的返回操作,在此处打断点:
如下图所示,在经过_Block_copy返回之后,block的内存地址发生了变化,从0x000000016f837728变到0x0000000282e036c0,而__NSStackBlock__也变成了__NSMallocBlock__

iShot2020-11-16 16.32.40.png

下图是_Block_copy的底层源码实现,内部实现了为什么从__NSStackBlock__转换成__NSMallocBlock__

iShot2020-11-16 17.16.00.png

总结:在block捕获外界变量时,会从__NSStackBlock__经过_Block_copy处理变成__NSMallocBlock__

下面来看一下block的签名,在block_layout的结构体当中,有很多属性,其中就存在Block_descriptor_1类型的descriptor,而Block_descriptor_2Block_descriptor_3都是可选类型,表示不是所有block都存在它们的一些属性;

iShot2020-11-16 16.48.36.png

而在它们是如何辨别是否需要属性呢?


iShot2020-11-16 17.02.22.png

看上图,主要是通过枚举值类型和进行地址平移来获得所需要的属性:
看下图的Block_descriptor的源码实现:


iShot2020-11-16 16.49.48.png

那现在去获取block的签名:

执行程序,将程序卡在_Block_copy执行完之后,读取寄存器x0的信息:
最终获取的__NSMallocBlock__的地址是0x0000000281e7c4e0,而在查看block_layout结构之后,通过x/4gx获取它的信息,其中第一个是isa的值,而第4个就是descriptor

iShot2020-11-16 16.55.43.png

那我们清楚,descriptor的类型有1,2,3,其中2和3都是可选类型的,并不清楚它们是否存在,因此需要一个一个去尝试,首先打印第四个地址的内存情况,通过上面给的枚举值属性左移的位数,来查看地址是否有值,经过一翻查询,Block_descriptor_2是没有的,而Block_descriptor_3就存在值,在打印第三个地址之后,得到了它的签名:

iShot2020-11-16 17.06.42.png

打印签名信息:

iShot2020-11-16 17.11.34.png

你可能感兴趣的:(iOS底层-Block底层原理)