美文网首页iOS高级技术文章
Block原理探究(上篇)-Block本质及存储域问题

Block原理探究(上篇)-Block本质及存储域问题

作者: 梧雨北辰 | 来源:发表于2019-10-07 21:14 被阅读0次

    主要内容:

    1. 分析Block的源码
    2. 验证Block的本质是对象
    3. 理解Block的存储域分类
    4. 验证Block的不同存储域
    5. 分析BlockCopy原理

    一、分析Block的源码

    为了分析Block的源码,从一个最简单的Block使用示例说起,测试代码如下:

    //main.m文件:
    #import <Foundation/Foundation.h>
    int main(int argc, char * argv[]) {
        int num = 10;
        void (^block)(void) =^{NSLog(@"num = %d",num);};
        block();
        return 0;
    }
    

    Objective-C语言是基于CC++的,为了深入理解Block的底层结构,我们可以通过如下的编译器命令将上述代码转换成C++源码:

    clang -rewrite-objc 源代码文件名(如此例中的main.m)
    

    转化后的C++源码如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int num;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int num = __cself->num; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_9e3646_mi_0,num);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    }
     __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    
    int main(int argc, char * argv[]) {
        int num = 10;
        void (*myBlock)(void) =((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
        return 0;
    }
    

    对比OC代码与C++源码中的main函数,我们发现:

    1. 创建Block其实是调用了__main_block_impl_0结构体的构造函数;
    2. Block中待执行代码也都被封装到了__main_block_func_0函数中;

    另外值得注意的是,这些C++的结构体和函数的命名,是根据Block语法所属的函数名(此处为main)和Block语法在该函数出现的顺序值(此处为0)来设定的;

    根据这些对应关系,我们对C++源码中的内容一一分析:

    1.__main_block_imp_0结构体

    __main_block_impl_0结构体对应了Block的定义,结构体内部包含了三个成员变量implDescnum

    num其实就是被捕获的变量(后续再讲),另外还有一个同名的构造函数__main_block_impl_0。可以看到相关的代码如下:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int num;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    Block通过调用这里的构造函数得以创建,调用时需传入了四个参数:(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0),前三个参数对应成员变量的初始化,而最后一个参数flags携带默认值可暂不考虑;

    2.__block_impl结构体

    __main_block_imp_0结构体的第一个成员变量impl,就是__block_impl结构体类型;

    尤其注意:

    1. 该结构体中包含有isa指针,从这一点就可以说明Block本质上还是一个OC对象,因为OC中只有对象才会具有isa指针的概念;
    2. FuncPtr是一个函数指针,在__main_block_imp_0构造函数调用时被赋值;
    3.__main_block_desc_0结构体

    __main_block_imp_0结构体构造函数中传入参数desc,其实就是__main_block_desc_0对象。该结构体包含两个成员变量:

    1. reserved:系统保留值;
    2. Block_size:代表Block的大小;
    4.__main_block_func_0函数

    __main_block_imp_0结构体构造函数中传入函数指针fp,其实就是__main_block_func_0函数的地址;

    该函数是将Block中所有的代码封装为函数,以待被调用;

    5.总结Block的特点
    1. Block本质上一个OC对象:比如这里的Block,其底层对应了__main_block_impl_0结构体,而且内部包含有isa指针;
    2. Block中携带了函数执行的环境:此处Block里待执行的代码,在底层被封装为__main_block_func_0函数,以实现调用;
    3. Block相当于其他语言中的闭包或者匿名函数:它与函数区别在于,Block相当于函数+函数执行的上下文环境(捕获外部变量下面会讲到);

    二、验证Block的本质是对象

    下面通过打印的方式验证Block对象本质,测试代码如下:

    - (void)testBlock5 {
        void(^block)(int a) = ^(int a) {
            NSLog(@"This is a block");
        };
        
        NSLog(@"%@",[block class]);
        NSLog(@"%@",[[block class] superclass]);
        NSLog(@"%@",[[[block class] superclass] superclass]);
        NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
    }
    
    //打印结果:
    //__NSGlobalBlock__
    //__NSGlobalBlock
    //NSBlock
    //NSObject
    

    观察打印结果:

    1. 我们看到Block最终继承于NSObject类型,这里再一次验证了Block本质就是OC对象的结论;
    2. 而打印结果中出现的__NSGlobalBlock__,说明此处的Block的存储域为静态区;

    三、理解Block的存储域分类

    在之前Block结构体构造函数中,我们很容易能找到这样一句代码:

    impl.isa = &_NSConcreteStackBlock;
    

    我们已经知道Block也是一个OC对象,而每个OC对象都有一个isa指针指向其类对象,这里的情况也是类似的;

    这里Blockisa指针指向了_NSConcreteStackBlock类对象,即此时的Block是以_NSConcreteStackBlock类为模板创建的实例;

    除此之外,其实还有两个与之类似的类_NSConcreteGlobalBlock_NSConcreteMallocBlock,不同的Block类创建的对象用于不同的存储域,也对应了对应不同的OC类型,具体整理如下:

    clang类 OC类 内存区域
    _NSConcreteStackBlock __NSStackBlock__ 栈区
    _NSConcreteMallocBlock __NSMallocBlock__ 堆区
    _NSConcreteGlobalBlock __NSGlobalBlock__ 静态区

    四、验证Block的不同存储域

    不同存储域的Block使用方式有很大差别,而正确区分Block类型的关键在于:Block中是否引用了自动变量(需要MRC下测试),总结起来如下:

    Block类型 环境 内存区域
    _NSConcreteGlobalBlock(__NSGlobalBlock__) 没有访问自动变量;
    或者只用到静态区变量
    静态区
    _NSConcreteStackBlock( __NSStackBlock__) 访问了自动变量 栈区
    _NSConcreteMallocBlock(__NSMallocBlock__) __NSStackBlock__调用了copy 堆区

    为了验证上述情况,我们需要切换到MRC环境下,因为在ARC环境下的编译器为我们做了很多优化的工作,比如自动将栈区的Block拷贝到堆区,这样我们也就不容易捕获到Block初始状态的位置了。

    所以,这里暂时将开发环境切换至MRC下来测试,相关的测试代码如下:

    - (void)testBlock7 {
        //1.Block内部没有调用外部自动变量
        void (^block1)(void) = ^{
            NSLog(@"Block");
        };
        
        //2.Block内部调用外部自动变量
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Block-%d",a);
        };
        
        //3.拷贝栈上的block
        void (^block3)(void) = ^{
            NSLog(@"Block-%d",a);
        };
        
        //打印Block类型
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [[block3 copy] class]);
    }
    
    //打印结果:
    //__NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__
    
    1.NSGlobalBlock(静态区)
    1. 判断依据:Block中没有引用自动变量或者只用到静态区变量;
    2. 此类型的Block与全局变量一样设置在程序的静态区,直到程序结束才会被回收;
    3. 此类型的Block不依赖执行时的状态,所以整个程序只需一个实例,用的也较少;
    2.NSStackBlock(栈区)
    1. 判断依据:Block中访问自动变量,并且存放在栈中;
    2. 栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放;
    3. 所以我们有可能遇到Block内存销毁之后才使用它的情况,开发中遇到的很多问题也都是因此而起;
    3.NSMallocBlock(堆区)
    1. _NSStackBlock__执行copy操作会生成__NSMallocBlock__
    2. Block被拷贝后存放在堆中后,需要我们自己进行内存管理,否则还可能造成一些循环引用的问题;

    五、分析Block的Copy原理

    Block有着不同的存储域类型,尤其是配置在栈上的Block(即__NSStackBlock__类型的Block),如果其所属的作用域结束,该Block就会被释放,此时若继续使用Block,就会造成野指针问题;

    所以,我们通常的做法就是执行copy操作,将其由栈区拷贝到堆区得到__NSMallocBlock__,而__NSMallocBlock__也会在其引用计数为0的时候被释放;

    进一步分析Block的拷贝,需要分为MRCARC两种环境来考虑。

    1.MRC下的Block拷贝

    MRC环境下,我们只能显式的通过copy来实现Block的拷贝;通常为了避免Block的释放,我们定义Block属性的时候必须使用copy修饰符也正是基于这个原因。

    下面是在MRC环境下测试栈Block的使用,具体代码如下:

    typedef void(^PrintBlock)(void);
    
    @interface ViewController ()
    @property (nonatomic ,copy)PrintBlock block1;
    @property (nonatomic ,copy)PrintBlock block2;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self createBlock];
        self.block1();
        self.block2();
        NSLog(@"block1:%@", [self.block1 class]); //报错Thread 1: EXC_BAD_ACCESS (code=1, address=0x7ffeeb90b8c0)
        NSLog(@"block2:%@", [self.block2 class]);
    }
    
    - (void)createBlock {
        int a = 10;
        //此处采用直接赋值的方式,不会触发setter方法
        _block1 = ^{
            NSLog(@"This is block1-%d",a);
        };
        
        self.block2 = ^{
            NSLog(@"This is block2-%d",a);
        };
        //离开此作用域,block1就会被释放
        NSLog(@"block1:%@、block2:%@", [self.block1 class],[self.block2 class]);
    }
    @end
    

    打印结果及分析如下:

    block1:__NSStackBlock__、block2:__NSMallocBlock__
    This is block1-10
    This is block2-10
    

    由于block1采用的是直接赋值的方式,没有调用setter方法,所以block1并没有被拷贝到堆上,是一个栈上的Block,这样也就直接导致了第二次打印block1时所发生的野指针崩溃;

    2.ARC下的Block拷贝

    ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,总结有以下几种情况:

    • Block作为函数返回值时;这就类似于MRC中对返回值Block执行了[[returnedBlock copy] autorelease];
    • Block被强引用,如Block被赋值给__strong或者id类型;
    • Block作为GCD API的方法参数时;
    • Block作为系统方法名含有usingBlock的方法参数时;

    下面的代码演示了这些情况:

    typedef void(^Block)(void);
    -(Block)getBlock{
        //ARC下的Block中访问了auto变量,此时block类型应为__NSStackBlock__
       int a = 10;
       return  ^{
            NSLog(@"---------%d", a);
        };
    }
    
    - (void)testBlock9 {
        //1.测试block作为函数返回值时
        NSLog(@"bock1-:%@",[[self getBlock] class]);
        
        //2.测试将block赋值给__strong指针时
        int a = 10;
        
        //2.1.block内没有访问auto变量
        Block block21 = ^{
            NSLog(@"block21");
        };
        NSLog(@"block21-%@",[block21 class]);
        
        //2.2.block内访问了auto变量,但没有赋值给__strong指针
        NSLog(@"block22-%@",[^{
            NSLog(@"block22-%d", a);
        } class]);
    
        //2.3.block赋值给__strong指针
        Block block23 = ^{
            NSLog(@"block23");
        };
        NSLog(@"block23-%@",[block23 class]);
        
        //3.block作为Cocoa API中方法名含有usingBlock的方法参数时
        NSArray *array = @[@"1",@"2",@"3"];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
        }];
        
        //4.block作为GCD API的方法参数时
        //Block中的延时操作完成时,系统将会对Block进行释放
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
        });
    }
    
    //打印结果如下:
    //bock1-:__NSMallocBlock__
    //block21-__NSGlobalBlock__
    //block22-__NSStackBlock__
    //block23-__NSGlobalBlock__
    
    3.其他存储域Block的拷贝

    上面讲述的重点都于对栈Blok的拷贝,若是对于已经配置在堆上或者配置在静态区的上的Block调用copy方法又将如何呢?下面是不同存储域的Block执行copy进行的总结:

    Block类型 副本源的配置存储域 复制效果
    _NSConcreteStackBlock 栈区 从栈复制到堆
    _NSConcreteGlobalBlock 静态区 什么也不做
    _NSConcreteMallocBlock 堆区 引用增加
    4. 总结Block需要拷贝的原理

    Block默认创建于其所在函数的函数栈上,所以当函数作用域结束时就会随之销毁;

    MRC环境下,没有编译器的优化,所以我们非常强调要使用copyBlock拷贝到堆上,从而避免Block在其作用域结束时被直接释放;

    ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,对于Block使用copy还是strong效果是一样的,所以写不写copy都行。在ARC环境下对于Block依然使用copy,更像是从MRC遗留下来的“传统”,时刻提醒我们:编译器自动对Block进行了拷贝操作。如果不写copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对Block进行了拷贝操作”,他们有可能会在调用之前自行拷贝属性值,这种操作多余而低效。

    最后,总结Block修饰符的使用:

    //MRC下block属性的建议写法:
    @property (copy, nonatomic) void (^block)(void);
    
    //ARC下block属性的建议写法:
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

    参考链接

    1. 苹果官方Block文档
    2. 深入研究 Block 捕获外部变量和 __block 实现原理

    相关文章

      网友评论

        本文标题:Block原理探究(上篇)-Block本质及存储域问题

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