美文网首页iOS开发文章面试iOS开发系列
iOS中__block 关键字的底层实现原理

iOS中__block 关键字的底层实现原理

作者: iOS程序犭袁 | 来源:发表于2016-05-18 15:40 被阅读8387次

《iOS面试题集锦(附答案)》 中有这样一道题目:
在block内如何修改block外部变量?(38题)答案如下:

默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上 __block 来让其写操作生效,示例代码如下:

   __block int a = 0;
   void (^foo)(void) = ^{ 
       a = 1; 
   };
   foo(); 
   //这里,a的值被修改为1

这是 微博@唐巧_boy的《iOS开发进阶》中的第11.2.3章节中的描述。你同样可以在面试中这样回答,但你并没有答到“点子上”。真正的原因,并没有书这本书里写的这么“神奇”,而且这种说法也有点牵强。面试官肯定会追问“为什么写操作就生效了?”真正的原因是这样的:

我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

Block不允许修改外部变量的值Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。

我们可以打印下内存地址来进行验证:

   __block int a = 0;
   NSLog(@"定义前:%p", &a);         //栈区
   void (^foo)(void) = ^{
       a = 1;
       NSLog(@"block内部:%p", &a);    //堆区
   };
   NSLog(@"定义后:%p", &a);         //堆区
   foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8

“定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。

那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

  1. 定义后前:6171559672
  2. block内部:5732708296
  3. 定义后后:5732708296

中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:

以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。

   NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
   NSLog(@"\\n 定以前:------------------------------------\\n\\
         a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
   void (^foo)(void) = ^{
       a.string = @"Jerry";
       NSLog(@"\\n block内部:------------------------------------\\n\\
        a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
       a = [NSMutableString stringWithString:@"William"];
   };
   foo();
   NSLog(@"\\n 定以后:------------------------------------\\n\\
         a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
   
enter image description here

这里的a已经由基本数据类型,变成了对象类型。对象类型,block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址,所以在上面的示例代码中,block体内修改的实际是a指向的堆中的内容。

但如果我们尝试像上面图片中的65行那样做,结果会编译不通过,那是因为此时你在修改的就不是堆中的内容,而是栈中的内容。

上文已经说过:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。

相关文章

网友评论

  • Hanser0503:写的挺好的,排版也很棒!!加油
  • wokenshin:__block 是修复局部变量的情况, 在代码块中 修改 类的成员变量为什么不需要修饰 就可以直接赋值呢?
  • asaBoat:最后一张图,block内部情况下,两个指针都应该是在堆中吧,地址很相似
  • 不留名的黄子嘉:貌似是值传递和指针传递的问题 加__block会把外面的指针重新指向 不加的话是捕获一个类型变量的临时指针.. c++的东西?
  • qBryant:受益匪浅。。。喜欢这种原理性的文章。。。👍!
  • PGOne爱吃饺子:楼主 你好 为什么不能修改栈中指针的地址(是因为栈中的地址指向堆中的对象,如果栈中的地址被改变了,就找不到了堆中的对象了,我这样的理解是对的么)
  • 5a8a9a3b9f54:[看着你],点击[ http://pinyin.cn/e33191 ]查看表情
  • 丶丶夏天:“这里的a已经由基本数据类型,变成了对象类型。对象类型,block会对对象类型的指针进行copy,copy到堆中”这里说了copy到堆中,为什么代码上后面的注释还是说在栈中呢
  • 鼻毛长长:我觉得该思考的是为什么Block不允许修改外部变量的值?如果Block允许修改外部变量的值会造成什么样的后果?如果知道了为什么了,我想也就理解了。而现在,大多数人都在思考是什么。
  • valentizx:看了博文 懂了点 看了开头的评论 基本都懂了 看到最后 又不懂了
  • d06445006fa3:大神怎么知道__block声明是放到堆里面而不是全局静态区?
  • kosser小屋:@zhnnnnn 那为什么栈上的数据不能修改呢?栈上的内存归os分配回收,不代表不能修改吧,修改了相当于就是改变了内存地址么?这样倒能说的通…
    049a6d94ddba:@zhnnnnn 你说的没有用__block修饰,a在block内也在堆上,block前后都在栈上,那是因为你用的arc吧。我感觉要解释清楚,得从mrc下开始捋顺。mrc下:一个局部int a = 1;在block前后都是在栈上的,如果不加__block,那么这个时候block内部的a地址还是在栈上,但是已经不是原来的地址了,也就是说他俩看着一样,实际上是不一样。而如果加了__block,那么发现3处的地址都是一样的。我觉得Apple是为了避免开发者混乱才这样规定的(因为你在里面捕获了,外面修改了,你觉得里面的也应该修改,至少从代码表面看应该如此,但是其实他们不是同一个东西)。那么这样又引出另外一个问题,为什么不用引用传递,我想改就改,不改就拉倒?我在网上看到过解释,说是为了避免block作为参数传递的时候,这个局部变量已经释放了,那么问题来了,block本身也在栈中,作为参数传递的时候肯定得copy啊,copy后到堆里,变量也到堆里,和原先栈中的变量没有关系了,还怕它被释放吗?为什么不用引用传递这一点是我一直想不通的。下面是我自己总结的,你看对不:不管mrc还是arc,不管用不用__block,block中的变量地址已经变了.区别仅仅是加了__block后,block结构内多了个结构体对象,block运行后,所有这个变量的读取都是通过__fowarding去读取,堆上有就取堆,堆上没有就取栈。
    kosser小屋:召唤大神吧…
    d9557f883fd8:@kosser小屋 按照楼主上面的思路我刚去试了一下,没有用__block修饰的int a 在block里面的内存也是在堆区,只是block 运行前后的a 的内存都是一样的在栈区。那么感觉说不通了啊。如果按照上面的思路那么a 也是可以修改的啊。。。。我再去找找资料看看。。。。
  • 知傲:从另一个角度看,__block是给这个变量包了一层,变成另一个对象的成员,因此能够修改变量
  • d9557f883fd8:自己写个demo看了半天,以前的理解错了…block内部复制外部变量没有在堆上开辟空间啊,只是在栈上开辟了空间指向了同一堆地址。。没有_block修饰的不能操作栈上面的空间。但是可以修改堆地址值,所以文中的string可以修改,但是直接赋值操作指针会报错。。。如果用_block修饰的情况开始也不会在堆上开辟新的内存地址,而且在block里面可以操作栈上的指针~
    d9557f883fd8:@zhnnnnn 刚才还一直想不明白,为什么要复制的时候要重新在栈上开辟空间。想明白了如果不开空间,那变量在栈上面过了作业域随时会被释放。。但是在block里面你会一直不想它释放,所以就新开开辟一个自己持有咯?这样理解对么
  • kosser小屋:@iOS程序犭袁 还有一个问题,为什么在block外部的基本数据类型在block里面修改时需要加__block,
    d9557f883fd8:@zhnnnnn 应该是不能改,你值还是能拿的
    d9557f883fd8:@kosser小屋 基本数据类型放在栈上面的,不用block修饰就不能操作啊
  • hhhhxy:后面的例子,代码和图好像是一样的吧,代码中应该把a = [NSMutableString stringWithString:@"William"];注释掉
  • kosser小屋:a = [NSMutableString stringWithString:@"William"]; 这句话为什么修改的是栈内存呢,我认为还是a指针所对应的堆内存,只要block外部变量所对应的堆内存地址发生改变,就应该使用__block来修饰。
    a26a820711da:我来补充一下,不知道说的对不对,仅供参考,因为栈上面的指针本身也是有地址的,指针在栈中的地址没有变化,但是指针指向的堆上的地址发生了变化,所以综合来看,指针还是发生了改变。所以在没有_block修饰的情况,a = [NSMutableString stringWithString:@"William"]; 会报错。
    MrGan先生:@iOS程序犭袁 a = [NSMutableString stringWithString:@"William"];应该是指针a指向的堆内存地址发生了变更,但a指针在栈中的指针地址没有变
    iOS程序犭袁:@kosser小屋 你理解的是对的,但堆内存的地址记录在哪里?不还是记录在栈里吗?修改了a在堆里的内存地址,不就也是在修改栈内存?
  • 小凡凡520:没有看懂
  • hauibojek:好文,看的我醍醐灌顶,茅塞顿开
  • xxttw:很棒
  • Colin_狂奔的蚂蚁:讲的很好,继续发表好文章
  • LaiYoung_:看懂了居然:smile:
    TimberTang:@LaiYoung_ :smile::smile::smile:
  • 萌小菜:最后两个图不是一样的吗?
  • 萌小菜:能力有限, 看不太明白
  • 翘楚iOS9:很清晰

本文标题:iOS中__block 关键字的底层实现原理

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