iOS Block学习笔记(八) -- Block捕获对象类型变量

前面的内容都是使用Block截取的自动变量, 如果我们的Block截取的是对�象, 其实情况基本一样, 除了copy和dispose略有区别.

int main(int argc, const char * argv[]) {
    NSMutableArray *array = [NSMutableArray array];
    Block block = ^(id obj){
        [array addObject:obj];
    }; // 只要赋值以后, block 就从栈上 -> copy 到堆上
    struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;

    block([NSObject new]);
    block([NSObject new]);
    block([NSObject new]);

    return 0;
}
//2018-11-06 17:11:28.350466+0800 Block-Demo2[51066:2273557] array count = 1
//2018-11-06 17:11:28.350600+0800 Block-Demo2[51066:2273557] array count = 2
//2018-11-06 17:11:28.350719+0800 Block-Demo2[51066:2273557] array count = 3

{}结束时,array的变量作用域结束, 变量array被废弃, 其强引用失效, 但是代码运行成功, 因此复制给变量array的NSMutableArray类的对象在该源代码最后Block执行部分, 超出了其作用域而存在.

通过编译器转化以后:

typedef void (^Block)(id obj);

//1. __block_impl 结构体定义, 有以下成员变量: `isa`, `Flags`, `Reserved`, `FuncPtr`
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id __strong array; // 被截获的自动变量, 注意是`id __strong`类型的成员变量, 由于Block在栈/堆上都知道合适废弃内存, 因此这里使用__strong修饰, 同时配合copy/dispose方法去销毁`id __strong`成员

  // 构造函数直接截获 array
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __string _array ,int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
  // Block底层实际使用的成员变量, 将cself强引用的array赋值给局部变量array
  id __strong array = __cself->array;
  [array addObject:obj];
  NSLog(@"array count = %ld", [array count]);
}

// `_Block_object_assign`相当于调用retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中.
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
  _Block_object_assign(&dst->array, src->array, BLOCK_FILED_IS_OBJECT);
}

// 用来释放赋值在对象类型的结构体成员变量中的对象
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
  _Block_object_dispose(src->array, BLOCK_FILED_IS_OBJECT);
}

//3. __main_block_desc_0, 用于描述特定Block的大小信息. 最重要的是成员使用的copy和dispose函数指针
static struct __main_block_desc_0 {
  unsigned long reserved;
  unsigned long Block_size;
  void (*copy)(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src);
  void (*dispose)(struct __main_block_impl_0 *src);


} __main_block_desc_0_DATA = { 
  0, 
  sizeof(struct __main_block_impl_0),
  __main_block_copy_0, // id __strong array 成员变量使用的copy函数
  __main_block_dispose_0, // id __strong array 成员变量使用的dispose函数
};

int main(int argc, const char * argv[]) {
  Block block;
  {
    id __strong array = [[NSMutableArray alloc] init]; // 默认使用`__strong` 修饰符

    block = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,array, 0x22000000);

    block = [Block copy];
  }


  (*blk->impl.FuncPtr)(block, [NSObject new]);
  (*blk->impl.FuncPtr)(block, [NSObject new]);
  (*blk->impl.FuncPtr)(block, [NSObject new]);

  return 0;
}

我们注意到id __strong array使用__strong修饰符, 由于编译器是不知道C语言结构体的初始化和废弃操作,不能很好的管理内存.但是OC的runtime能够准确的把握Block从栈上copy到堆上以及堆上的Block被废弃的时机, 因此Block用结构体中即使含有__strong修饰符或__weak修饰符的变量,也可以恰当的进行初始化和废弃, 这就是在__main_block_desc_0中引入copydispose指针, 并且引入__main_block_copy_0__main_block_dispose_0两个静态函数的原因, 简单来说用来管理 在Block结构体(主要是堆上的Block)中的id __strong array这个成员变量的内存管理操作, 这两个函数会在以下情况下调用:

  • copy函数 -- 栈上的Block被复制到堆上时调用
  • dispose函数 -- 堆上Block被废弃时调用

那么具体情况什么情况下,Block会被从栈复制到堆呢(也就是以下情况会调用_Block_copy函数):

  • 调用Block的copy实例方法时
  • Block作为函数的函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa方法或者GCD的API中传递Block时
iOS Block学习笔记(八) -- Block捕获对象类型变量_第1张图片
Block3.6.jpg

实际我们在后文看到, 在栈的Block内部捕获了对象, 会导致对象的引用计数增加, 但是并不会强引用该对象. 在堆上的Block, 不仅会导致对象的引用计数增加, 也会强引用该对象.

下面的代码可以输出, array在Block调用copy以后引用计数是3;

{
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj){
        [array addObject:obj];
        NSLog(@"array count = %d", [array count]);
    } copy];
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)array));
    // 输出的 Retain count is 3
}

通过上面的内存状态图和代码, 我们可以看到通过使用__strong修饰符修饰的自动变量,Block中截获的对象就能超出作用域而存在, 这里同使用__block变量的逻辑是类似的,唯一不同是copy和dispose中传入的最后的类型参数不一致, 用该参数区分传入的是对象还是__block变量:

  • 对象 -- BLOCK_FILED_IS_OBJECT
  • __block变量 -- BLOCK_FILED_IS_BYREF

总结:

  1. 一旦block中捕获的变量为对象类型, block结构体中的__main_block_desc_0会出现两个函数copydispose. 因为block内部会访问这个对象, block需要拥有这个对象,就需要对被捕获的对象进行强引用, 因此Block内部也对内存进行管理操作.因此一旦block捕捉到了变量的类型是对象类型, 就会生成copydispose来对内部引用的对象进行内存管理.

  2. 如果block被拷贝到堆上, copy函数会调用_Block_object_assign, 该函数会根据auto变量的修饰符(__strong,__weak,unsafe_unretained)来做出相应的操作, 行成强引用或者弱引用.

  3. 如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量。

如果block捕获的是__block 基础变量, 那么block会将它当做一个对象类型 -- __Block_byref_xxx_0来看, 因为它也有 copydispose

参考资料

  1. <>
  2. https://blog.csdn.net/deft_mkjing/article/details/53149629
  3. iOS底层原理总结 - 探寻block的本质(二)

你可能感兴趣的:(iOS Block学习笔记(八) -- Block捕获对象类型变量)