Block的内存管理以及变量Capture

一、 概述

总的来说,Block = 匿名函数 + Capture变量。

它的用途包括:

  1. 作为匿名函数使用,即把函数定义在使用到的地方,逻辑更加集中。
  2. Capture变量,可以capture Block定义所在的作用域内的变量,类似于保存上下文,Block在别处被调用时,好像具备Block定义所在的上下文一样可以正常执行,这些都是OC做的事情。而我们在使用Block的时候,当前作用域内的变量,都可以在Block中使用,不需要顾及当Block在别处被调用,当前上下文已经不存在的问题,提升开发效率。

二、Stack Block VS Malloc Block VS Global Block

Block是特殊的OC对象,是可以在Stack上创建的对象。在Stack上创建Block是为了运行效率而做的优化。

我们知道,一般来说,栈上变量的分配、回收、访问速度要高于堆上变量,一是栈上数据会频繁创建与销毁,创建与销毁的逻辑相对简单(从当前可用区域中划分一块儿给变量,栈向下增长,可用空间的起始地址变小一点),而堆上创建销毁逻辑相对复杂(有很多算法,大小最适合的空间还是第一块可用的空间等等),一是栈帧数据会被频繁访问、修改,数据很大可能性已经映射到处理器缓存中,二是堆数据是线程共享数据,访问堆上数据可能会有线程同步开销,当然这不是一定的。

所以如果一个Block只在当前作用域内使用,可以在Stack上创建,速度更快。

使用StackBlock也有要小心的地方,StackBlock可以理解为局部变量,将会随栈帧的销毁而销毁,retain不起作用,因为StackBlock是栈变量,它的回收不依赖引用计数机制。

如果需要可以调用Block的copy方法,把StackBlock变成MallocBlock,MallocBlock的内存位于堆上。

Global Block
没有Capture任何变量的Block是Global Block。Global Block存储在特殊的静态区域,全局唯一,不会被dispose,不会被copy(并不了解内幕,MallocBlock的copy也并没有新开辟一块儿内存给新的实例)。

三、 __block

简单来说,

  • 使用__block修饰变量,block将capture变量的地址。
  • 没有使用__block修饰变量,block仅capture变量的值。

在block内,不能修改没有使用__block修饰的capture变量。原因是修改是无效的。为了避免难以发现的bug,编译器会报错,如果真的想要修改,就使用__block修饰变量。因为没有使用__block修饰变量,block仅capture变量的值。在block内,修改没有使用__block修饰的capture变量,类似于下面这种情形。

- (void)test:(id) foo{
    foo = nil;
}

...
Foo *foo = [Foo new];
NSLog(@"%@", foo);
[self test:foo];
NSLog(@"%@", foo);
...

//output
2017-05-07 10:05:01.887 TestBlock[53766:7428855] <Foo: 0x618000005800>
2017-05-07 10:05:01.887 TestBlock[53766:7428855] <Foo: 0x618000005800>

(此外,使用__block修饰的变量在block内外的修改对彼此都可见,因为都dereference了。)

此外,使用__block修饰变量会产生一次relocate,变量将被relocate到堆上。验证如下:

...
    int x = 0;

    __block Foo *foo = [Foo new];

    NSLog(@"%p", &x);
    NSLog(@"%p", &foo);

    void(^block1)(void) = ^(){
        NSLog(@"%@", foo);
    };

    NSLog(@"%p", &foo);

...

//output
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x7fff52a7fa3c
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x7fff52a7fa30
2017-05-07 10:21:10.288 TestBlock[53977:7470636] 0x618000248398

(从中,我们也能得知,relocate不是发生在变量声明时,而是发生在变量在block中被使用时。这么做是合理的,使用__block修饰而实际并未在任何block中使用的变量,是不需要relocate的。)

为什么需要relocate?

简单来说,因为栈是临时的,block可能生命周期很长。

block的生命周期可能长于当前栈帧(比如使用分发到GCD执行)。也就是说,block可能在栈帧被销毁之后某个时间执行。这要求block不能capture一个栈上的地址。一但capture,并且使用时栈帧已被销毁,就会发生bad_access错误。

猜你喜欢

转载自blog.csdn.net/fly1183989782/article/details/71325709