美文网首页
Block截获对象的内存思考

Block截获对象的内存思考

作者: X_L_F | 来源:发表于2020-04-05 00:16 被阅读0次

    在讨论Block截获对象的内存变化前。先看一下Block截获对象时,截获的是什么。

    下面举个例子

    // main.m
    #import <Foundation/Foundation.h>
    typedef void(^blk)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            blk block = ^{
              NSLog(@"%@", array)   // 内部截获了array对象
            };
            block();
        }
        return 0;
    }
    

    这样看不够直观,通过clang将Objective-C代码转换为C++源码。

    步骤如下:

    1.找到对应的文件所在

    2.在终端中输入

    clang -rewrite-objc main.m
    

    3.得到main.cpp

    找到关键点__main_block_impl_0。就是对应上述blk类型变量的对应结构体

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSMutableArray *array;        // 成员变量,用来保存外部传入的array指针值
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以明显得看到结构体内部含有成员变量,NSMutableArray *array,就是用来存储外部传入的array指针值。

    由此可证最开始的猜想:Block截获对象时,保存的指向对象的指针值。

    Block截获对象时的,对象的内存变量

    确切来说,是对象引用计数的变化。

    接着针对上面的例子,获得截获对象的引用计数。

    // main.m
    #import <Foundation/Foundation.h>
    typedef void(^blk)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            // 位置1
            NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));           
            // 位置2
            blk block = ^{
                NSLog(@"%@", array);
            };      
            // 位置3  
            NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));           
        }
        return 0;
    }
    

    下面来看看输出结果

    count:1
    count:3
    

    位置1时,count为1不难理解,此时指向对象的只有一个array指针。而过了位置2之后,count变成了3。

    这里有两个疑惑点。

    1.block没有执行的情况下,对象已经被截获了?

    2.count为什么是3,而不是2?

    一个一个来解答。

    1.block没有执行的情况下,对象已经被截获了?

    通过clang的源码转换如下。

    // main.cpp
    // 简化结果
    int main(int argc, const char * argv[]) {
        blk block = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344);
        return 0;
    }
    

    可以看到将__main_block_impl_0赋值给了block变量,而__main_block_impl_0正是之前例子提到的Block变量转化后的结构体。结构体内部有一个成员变量专门用来存对象指针的值。因此只要定了Block,对象就已经被截获了。

    2.count为什么是3,而不是2?

    通过第一个解答,得知此时array对象已经被截获。那么引用计数+1。但为什么多了1。

    原因是,在ARC的情况下,有以下4中情况,会将Block从栈空间复制到堆空间

    1. Block变量使用copy
    2. Block变量被__strong修饰符修饰
    3. 方法返回Block变量
    4. Cococa框架中带“usingBlock”的方法及GCD中的API

    在ARC中,指向OC对象的变量没有明确写所有权修饰符,则修饰符默认为__strong。(实际上Block就是一种OC对象)

    blk block = ^{
                NSLog(@"%@", array);
            };
    

    等价于

    blk __strong block = ^{
                NSLog(@"%@", array);
            };
    

    根据规则2,“Block变量被__strong修饰符修饰”时,会将Block从栈空间复制到堆空间。

    则此时内存中有两份Block,且两份Block又各自截获了array对象。因此此时指向对象的引用计数为3(array变量+栈Block截获+堆Block截获)。

    那么,如何使count变为2呢?

    思路就是只保留栈空间的Block。

    // main.m
    #import <Foundation/Foundation.h>
    typedef void(^blk)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));           
            
            blk __unsafe_unretained block = ^{
                NSLog(@"%@", array);
            };          // 使用__unsafe_unretained修饰
            NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));           
        }
        return 0;
    }
    

    结果

    count:1
    count:2
    

    通过__unsafe_unretained进行修饰,即可只保留栈空间的Block。__unsafe_unretained作用于weak相似,但是不会自动置为nil。

    备注:

    不用weak的原因是因为Block刚生成对象,赋值的瞬间就被释放了。

    相关文章

      网友评论

          本文标题:Block截获对象的内存思考

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