Objective-C 中的 Block (1)

作者: Elenion | 来源:发表于2017-11-28 15:04 被阅读85次

    block 是 Objective-C 中的重要技术。本文主要是写了一点对 block 应用的探究。

    什么是 Block

    首先补充几个概念和解释帮助理解,大神们直接略过:

    1. block 本质是一个结构体。
    2. 结构体可以创建在栈上,堆上,还可以在全局区,因此就有对应的三种 block:栈上的,堆上的和全局的。

    如果 block 中没有捕捉变量,就是全局类型的, block 以及运行结果在编译期就能决定了,因此放在全局/静态区。

    如果 block 捕捉了变量,就必须依赖于其创建时的上下文,并被储存在栈内,栈弹出之后就会失效(失效并不代表着执行 block 一定会失败)。

    如果希望 block 的存活不再依赖于栈,就必须通过 copy 方法将 block 转化成堆类型的 block。堆类型的 block 保存在堆中,并带有引用计数,block 的引用计数和 Objective-C 对象的引用计数采用类似的原理,但并不是同一套实现,二者是有所区别的。

    Block 怎样捕获变量

    block 捕捉变量是通过复制,捕捉值类型的变量是复制值,捕捉引用类型的变量是通过复制指针,捕捉到的变量和指针在 block 内部的修改无法影响到 block 外部的值。可以在声明变量的时候加上 __block 标记。带有 __block 标记的变量在被捕捉后就可以被修改了。

    问题的产生

    Objective-C 是如何做到这一特性的呢?如果一个变量 foo 带有 __block 标记,并且被一个 block 捕捉,这时候在 block 内部修改 foo 的值会引起 block 外变量值的变化,不妨假设 block 创建了一个引用类型的变量指向了栈中的变量 foo 。不过想想都不可能,很明显这是不可靠地,因为程序运行过程中栈在不断变化,一旦 block 失去了当时栈的上下文,必然会产生野指针。

    对推测的验证

    所以我写了个 Demo ,用 block 捕获变量,来观察 block 是怎样操作的。

    XCode Version 8.2.1 (8C1002) ARC Objective-C

    #import "ViewController.h"typedef double (^SampleMultiplyBlockRef)();@interface ViewController ()@property (nonatomic, copy) SampleMultiplyBlockRef block;@end@implementation ViewController- (void)viewDidLoad {
      [super viewDidLoad];
      // Do any additional setup after loading the view, typically from a nib.
      __block double a = 10;
      __block double b = 20;
      // 变量初始化
      NSLog(@"Var a after init: %f, %p", a, &a);
      NSLog(@"Var b after init: %f, %p", b, &b);
      a = 25;
      a = 22;
      // 修改变量值
      NSLog(@"Var a after change: %f, %p", a, &a);
      NSLog(@"Var b after change: %f, %p", b, &b);
      SampleMultiplyBlockRef multiply = ^(){
        // block 中修改变量值前
        NSLog(@"Var a in block: %f, %p", a, &a);
        NSLog(@"Var b in block: %f, %p", b, &b);
        a = a + 1;
        b = b + 1;
        // block 中修改变量值后
        NSLog(@"Var a after change: %f, %p", a, &a);
        NSLog(@"Var b after change: %f, %p", b, &b);
        return a * b;
      };
      // 变量被捕捉后,block执行前
      NSLog(@"Var a out of block before execute: %f, %p", a, &a);
      NSLog(@"Var b out of block before execute: %f, %p", b, &b);
      // block 的地址
      NSLog(@"mulitply %p", &multiply);
      // block 的返回
      NSLog(@"Where is %f", multiply());
      // block 执行后
      NSLog(@"Var a out of block: %f, %p", a, &a);
      NSLog(@"Var b out of block: %f, %p", b, &b);
      // 储存到属性中(注意:类型是 copy)
      self.block = multiply;
      NSLog(@"mulitply %p", &multiply);}- (void)viewDidAppear:(BOOL)animated {
      [super viewDidAppear:animated];
      self.block();
      // 作为属性储存的 block 地址
      NSLog(@"mulitply %p", &_block);
    }
    @end
    

    Demo 比较简单,就是在 viewDidLoad 方法内创建 block 并作为属性储存下来,在 viewDidAppear 方法中执行 block。

    运行后我们一条一条的分析输出结果,主要关注值和内存地址:

    首先在方法内部创建 a 和 b 两个值类型变量。

    2017-01-13 09:24:17.648 Test Project[36246:1304073] Var a after init: 10.000000, 0x7fff5bbf9a482017-01-13 09:24:17.649 Test Project[36246:1304073] Var b after init: 20.000000, 0x7fff5bbf9a28
    

    之后在方法内部修改 a 和 b 的值,可以看到两个变量的值发生了变化,但是内存地址并没有变化(废话)。

    2017-01-13 09:24:17.649 Test Project[36246:1304073] Var a after change: 25.000000, 0x7fff5bbf9a482017-01-13 09:24:17.650 Test Project[36246:1304073] Var b after change: 22.000000, 0x7fff5bbf9a28
    

    block 声明完成,此时发现两变量的地址都发生了变化,应该是已经完成变量的捕获。

    2017-01-13 09:24:17.650 Test Project[36246:1304073] Var a out of block before execute: 25.000000, 0x60800002e2d82017-01-13 09:24:17.651 Test Project[36246:1304073] Var b out of block before execute: 22.000000, 0x60800002e338
    

    block 在所在栈内执行过程中,对两变量赋值前的情况。

    2017-01-13 09:24:17.652 Test Project[36246:1304073] Var a in block: 25.000000, 0x60800002e2d8
    2017-01-13 09:24:17.652 Test Project[36246:1304073] Var b in block: 22.000000, 0x60800002e338
    

    block 在所在栈内执行过程中,对两变量赋值后的情况。

    2017-01-13 09:24:17.653 Test Project[36246:1304073] Var a after change: 26.000000, 0x60800002e2d8
    2017-01-13 09:24:17.653 Test Project[36246:1304073] Var b after change: 23.000000, 0x60800002e338
    

    block 在所在的栈内执行完成后,两变量的情况。

    2017-01-13 09:24:17.654 Test Project[36246:1304073] Var a out of block: 26.000000, 0x60800002e2d8
    2017-01-13 09:24:17.654 Test Project[36246:1304073] Var b out of block: 23.000000, 0x60800002e338
    

    拷贝到堆中的 block 执行过程中,对两变量赋值前的情况。

    2017-01-13 09:24:17.666 Test Project[36246:1304073] Var a in block: 26.000000, 0x60800002e2d8
    2017-01-13 09:24:17.666 Test Project[36246:1304073] Var b in block: 23.000000, 0x60800002e338
    

    拷贝到堆中的 block 执行过程中,对两变量赋值后的情况。

    2017-01-13 09:24:17.667 Test Project[36246:1304073] Var a after change: 27.000000, 0x60800002e2d8
    2017-01-13 09:24:17.667 Test Project[36246:1304073] Var b after change: 24.000000, 0x60800002e338
    

    栈中刚声明后的 block 地址

    2017-01-13 09:24:17.651 Test Project[36246:1304073] mulitply 0x7fff5bbf99f8
    

    栈中执行后的 block 地址

    2017-01-13 09:24:17.656 Test Project[36246:1304073] mulitply 0x7fff5bbf99f8
    

    拷贝到堆中的 block 地址

    2017-01-13 09:24:17.667 Test Project[36246:1304073] mulitply 0x7fac94d06338
    

    结论

    这样看下来,在 block 捕获后,变量的地址就发生了变化,变量的存活也不再依赖于栈,因此应该是储存在堆区。
    block 复制到堆后,之前捕获的的变量地址依然没有变化,可以推断 block 中的变量值应该是指针指向了同一个地址。

    关注微信订阅号:iOS开发笔记本

    http://weixin.qq.com/r/Kji_plrEqwfUrR7F9204

    关注知乎专栏:iOS开发学记笔记

    https://zhuanlan.zhihu.com/iOSDevNote

    相关文章

      网友评论

        本文标题:Objective-C 中的 Block (1)

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