美文网首页iOS进阶
探究 Block 的奥秘

探究 Block 的奥秘

作者: Miss_QL | 来源:发表于2018-08-30 10:52 被阅读6次

    闲来无事,总结了一下 block 的几点知识,以作巩固,欢迎指正。

    一、block 的本质
    block 本质上是一个 OC 对象,它内部有一个 isa 指针。
    block 是封装了函数调用以及函数环境的 OC 对象。

    要研究 block 在底层编译器具体的源码实现方式,可以使用 llvm 编译器中的 clang 命令clang-rewrite-objc main.m查看,它会将 OC 的源码 main.m 文件改写成 C++ 的 main.cpp 文件(但只可作为参考,因为 llvm 编译器生成的中间文件和 C++ 文件还是有所差异的)。更加完整的命令是:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,感兴趣的可以试一试。
    从源码中可以发现,block 在转换成 C++ 的时候显示为:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;  //block中调用的函数的地址
    };
    
    // block的描述信息
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;  //block所占的内存大小
    }
    
    // block的源码结构
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc; 
    };
    

    显而易见,block 结构体的第一个元素是结构体 __block_impl,而 __block_impl 结构体的第一个元素是 isa,由此可知 block 实际上是一个 OC 对象。
    其次,我们都知道 OC 对象都有其类型,可调用-class方法查看对象所属的类。

    void (^block)(void) = ^{
      NSLog(@"hello");
    };
    NSLog(@"%@", [block class]);
    NSLog(@"%@", [[block class] superclass]);
    NSLog(@"%@", [[[block class] superclass] superclass]);
    NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    
    控制台打印结果如下: image.png

    明显看出,__NSGlobalBlock __ 继承自__NSGlobalBlock,__NSGlobalBlock 继承自 NSBlock,NSBlock 继承自NSObject,即 block 所属的类最终是继承自 NSObject 的,所以说 block 是 OC 对象。

    二、block分类
    OC 中有3种类型的 block,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类。
    1、_NSConcreteGlobalBlock 全局的静态 block,在常量区(数据区域),不会访问任何外部变量。
    2、_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
    3、_NSConcreteMallocBlock 保存在堆中的 block,动态分配内存,当引用计数为 0 时会被销毁。

    // _NSConcreteGlobalBlock:没有访问auto变量
    void (^block)(void) = ^{
      NSLog(@"hello");
    };
    NSLog(@"%@", [block class]); //输出__NSGlobalBlock__
    
    // _NSConcreteStackBlock :访问了auto变量
    int a = 10;
    NSLog(@"%@", [^{
      NSLog(@"hello %d", a);
    } class]); //输出__NSStackBlock__
    
    // _NSConcreteMallocBlock:__NSStackBlock__调用copy方法
    int a = 10;
    void (^block)(void) = [^{
      NSLog(@"hello %d", a);
    } copy];
    NSLog(@"%@", [block class]);//输出__NSMallocBlock__
    
    每一种类型的 block,调用 copy 后的结果如下所示: image.png

    注意!在ARC环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况:
    1、block 作为函数返回值时
    2、将 block 赋值给 __strong 指针时
    3、block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
    4、block 作为 GCD API 的方法参数时

    当 block 内部访问了对象类型的 auto 变量的时候,会产生两种可能:
    1、如果 block 是在栈上,将不会对 auto 变量产生强引用;
    2、如果 block 被拷贝到堆上,会调用 block 内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)操作,类似于 retain (形成强引用、弱引用)。
    如果 block 从堆上移除,会调用 block 内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的 auto 变量,类似于 release。

    三、block 的变量捕获
    问题1:下面的代码输出结果为何?

    int a = 10;
    void (^block)(void) = ^{
      NSLog(@"a = %d", a);
    };
    a = 20;
    block();
    

    通过终端命令,可以查看到编译后的最终代码如下:

    // 当前block的结构
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    int a = 10;
    //此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为a,被block结构体内部的int a;元素所捕获,故此block结构体内部的a元素保存的是传入的参数a的值
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    //再修改外部参数a=20也无用,因为这并不影响block结构体内部a元素的值
    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为: image.png

    问题2:若将上述问题中的int a = 10;修改成static int a = 10;,结果是否会有所不同?
    同上,粘代码便一目了然(截取部分 OC 代码与 C++ 代码)。

    /***** 编译前的OC代码 *****/
    static int a = 10;
    void (^block)(void) = ^{
      NSLog(@"a = %d", a);
    };
    a = 20;
    block();
    
    /***** 编译后的C++代码 *****/
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static int a = 10;
    //此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为&a,被block结构体内部的int *a;元素所捕获,故此block结构体内部的指针a元素保存的是传入的参数a的地址值
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
    //再修改外部参数a=20,在调用block函数时,因其结构体内部a元素保存的是外部参数a的地址值,此时该地址值所指向的外部参数a=20
    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为: image.png

    问题3:若将上述问题中的int a = 10;声明为全局变量,结果又是否会有所不同?
    同上,粘代码便一目了然(截取部分 C++ 代码)。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    很明显,block 结构体内部并没有捕获全局变量a的值或者地址,而是直接访问全局变量的,故此可以看出输出的结果为: image.png

    总结:
    为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制:
    全局变量:不捕获到 block 内部,直接访问
    局部变量:捕获到 block 内部,其中auto变量通过值传递访问,static变量通过指针传递访问

    备注:auto自动变量,离开作用域就会被销毁。

    - (void)blockTest {
        // block外面的a和block内部的a实际上不是同一个对象
        int a = 10; //a在栈上
        NSLog(@"outside a === %p", &a);
        void (^blocka)(void) = ^(void) {
            NSLog(@"a ==== %p", &a); //a被copy了一份,实际上C方法的代码块中传递的是copy得到的a的值
        };
        blocka();
        
        __block int b = 10; //b在栈上
        NSLog(@"outside b === %p", &b);
        void (^blockb)(void) = ^(void) {
            NSLog(@"b ==== %p", &b); //b被copy到堆上,实际上C方法的代码块中传递的是copy得到的b的指针地址
        };
        blockb();
    }
    

    四、__block 的作用

    int age = 10;
    void (^block)(void) = ^{
      NSLog(@"age is %d", age);
    };
    block();
    

    毫无疑问,控制台会输出 “age is 10”。如果想在 block 代码块中修改局部变量 age 的值为20,该怎么做?
    直接修改肯定会报错,当然可以直接将 age 定义为 static 变量或者全局变量,问题就会迎刃而解。但是这样的话,age 就会一直存在在内存的全局区中,这并不是最理想的状态,希望 age 还是一个临时变量,在不用的时候会自动销毁。这时候可以使用 __block 修饰 age 即可。

    __block int age = 10;
    void (^block)(void) = ^{
      age = 20;
      NSLog(@"age is %d", age);
    };
    block();
    

    此时控制台会输出 “age is 20”。用 __block 的好处是不会修改变量的性质,age 还是一个 auto 类型的自动变量。

    __block 可以用来用于解决 block 内部无法修改 auto 变量值的问题。
    __block 不能修饰全局变量、静态变量(static)。
    编译器会将 __block 变量包装成一个对象。
    上面的代码中__block int age = 10;会编译成下面的代码结构:

    struct __Block_byref_age_0 {
      void *__isa;
      __Block_byref_age_0 *__forwarding; //指向结构体自身的指针
      int __flags;
      int __size;
      int age; //这个age才是对应外部 __block 修饰的局部变量,共享同一块内存地址
    };
    
    //block 内部有个 *age 指针,指向 __Block_byref_age_0 结构体,__Block_byref_age_0 结构体内部有 int age ;通过 __Block_byref_age_0 结构体的 __forwarding 指针找到其内部 age 的内存地址,并修改其值,完成修改。
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_age_0 *age; // by ref
    };
    

    __block 的内存管理
    当 block 在栈上时,并不会对 __block 变量产生强引用。
    当 block 被拷贝到堆上时,会调用 block 内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对 __block 变量形成强引用(retain)。

    五、block 的使用
    1、作为对象的属性
    用 block 作为属性,保存一段代码块,可以在任何想用它的地方随时使用。MVVM 设计模式中,在绑定 view 和 VM 的时候就常用到。

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    // ARC下block可以用strong修饰,MRC下用copy修饰
    @property (nonatomic, strong) void(^myName)(NSString * name);
    @end
    

    ViewController中使用即可:

    - (void)viewDidLoad {
        [super viewDidLoad];
        Person * p = [[Person alloc] init];
        p.myName = ^(NSString *name) {
            NSLog(@"你好,我的名字叫%@", name);
        };
        p.myName(@"Rac");
    }
    
    打印结果如下: image.png

    2、作为方法的参数

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    - (void)eat:(void (^)(void))block;
    @end
    
    #import "Person.h"
    @implementation Person
    - (void)eat:(void (^)(void))block {
        //1、保存这个block
        //2、做特定的事情
        //3、得到一个结果,将结果给block
        block();
    }
    @end
    

    ViewController中调用即可:

    - (void)viewDidLoad {
        [super viewDidLoad];
        Person * p = [[Person alloc] init];
        [p eat:^{
            NSLog(@"我要吃东西");
        }];
    }
    
    打印结果如下: image.png

    3、作为方法的返回值
    block作为方法的返回值,可以通过点语法进行调用,进而实现链式编程功能,我们常用的自动布局第三方Mansory的实现就是这个原理。

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    //打点调用,必须是getter方法,满足两点:1、有返回值;2、没有参数
    - (void(^)(int))run;
    // 持续打点调用(即链式编程),block返回当前类的对象即可
    - (Person *(^)(int))runAgain;
    @end
    
    #import "Person.h"
    @implementation Person
    - (void (^)(int))run {
        return ^(int m){
            NSLog(@"我跑了%d米", m);
        };
    }
    - (Person *(^)(int))runAgain {
        return ^(int m){
            NSLog(@"我这次又跑了%d米", m);
            return self;
        };
    }
    @end
    

    ViewController中调用即可:

    - (void)viewDidLoad {
        [super viewDidLoad];
        Person * p = [[Person alloc] init];
        p.run(10);
    //    p.run(10)相当于:
    //    void (^blocka)(int m) = p.run;
    //    blocka(10);
        p.runAgain(10).runAgain(20).runAgain(30);
    }
    
    打印结果如下: image.png

    六、block 循环引用问题
    循环引用,肯定大家都知道的,通俗点说,就是A持有B,B持有A,互相强引用,导致双方(或多方)都无法正常释放,从而引起内存泄漏。
    我这里也提供三种解决block循环引用的方法,当然还有其他的解决办法,原则就一个:打破这个循环链!
    1、__weak

        __weak typeof (self) weakself = self;
        self.vblock = ^(){
            weakself.view.backgroundColor = [UIColor yellowColor];
            [weakself.navigationController popViewControllerAnimated:YES];
        };
        self.vblock();
    

    2、__block
    注意:blockSelf在代码块中用要完置为nil

        __block ViewController * vself = self;
        self.vblock = ^{
            vself.view.backgroundColor = [UIColor yellowColor];
            [vself.navigationController popViewControllerAnimated:YES];
            vself = nil;
        };
        self.vblock();
    

    3、self作为block的参数传递进去

        self.sblock = ^(ViewController * vc) {
            vc.view.backgroundColor = [UIColor yellowColor];
            [vc.navigationController popViewControllerAnimated:YES];
        };
        self.sblock(self);
    

    检测是否解决循环引用问题,只需查看dealloc方法是否被调用即可,因为当前对象被销毁时肯定会走dealloc方法,大家可自行测试。

    相关文章

      网友评论

        本文标题:探究 Block 的奥秘

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