美文网首页
《Objective-C高级编程 iOS与OS X多线程与内存管

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

作者: 我才是臭吉吉 | 来源:发表于2019-01-12 18:37 被阅读11次

    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”。

    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对象时,也要注意引用循环问题。

    相关文章

      网友评论

          本文标题:《Objective-C高级编程 iOS与OS X多线程与内存管

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