美文网首页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

相关文章

  • OC中的Block(二)

    block的变量捕获(capture) 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制 ...

  • block变量的捕获(capture)

    ?为了保证block内部能够正常访问外部变量,block有个变量捕获机制 auto变量的捕获

  • iOS开发 (解惑-02)

    一、block 1)iOS开发的内存分配;2)block的变量捕获机制:为了保证在block的内部可以正常访问外部...

  • Objective - C block(二)block的类型及捕

    (一)block 捕获变量类型 为了保证block内部能够正确访问外部的变量,block有一个变量捕获机制 (1)...

  • Block 之 变量捕获

    为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,即捕获外部变量。 前言: 搞清成员变量、...

  • iOS Block的变量捕获机制

    block的变量捕获机制 先看几段代码: 执行下面的代码会输出什么? 会输出 a = 10, b = 20 执行下...

  • block:block捕获变量

    一、block捕获变量根儿上的东西 1、block会捕获局部变量 2、block不会捕获全局变量二、block捕获...

  • block-变量的捕获(capture)

    为了能够保证block正常访问外部的变量,block有个变量捕获机制,如下图 auto:自动变量,平时我们定义in...

  • iOS Block本质笔记

    OC中定义block block访问外部参数 OC转C++分析 block的变量捕获机制 为了保证block能够正...

  • [iOS]Block系列探究六 - __block变量和对象

    之前的文章[iOS]Block系列探究二 - 捕获变量谈论过__block是如何处理基础类型的局部变量使block...

网友评论

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

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