Block的内存管理以及变量Capture

##一、 概述

    总的来说,Block = 匿名函数 + Capture 变量。和 Java 中的 Lambda 表达式类似。它的用途包括:

  1. 作为匿名函数使用,即把函数定义在使用到的地方,逻辑更加集中。RAC 就有很多这样的应用。
  2. Capture 变量,可以 Capture Block 定义所在的作用域内的变量,类似于保存上下文。之后,Block 在别处被调用时,就好像 Block 还在原本上下文一样,可以正常执行。这些都是 OC 自动做的事情。而我们在定义 Block 的时候,当前作用域内的变量,都可以在 Block 中使用。不需要顾及当 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 的内存位于堆上。

    没有 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 错误。

##四、 block 的 c 实现(待补充)

参考:iOS 开发进阶 by 唐巧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值