美文网首页iOS
iOS Block的变量捕获机制

iOS Block的变量捕获机制

作者: 左左4143 | 来源:发表于2019-06-13 22:38 被阅读25次

    block的变量捕获机制

    先看几段代码:

    执行下面的代码会输出什么?

    int main(int argc, const char * argv[]) { 
        void(^block)(int,int) = ^(int a, int b){
            NSLog(@"a = %d, b = %d",a,b);
        };
        block(10,20);
        return 0;
    }
    

    会输出 a = 10, b = 20

    执行下面的代码会输出什么?

    int main(int argc, const char * argv[]) { 
        int age = 10;
        void (^block)(void) = ^{
            NSLog(@"age = %d",age);
        };
        age = 20;
        block();
        return 0;
    }
    

    会输出age = 10,但是age明明已经重新赋值成20了,为什么执行block age的值还是10 呢?

    我们将代码通过clang -rewrite-objc main.m命令将文件转换为cpp格式的文件,可以看到block的底层结构,可以看到上面这两种block的底层结构有什么区别:
    第一种block:

    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;
      }
    };
    

    第二种block

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

    可以看到第二种block的底层结构中,多了一个int类型 名字为age的变量,为什么会多一个这样的变量呢?
    因为block为了保证在其内部能够正常访问外部的变量,block有一个变量捕获机制 capture,在创建block的时候,age=10, age这个值已经存储到block内部了,所以即使age后来被重新赋值,运行block时打印结果依然是age = 10,第一种block内部没有访问外界的变量,所以它的底层结构不会发生变化

    此时 我们把age改成一个静态变量,作用域不变,就像这样:

    int main(int argc, const char * argv[]) { 
        static int age = 10;
        void (^block)(void) = ^{
            NSLog(@"age = %d",age);
        };
        age = 20;
        block();
        return 0;
    }
    

    运行程序后会看到此时的打印结果为 age = 20,block运行后得出的值会随着age的改变而改变, 那么block是不是就没有捕获这个静态变量呢?

    我们同样可以看一下这个block的底层结构:

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

    可以看到block的底层结构中,依然会增加一个 *age 的变量,说明这种情况下block依然捕获了静态类型的age变量,与第二种block不同的是,第二种block相当于在block内部新建了一个int类型的变量来保存外部的那个age的值,而在这个block内部 相当于保存了外部age这个变量的内存地址,block内部的age与外部的age是同一个地址,所以当外部的age值改变时,block内部的age值也会改变

    那如果age是一个全局变量 而不是一个局部变量呢?像这样:

    int age = 10;//全局变量
    static int height = 60;//静态全局变量
    
    int main(int argc, const char * argv[]) {
        
        void (^block)(void) = ^{
            NSLog(@"age = %d, height = %d",age,height);
        };
        age = 20;
        height = 120;
        block();
        
        return 0;
    }
    

    此时程序的运行结果为 age = 20,height = 120, 同样我们查看block的底层数据 发现 block并没有捕获这两个全局变量

    int age = 10;
    static int height = 60;
    
    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;
      }
    };
    

    刚才用到的都是基础类型的变量,如果我们用对象类型的变量呢?来试一试:

    NSNumber *number1;
    static NSNumber *number2;
    
    int main(int argc, const char * argv[]) {
        
        NSNumber *number3;
        static NSNumber *number4;
        
        number1 = @1;
        number2 = @2;
        number3 = @3;
        number4 = @4;
        
    
        void (^block)(void) = ^{
            NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
            NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
            NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
            NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);
        };
        
        number1 = @10;
        number2 = @20;
        number3 = @30;
        number4 = @40;
        
        NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
        NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
        NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
        NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);
    
        
        block();
        
        return 0;
    }
    

    运行程序 得到的结果是:


    屏幕快照 2019-06-16 上午9.49.34.png

    可以看到 只有number3 的值和内存地址都发生了变化,其余的都没有变化,那其他三个是不是都没有被block捕获呢?我们还是通过这个block的底层结构来看一下:

    NSNumber *number1;
    static NSNumber *number2;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSNumber *number3;
      NSNumber **number4;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSNumber *_number3, NSNumber **_number4, int flags=0) : number3(_number3), number4(_number4) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    首先可以确定的是 number1和number2是没有被block捕获的,因为NSNumber是对象类型本身就是一个指针,所以number3 是被block捕获了,从前后两次打印出来的number3的数据可以看出来两个number3的地址是不同的,block内部相当于新建了一个NSNumber类型的变量来保存外部的number3,而number4 在block内部是一个双指针,也就是block内部保存了这个number4内存地址的指针,所以两个number4前后的值和地址是一样的。

    可以看到无论是基础类型或者对象类型,block对于变量的捕获机制基本是相同的:

    • 局部变量
      • auto变量 会被捕获,访问方式是值传递 (block内部会专门新增一个成员来存储auto变量的值,block运行时会访问这个新增的成员)
      • static变量 会被捕获,访问方式是指针传递(问题:这种方式到底算不算捕获?)
    • 全局变量 不会捕获,会直接去访问

    可以看到只要是在block内部访问局部变量,那么block就会捕获这个变量,区别在于如果是自动变量是捕获它的值,而静态变量是捕获它的指针,如果block内部访问的是全局变量,block就不会捕获这个变量(无论是静态还是非静态全局变量)

    那么block为什么要采用这种做法呢?为什么局部变量就需要捕获,全局变量就不用?

    我们来看另一段代码:

    void (^block)(void);
    void test(){
        int age = 10;
        static int height = 60;
        block = ^{
            NSLog(@"age = %d, height = %d",age,height);
        };
    }
    
    int main(int argc, const char * argv[]) {
        test();
        block();
        
        return 0;
    }
    

    很明显 test()方法执行完毕之后,它方法内部的变量age和height就出了作用域了,在作用域之外就无法访问,然后执行block()方法,而block()方法内部又用到了age和height,但是此时这两个变量已经不能访问了(auto变量已经销毁,自然无法访问,static局部变量虽然不会销毁,但已经出了作用域,也不能访问),如果要保证正常的访问,就相当于要达到跨函数访问变量这种效果,所以block就会采用捕获局部变量这种方式来保证程序正常运行。

    为什么auto变量是值传递?static变量是指针传递?

    因为auto类型的局部变量 出了自己的作用域就被销毁了,这个变量就不存在了,它原来所占的内存就变成了垃圾内存了,不可以再访问,所以针对这种变量就需要在创建block的时候马上保存到block内部,否则在运行block的时候这个变量就可能没了,所以在block创建之后再怎么改变这个变量的值,运行block的时候依然是之前的值 。

    而static局部变量虽然出了作用域也不能访问,但它的内存是一直存在的,不会销毁,所以block只需要在运行的时候能访问到它就可以,所以针对这种变量block采用的是指针传递,block内部只要保存这个变量的内存地址就可以保证在block运行的时候访问到这个变量,而正因为是指针传递,多以block在运行的时候总能够访问到这个变量最新的值。

    看到这里,我们也很容易明白为什么全局变量不用捕获,因为全局变量既不会被销毁,也可以随处访问,所以block根本不用去捕获它也可能随时随地访问到它的值。

    注意:
    在一个类中的block的实现中用到了self,那这个block会捕获self(其实也就是这个类的实例对象),因为self是一个局部变量,通过类中的方法底层实现可以看到,每个方法的前两个参数都是self和方法名,那么self也就是一个参数,肯定是一个局部变量

    如果block的实现中用到了这个类的某个属性(比如_name)那block也是会捕获的,因为_name相当于self->name,此时block会直接捕获self,而不是单单捕获name这个属性,同样对于self.name这样的属性,block也会捕获self

    相关文章

      网友评论

        本文标题:iOS Block的变量捕获机制

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