Block截获对象的内存思考

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

下面举个例子

// main.m
#import 
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 
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 
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截获对象的内存思考)