美文网首页
iOS底层-Block底层原理

iOS底层-Block底层原理

作者: 大橘猪猪侠 | 来源:发表于2020-11-16 17:29 被阅读0次

    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底层原理

          本文链接:https://www.haomeiwen.com/subject/qhszbktx.html