iOS进阶—— Block

移动开发 iOS
说明还是有很多 iOS 的朋友对于 Block 并没有透彻理解。本篇博文会对 Block 进行详细的解说。

花几分钟时间看下面三个小题目,写下你的答案。 

 

 

 

这个三个小题目,我在整理此片博文之前给了三位朋友去解答,***的结果,除了一位朋友 3 题全部正确,其他两个朋友均只答中 1 题。

说明还是有很多 iOS 的朋友对于 Block 并没有透彻理解。本篇博文会对 Block 进行详细的解说。

1 Block 使用的简单规则

先了解简单规则,再去分析原理和实现:

Block 中,Block 表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。

修饰为 __block 的变量,在捕获时,获取的不再是瞬间值。

至于 Why,后面将会继续说。

2 Block 的实现

Block 是带有自动变量(局部变量)的匿名函数。

Block 表达式很简单,总体可以描述为:『^ 返回值类型 参数列表 表达式』。

但是 Block 并不是 Objective-C 中才有的语法,这是怎么一回事?

clang 编译器提供给程序员了解 Objective-C 背后机制的方法,通过 clang 的转换可以看到 Block 的实现原理。

通过 clang -rewrite-objc yourfile.m clang 将会把 Objective-C 的代码转换成 C 语言的代码。

2.1 Block 基本实现剖析

用 Xcode 创建 Command Line 项目,写如下代码:

  1. int main(int argc, const char * argv[]) { 
  2.  
  3. void (^blk)(void) = ^{NSLog(@"Block")}; 
  4.  
  5. blk(); 
  6.  
  7. return 0; 
  8.  
  9.  

用 clang 转换: 

 

 

 

以上是转换后的代码,不要方,一段一段看。

可以看到,Block 内部的内容,被转换成了一个普通的静态函数 __main_func_0。

再看其他部分:

main.cpp __block_impl:

  1. struct __block_impl { 
  2.  
  3. void *isa; 
  4.  
  5. int Flags; 
  6.  
  7. int Reserved; 
  8.  
  9. void *FuncPtr; 
  10.  
  11. };  

__block_impl 结构体包括了一些标志、今后版本升级预留的变量、函数指针。

main.cpp __main_block_desc_0:

  1. static struct __main_block_desc_0 { 
  2.  
  3. size_t reserved; 
  4.  
  5. size_t Block_size; 
  6.  
  7. } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};  

__main_block_desc_0 结构体包括了今后版本升级预留的变量、block 大小。

main.cpp __main_block_impl_0:

__main_block_impl_0 结构体含有两个成员变量,分别是 __block_impl 和 __main_block_desc_0实例变量。

此外,还含有一个构造方法。该构造方法在 main 函数中被如下调用:

main.cpp __main_block_impl_0 构造函数的调用:

  1. void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
  2.  
  3. &__main_block_desc_0_DATA));  

去掉各种强制转换,做简化:

main.cpp __main_block_impl_0 构造函数的调用 简化:

  1. struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); 
  2.  
  3. struct __main_block_impl_0 *blk = &tmp; 

 

以上代码即:将 __main_block_impl_0 结构体实例的指针,赋值给 __main_block_impl_0 结构体指针类型的变量 blk。也就是我们最初的结构体定义:

  1. void (^blk)(void) = ^{NSLog(@"Block");}; 

另外,main 函数中还有另外一段:

  1. ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); 

去掉各种转换:

  1. (*blk->impl.FuncPtr)(blk); 

实际就是最初的:

  1. blk(); 

本节所有代码在 block_implementation (https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory/ObjcMemory-Test-Code/block_implementation)中

2.2 Block 截获外部变量瞬间值的实现剖析

2.1 中对最简单的 无参数 Block 声明、调用 进行了 clang 转换。接下来再看一段『截获自动变量』的代码(可以使用命令 clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 main.m):

  1. int main(int argc, const char * argv[]) {  
  2.   
  3.  
  4. int val = 10; 
  5.  
  6. const char *fmt = "val = %d\n"
  7.  
  8. void (^blk)(void) = ^{printf(fmt, val);};  
  9.   
  10.  
  11. val = 2; 
  12.  
  13. fmt = "These values were changed, val = %d\n" 
  14.   
  15.  
  16. blk();  
  17.   
  18.  
  19. return 0; 
  20.  
  21.  

clang 转换之后: 

 

 

 

和 2.1 节中的转换代码对比,可以发现多了一些代码。

首先,__main_block_impl_0 多了一个变量 val,并在构造函数的参数中加入了 val 的赋值:

main.cpp __main_block_impl_0:

  1. struct __main_block_impl_0 { 
  2.  
  3. struct __block_impl impl; 
  4.  
  5. struct __main_block_desc_0* Desc
  6.  
  7. const char *fmt; 
  8.  
  9. int val; 
  10.  
  11. __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) { 
  12.  
  13. impl.isa = &_NSConcreteStackBlock; 
  14.  
  15. impl.Flags = flags; 
  16.  
  17. impl.FuncPtr = fp; 
  18.  
  19. Desc = desc
  20.  
  21.  
  22. };  

而在 main 函数中,对 Block 的声明变为此句:

main.cpp __main_block_impl_0 构造函数的调用:

  1. void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)); 

去掉转换:

main.cpp __main_block_impl_0 构造函数的调用 简化:

  1. struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, val); 
  2.  
  3. struct __main_block_impl_0 *blk = &tmp;  

_所以,在 Block 被声明时,Block 已经将 val 作为 __main_block_impl_0 的内部变量保存下来了。无论在在声明之后怎样更改 val 的值,都不会影响,Block 调用时访问的内部 val 值。这就是 Block 捕获变量瞬间值的原理。_

本节所有代码在 EX05 中

2.3 __block 变量的访问实现剖析

我们知道,Block 中能够读取,但是不能更改一个局部变量,如果去更改,Xcode 会提示你无法在 Block 内部更改变量。

Block 内部只是对局部变量只读,但是 Block 能读写以下几种变量:

  1. 静态变量
  2. 静态全局变量
  3. 全局变量

也就是说以下代码是没有问题的:

  1. int global_val = 1; 
  2.  
  3. static int static_global_val = 2; 
  4.   
  5.  
  6. int main(int argc, const char * argv[]) { 
  7.  
  8. static int static_val = 3; 
  9.   
  10.  
  11. void (^blk)(void) = ^ { 
  12.  
  13. global_val = 1 * 2; 
  14.  
  15. static_global_val = 2 * 2; 
  16.  
  17. static_val = 3 * 2; 
  18.  
  19. }      
  20.  
  21. return 0; 
  22.  
  23.  

如果想在 Block 内部写局部变量,需要对访问的局部变量增加 __block 修饰。

__block 修饰符其实类似于 C 语言中 static、auto、register 修饰符。用于指定将变量值设置到哪个存储域中。

具体 __block 之后究竟做了哪些变化我们可以写代码测试:

EX07:

  1. int main(int argc, const char * argv[]) {  
  2.   
  3.  
  4. __block int val = 10; 
  5.  
  6. void (^blk)(void) = ^{val = 1;};  
  7.   
  8.  
  9. return 0; 
  10.  
  11.  

clang 转换之后: 

 

 

 

跟 2.2 对比,似乎又加了非常代码。发现多了两个结构体。

main.cpp __Block_byref_val_0:

  1. struct __Block_byref_val_0 { 
  2.  
  3. void *__isa; 
  4.  
  5. __Block_byref_val_0 *__forwarding; 
  6.  
  7. int __flags; 
  8.  
  9. int __size; 
  10.  
  11. int val; 
  12.  
  13. };  

很惊奇的发现,block 类型的 val 变成了结构体 Block_byref_val_0的实例。这个实例内,包含了isa指针、一个标志位flags、一个记录大小的size。最最重要的,多了一个forwarding指针和val 变量。这是怎么回事?

在 main 函数部分,实例化了该结构体:

main.cpp main.m 部分:

  1. __Block_byref_val_0 val = {(void*)0, 
  2.  
  3. (__Block_byref_val_0 *)&val, 
  4.  
  5. 0, 
  6.  
  7. sizeof(__Block_byref_val_0), 
  8.  
  9. 10};  

我们可以看出该结构体对象初始化时:

  1. __forwarding 指向了结构体实例本身在内存中的地址
  2. val = 10

而在 main 函数中,val = 1 这句赋值语句变成了:

main.cpp val = 1; 对应的函数

  1. (val->__forwarding->val) = 1; 

这里就可以看出其精髓,val = 1,实际上更改的是 __Block_byref_val_0 结构体实例 val 中的 __forwarding 指针(也就是本身)指向的 val 变量。 

 

 

 

而对 val 访问也是如此。你可以理解为通过取地址改变变量的值,这和 C 语言中取地址改变变量类似。

所以,声明 block 的变量可以被改变。至于 forwarding 的其他巨大作用,会继续分析。

本节代码在 EX05 中

3 Block 的存储域

Block 有三种类型,分别是:

  1. __NSConcreteStackBlock ————————栈中
  2. __NSConcreteGlobalBlock ————————数据区域中
  3. __NSConcreteMallocBlock ————————堆中

__NSConcreteGlobalBlock 出现的地方有:

  1. 设置全局变量的地方有 Block 语法时
  2. Block 语法的表达式中不使用任何外部变量时

设置在栈上的 Block,如果所属的变量作用域结束,Block 就会被废弃。如果其中用到了 block,block 所属的变量作用域结束也会被废弃。

为了解决这个问题,Block 在必要的时候就需要从栈中移到堆中。ARC 有效时,很多情况下,编译器会帮助完成 Block 的 copy,但很多情况下,我们需要手动 copy Block。

对不同存储域的 Block copy 时,影响如下: 

 

 

 

copy 时,对访问到的 __block 类型对象影响如下: 

 

 

 

此时可以看出 __forwarding 的巨大作用——无论 Block 此时在堆中还是在栈中,由于 __forwarding 指向局部变量转换成的结构体实例的真是地址,所以都能确保正确的访问。

具体的来说:

  1. 当 block 变量被一个 Block 使用时,Block 从栈复制到堆,block 变量也会被复制到,并被该 Block 持有。
  2. 在 block 变量被多个 Block 使用时,在任何一个 Block 从栈复制到堆时, block 变量也会被复制到堆,并被该 Block 持有。但由于 __forwarding 指针的存在,无论 block 变量和 Block 在不在同一个存储域,都可以正确的访问 block 变量。
  3. 如果堆上的 Block 被废弃,那么它所使用的 __block 变量也会被释放。 

 

 

 

前面说到编译器会帮助完成一些 Block 的 copy,也有手动 copy Block。那么 Block 被复制到堆上的情况有(此段摘自于『Objective-C高级编程 iOS与OS X多线程和内存管理』):

  1. 调用 Block 的 copy 方法时
  2. Block 作为返回值时
  3. 将 Block 赋值给附有 __strong 修饰符的成员变量时(id类型或 Block 类型)时
  4. 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时

4 Block 循环引用

Block 循环引用,是在编程中非常常见的问题,甚至很多时候,我们并不知道发生了循环引用,直到我们突然某一天发现『怎么这个对象没有调用 delloc』,才意识到有问题存在。

在『Block 存储域』中也说明了 Block 在 copy 后对 __block 对象会 retain 一次。

那么对于如下情况就会发生循环引用: 

  1. block_retain_cycle: 
  2.  
  3.  
  4. @interface MyObject : NSObject  
  5.   
  6.  
  7. @property (nonatomic, copy) blk_t blk; 
  8.  
  9. @property (nonatomic, strong) NSObject *obj;  
  10.   
  11.  
  12. @end  
  13.   
  14.  
  15. @implementation MyObject  
  16.   
  17.  
  18. - (instancetype)init { 
  19.  
  20. self = [super init]; 
  21.  
  22. _blk = ^{NSLog(@"self = %@", self);}; 
  23.  
  24. return self; 
  25.  
  26.  
  27.   
  28.  
  29. - (void)dealloc { 
  30.  
  31. NSLog(@"%@ dealloc", self.class); 
  32.  
  33.  
  34.   
  35.  
  36. @end  
  37.   
  38.  
  39. int main(int argc, const char * argv[]) { 
  40.  
  41. id myobj = [[MyObject alloc] init]; 
  42.  
  43. NSLog(@"%@", myobj); 
  44.  
  45. return 0; 
  46.  
  47.  

由于 self -> blk,blk -> self,双方都无法释放。

但要注意的是,对于以下情况,同样会发生循环引用:

  1. block_retain_cycle 
  2.   
  3.  
  4. @interface MyObject : NSObject 
  5.   
  6.  
  7. @property (nonatomic, copy) blk_t blk; 
  8.   
  9.  
  10. // 下面是多加的一句 
  11.  
  12. @property (nonatomic, strong) NSObject *obj; 
  13.   
  14.  
  15. @end 
  16.   
  17.  
  18. @implementation MyObject 
  19.   
  20.  
  21. - (instancetype)init { 
  22.  
  23. self = [super init]; 
  24.   
  25.  
  26. // 下面是多加的一句 
  27.  
  28. _blk = ^{NSLog(@"self = %@", _obj);}; 
  29.   
  30.  
  31. return self; 
  32.  
  33.   
  34.  
  35. - (void)dealloc { 
  36.  
  37. NSLog(@"%@ dealloc", self.class); 
  38.  
  39.   
  40.  
  41. @end 
  42.   
  43.  
  44. int main(int argc, const char * argv[]) { 
  45.  
  46. id myobj = [[MyObject alloc] init]; 
  47.  
  48. NSLog(@"%@", myobj); 
  49.  
  50. return 0; 
  51.  
  52.  

这是由于 self -> obj,self -> blk,blk -> obj。这种情况是非常容易被忽视的。

5 重审问题

我们再来看看最初的几个小题目: 

 

 

 

***题:

由于 Block 捕获瞬间值,所以输出为 in block val = 0

第二题:

由于 val 为 __block,外部更改会影响到内部访问,所以输出为 in block val = 1

第三题:

和第二题类似,val = 1 能影响到 Block 内部访问,所以先输出 in block val = 1,之后在 Block 内部更改 val 值,再次访问时输出 after block val = 2。

Other

我写这篇文章是在我阅读了『Objective-C高级编程 iOS与OS X多线程和内存管理』一书之后,博文中也有很内容源于『Objective-C高级编程 iOS与OS X多线程和内存管理』。

非常向大家推荐此书。这本书里记录了关于 iOS 内存管理的深入内容。但要注意的是,此书中的多处知识点并不是很详细,需要你以拓展的心态去学习。在有解释不详细的地方,自己主动去探索,去拓展,找更多的资料,***,你会发现你对 iOS 内存管理有了更多的深入的理解。

对于文章中的测试代码,全部在(https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory)。

责任编辑:庞桂玉 来源: iOS大全
相关推荐

2013-06-04 15:41:31

iOS开发移动开发block

2013-07-19 12:52:50

iOS中BlockiOS开发学习

2017-03-07 10:15:35

iOS内存管理开发

2011-08-08 18:11:45

IOS 4Block UIActionShe

2013-07-19 14:00:13

iOS中BlockiOS开发学习

2013-07-19 14:35:59

iOS中BlockiOS开发学习

2017-01-19 19:07:28

iOS进阶性能优化

2013-07-19 13:16:26

iOS中BlockiOS开发学习内存管理

2014-07-30 11:12:09

block

2010-09-16 09:13:09

CSS display

2016-03-07 09:09:35

blockios开发实践

2014-07-31 16:47:10

block

2015-09-18 09:12:08

2011-07-29 16:16:30

Objective-c block

2010-04-07 16:54:55

Oracle性能

2010-09-14 15:32:51

CSSdisplay:inl

2010-09-03 10:18:06

CSSdisplay:inl

2010-09-03 12:55:15

CSSblockinline

2013-07-21 18:09:21

iOS开发ASIHttpRequ创建和执行reques

2010-09-09 15:54:00

blockinlineCSS
点赞
收藏

51CTO技术栈公众号