iOS Block的变量捕获机制

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,183评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,850评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,766评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,854评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,871评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,457评论 1 311
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,999评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,914评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,465评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,543评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,675评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,354评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,029评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,514评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,616评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,091评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,685评论 2 360

推荐阅读更多精彩内容

  • 参考篇:iOS-Block浅谈 前言:本文简述Block本质,如有错误请留言指正。 第一部分:Block本质 Q:...
    梦蕊dream阅读 61,225评论 41 322
  • 第一部分:Block本质 Q:什么是Block,Block的本质是什么? block本质上也是一个OC对象,它内部...
    sheldon_龙阅读 559评论 0 0
  • 一. 查看block内部实现 1.编写block代码void (^DemoBlock)(int, int) = ^...
    李永开阅读 157评论 0 0
  • 前言:Block 是开发过程中常用便捷的回调方式,本文简单介绍 Block 一、Block 简介 Block 对象...
    梦蕊dream阅读 4,755评论 5 26
  • Block概要 Block:带有自动变量的匿名函数。 匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作...
    zweic阅读 505评论 0 2