《Objective-C高级编程 iOS与OS X多线程与内存管理》13

Blocks篇:4.Blocks的存储域

在上一节中我们知道,在Block捕获不同种类的变量时,生成的Block对象的类型(isa指针)分为三种:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

此三种类型的Block对象分别存储在栈区、全局(数据区)和堆区

我们知道,由于Block对象的函数体定义在Block实例化的生命周期外部,故其执行时早已不在原作用域内。况且,由于在函数中,定义的Block对象也是局部变量,超出作用域也会被自动回收。所以,要保证Block超出原作用域仍然可以存在的方式,就是将其转化为全局Block,或者复制到堆内存中,这样才可以保证其内存可控并正确执行Block的函数。在ARC环境下,LLVM在编译期已经可以在绝大多数情况下正确处理这种情况。通过测试,编码时定义的局部Block变量(即_NSConcreteStackBlock对象),在运行时可以得到如下结果:

捕获变量情况 运行期生成的Block对象
_NSConcreteGlobalBlock
全局或静态变量 _NSConcreteGlobalBlock
普通局部变量 _NSConcreteMallocBlock

但是,发现在一种情况下,ARC不会自动处理,需要我们对Block对象进行手动转换。

1.ARC下的Blocks陷阱

先看代码:

// main.m

typedef void(^VoidBlock)(void);

/** 返回包含Block对象的集合的函数 */
NSArray *getBlocksArray () {
    int myVal = 2;
    // 内部的Block对象均为__NSConcreteStackBlock对象
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"block1~%d", myVal);},
            ^{NSLog(@"block2~%d", myVal);},
            nil
            ];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 获取该数组
        NSArray *blocksArray = getBlocksArray();
        // 取出Block对象
        VoidBlock voidBlock = blocksArray[0];
        // 执行
        voidBlock();
    }
    return 0;
}

执行情况,我们可以直接得到个漂亮的“EXC_BAD_ACCESS”。

450996-4062d1034bd79752.jpg
Block在集合中的坑.jpg

在图中已经看出,这种情况下,编译器并没有将捕获有变量的Block拷贝至堆中。故在准备执行时,Block对象已经被释放(第一个被转成__NSConcreteMallocBlock的原因是由于NSArray的init方法会自动保留对象,进而发生了Block的copy操作)。当执行完毕后,由于数组对象的释放,在对其内部元素依次释放时访问了野指针,导致崩溃。

所以,在集合中使用Block对象时,为了保证其安全性,我们可以有两种方式:

  1. 手动将Block复制到堆中:
NSArray *getBlocksArray () {
    int myVal = 2;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"block1~%d", myVal);} copy],
            [^{NSLog(@"block2~%d", myVal);} copy],
            nil
            ];
}

由于Block是OC对象,故对其发送copy消息可以直接将其转换为__NSConcreteMallocBlock对象。

  1. 在初始化Block时,利用ARC的特性,对Block进行显式声明,以获取“__strong”修饰的Block,自动生成__NSConcreteMallocBlock对象
NSArray *getBlocksArray () {
    int myVal = 2;
    
    // 生成了强引用Block变量,自动分配到了堆内存中
    VoidBlock block1 = ^{NSLog(@"block1~%d", myVal);};
    VoidBlock block2 = ^{NSLog(@"block2~%d", myVal);};

    return [[NSArray alloc] initWithObjects:
            block1,
            block2,
            nil
            ];
}

注意:例外情况

在系统带有Block参数的API中(如GCD或是Animation相关等等),无需手动对Block进行复制(其内部实现已经包含了复制操作)。

2.Blocks的保留操作

2.1 Blocks的保留解析

我们知道,在生成__strong修饰的Block对象时,其实隐含的对生成的对象进行了retain操作。此操作实际为:

VoidBlock blockObj = ...;
// 对Block进行保留操作
objc_retainBlock(blockObj);
...

在NSObject.mm中,我们找到了此方法的实现:

// NSObject.mm

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

因此,对Block进行retain其实也就是进行了copy操作,进而在堆上生成了Block

2.2 Blocks的copy操作

现在,我们知道了对栈中的Block进行复制或保留操作,会在堆内存上生成对应的Block对象。但对于其他两者呢?

copy对应Block 效果
_NSConcreteGlobalBlock 无作用
_NSConcreteMallocBlock 引用计数 + 1

对于堆内存中的Block对象,其实是遵循了引用计数的内存管理方式。因此,在使用Block对象时,也要注意引用循环问题。

猜你喜欢

转载自blog.csdn.net/weixin_34390996/article/details/87569694
今日推荐