📖 狮子书笔记-理解 Block

223 阅读19分钟

背景

每次看完 狮子书🦁️ 的 Block 部分,过一段时间记忆就会有点模糊...

想达到熟悉源码实现的程度,恐怕还需要反复翻看和一定场景的锻炼 ,而大部分开发日常也碰不到需要源码的场景。

不过 Block 作为 iOS 日常开发高频使用的东西,无论如何还是需要想办法去加深记忆的。

于是尝试做笔记,再加上一些自己的 图解,图像理解起来更直观容易,也许能帮助我们(可能只是菜鸡的我)更好的记忆。

文章主要按下面顺序来做(熟悉使用的开发者,可以直接跳到 Block 实现的部分看):

  • Block 语法
  • Block 使用
  • Block 实现 - Block 如何做到捕获变量
  • Block 实现 - Block 为什么不能直接对捕获的自动变量赋值?
  • Block 实现 - __Block 说明符的相关实现
  • Block 存储域 - NSConcreteStackBlcok / NSConcreteMallocBlock 的关系
  • Block 存储域 - 为什么要有堆 Block?
  • Block 存储域 - __Block 变量 如何从栈复制到堆
  • Block 存储域 - 为什么 __Block 变量 要通过 __forwarding 访问?
  • Block 存储域 - 堆 Block 截获对象的引用和释放

本文内容基本根据 《Objective-C 高级编程 - iOS 与 OS X 多线程和内存管理》而来,推荐所有 iOS 开发者阅读 📖

Block 语法

对于如何定义一个 Block,首先一定要去理解 Block 的 BN 范式 (Backus-Naur Form):

    Block_literal_expression ::= ^ block_decl compound_statement_body
    
    block_decl ::=
    
    block_decl ::= parameter_list
    
    block_decl ::= type_expression

关键在于记忆 block_decl 的定义 ,就能比较理解容易 Block 的语法了。

如果不太明白 BN 范式 的意思,尝试看看 BNF范式(巴科斯范式) 会有助于我们理解,其实就是这副图的关系:

再结合 Block 具体的 4 种写法:

  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式

上面可以看到, Block 语法中是有省略写法的,对于省略的情况主要遵循 2 点规则:

  • 如果省略返回值,那么 return 什么类型就返回什么类型,多个 return 则需要相同类型
  • 如果省略参数列表,即参数类型为 void 可以省略,代表不使用参数。

日常比较多的一种写法是 同时省略,返回值是void,参数也是 void ,可以这么写:

^{ printf("hello"); }

值得注意的是 Block 同时省略返回值和参数时,返回类型也只取决于 return 的类型 ,例如这么写:

NSInteger num = ^{ return 8;}();
    
NSLog(@"num %@",@(num))
//输出: num 8

掌握了上面的东西,在手写 Block 的能力上估计能有所进步了,虽然大家肯定还是补全用的多 : )

Block 使用

在使用上,是可以直接把 Block 当作一个变量来使用的:

  • 自动变量 (局部变量)
  • 函数参数
  • 静态变量 (静态局部变量)
  • 静态全局变量
  • 全局变量

创建 Block 作为变量使用的语法:

returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}

举个例子🌰 :

//将 Block 赋值为 Block 类型变量
int (^blk)(int)  = ^ (int count){
   return count + 1;
};

int (^blk1)(int) = blk;

int (^blk12);
blk12 = blk1;

typedef 声明 Block 类型变量

Block 在作为函数参数返回值时,写起来比较冗长:

//Block 作为函数参数
void func(int (^blk_t)(int))
{
  //do something
}

//Block 作为函数返回值
int (^func())(int)
{
  return ^(int count){ return count + 1;};
}

我们能通过 typedef 来简化 Block 的使用:

typedef int (^blk_t)(int)
  
//Block 作为函数参数
void func(blk_t blk)
{
  //do something
}

//Block 作为函数返回值
blk_t func()
{
  return ^(int count){ return count + 1;};
}

Block 的指针类型变量

Block 类型变量可以使用指向 Block 类型变量的指针来访问:

typedef int (^blk_t)(int)

blk_t blk = ^(int count){ return count+1 };

blk_t *blkptr = &blk;

(*blkptr)(10);

Block 截获变量

通过实例看一下什么是 Block 截获变量:

    int val = 10;
    const char *fmt = "val = %d \n";

    void (^block)(void) = ^{
       printf(fmt,val);
    };

    val = 2;
    fmt = " the values were changed.val = %d \n";

    block();

结果的输出:

val = 10

说明代码中的 val / fmt 在执行 Block 语法时的 瞬间值被截获(被保存),然后在执行时会使用截获的变量。

Block 的实现

截获变量 作为 Block 的重要特性,下面的篇幅,也将回答 3 个截获变量问题:

  • 为什么可以截获变量?
  • 为什么 Block 内给截获变量赋值,编译器会报错?
  • Block 如何实现修改截获的变量

在文件 main.m 的代码:

#include <stdio.h>

int main(int argc, const char * argv[]) {
    void(^block)(void) = ^()
    {
        printf("hello");
    };
    block();
    return 0;
}

通过命令:

clang -rewrite-objc  main.m

在转换后的文件 main.cpp 中,找到 main 方法关联的实现:

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;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

      printf("hello");
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

对于上面代码里的 名称规则,以 __main_block_impl_0 为例介绍一下:

image-20210328090125407

Block 结构

根据上面代码,整理出来 Block 的结构和对应的关系:

生成一个类型为 __main_block_impl_0 结构体变量 block ,构成也比较简单,由 2 部分组成:

  • 类型为 __block_impl 的结构体 impl
  • 类型为 __main_block_desc_0 的结构体 Desc

如图:

image-20210328083717302

这里最重要的就是 impl 了,它是一个 __block_impl 结构体:

image-20210328084813685

Desc 是一个 __main_block_desc_0 结构体:

而 Block 通过构造函数,将 __main_block_func_0__main_block_desc_0_DATA 作为参数传入,来生成结构体实例:

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;//flags = 0
    impl.FuncPtr = fp;//__main_block_func_0
    Desc = desc;//__main_block_desc_0_DATA
  }

Block 执行调用

对于 Block 的执行调用为下面的代码:

 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

这里需要注意,就是__main_block_impl_0类型的 block 被强转换为了__block_impl

这在C语言是可行的,因为__block_impl位于__main_block_impl_0的最顶部,就相当于__block_impl的变量直接排列在__main_block_impl_0的顶部。

上面代码去掉多余的转换部分就变成:

(*block->impl.FuncPtr)(block)

其实就是简单使用 函数指针 调用函数 __main_block_func_0

再看一下对应函数 __main_block_func_0 的实现定义:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

      printf("hello");
}

函数实现就是 Block 语句内的实现,且 block 本身作为 参数 __cself 进行了传递。

 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

Block 为什么能截获自动变量值

下面开始来分析 Block 的重点 -- 截获自动变量值

因为需要基于实际分析,仍然会把所有代码都贴出来...但建议大家不要直接全看,看多了确实头晕.. 等需要对照具体的实现代码去理解,再来看。 重点关注在 Block 截获自动变量值与未截获的 差异

将这一段代码 :

#include <stdio.h>
int main(int argc, const char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d \n";

    void (^block)(void) = ^{
       printf(fmt,val);
    };

    val = 2;
    fmt = " the values were changed.val = %d \n";

    block();
    return 0;
}

进行转换后,得到:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

  printf(fmt,val);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d \n";

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;
    fmt = " the values were changed.val = %d \n";

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

经过对比,我们可以发现,__main_block_impl_0 的实现已经发生了变化,代码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;// 多出来的部分,Block 内要使用的变量 fmt
  int val;// 多出来的部分,Block 内要使用的变量 val
  
  //构造函数..
};

上面的代码里,多出来了 Block 内要用到的 2 个变量:

image-20210328115748440

然后通过 __main_block_impl_0 初始化的构造函数,对 fmt / val 进行截取和存储:

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

Block 使用截取的自动变量值

因为截取自动变量后的 Block 执行调用没有变化,仍然是通过函数指针调用函数,并传递 __main_block_impl_0 结构体实例作为参数,我们就不再做分析了。

重点放到新的函数实现上:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

  printf(fmt,val);
}

通过代码发现,函数执行时使用的自动变量值,是通过访问 __cself 上捕获的自动变量,再赋值给函数内部的局部变量,用于在执行时使用。

Block 捕获和使用自动变量的流程

总结一下 Block 捕获和使用自动变量的流程:

image-20210328141211671

Block 为什么不能直接对截获的自动变量赋值?

当我们尝试对捕获的自动变量进行 赋值操作,一般都会提示编译错误:

 	 int a = 0 ;
    int (^blk)(void) = ^{
        a = 9;//❌Variable is not assignable (missing __block type specifier)
        return a+1;
    };

这是为什么呢?

在截获自动变量值的 __main_block_func_0 实现里,能看到系统自动加上的注释: bound by copy。Block 对它引用的局部变量做了只读拷贝,也就是说block引用的是局部变量的副本。

自动变量 val 虽然被捕获进来了,但是 Block仅仅捕获了val的值,并没有捕获val的内存地址。在 __main_block_func_0 中修改自动变量的值,依旧不能改写 Block 外面的自动变量值。

基于上面原因,Block 内的修改无法影响外部变量的值。

所以在编译层面检测出被截获的自动变量的赋值操作时,就会报编译错误。

另外会提示错误的还有这种情况:

  const char text[] = "hello";

  void (^blk)(void) = ^{
       //错误提示:Cannot refer to declaration with an array type inside block
       NSLog(@"num %c ",text[2]);
  };

Block 不能直接对 C 语言数组的截获,要使用指针:

  const char *text = "hello";

  void (^blk)(void) = ^{
       NSLog(@"num %c ",text[2]);
  };

Block 如何做到修改自动变量值

想要在 Block 中做到修改变量,在函数内可以使用:

  • 静态全局变量
  • 全局变量

因为 Block 的匿名函数部分的实现还是变换成 C 语言函数,在其中访问 静态全局变量/全局变量 并没有任何改变,可以直接使用。

除此之外,可以用两种方式在 Block 修改变量,一种是 静态变量,一种是利用 __block 说明符 修饰。

静态变量在 Block 中的实现

在 Block 中想修改变量值,除了 静态全局变量/全局变量 之外,使用静态变量也能办到修改变量值。

#include <stdio.h>
int main(int argc, const char * argv[]) {
    static int val = 10;//原来为:int val = 10;
    const char *fmt = "val = %d \n";

    void (^block)(void) = ^{
        val = 3; // 对变量进行修改
       printf(fmt,val);
    };

    val = 2;
    fmt = " the values were changed.val = %d \n";

    block();
    return 0;
}

只是这里捕获的变量稍微有些不同,由于源码和上面最开始的差不多,就不再全部贴上来了,省略了部分代码

struct __main_block_impl_0 {
  ...
  int *val;//原来为 int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int *_val, int flags=0) : fmt(_fmt), val(_val) {
   ...
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  ...
  int *val = __cself->val; // bound by copy,原来为  int val = __cself->val;
   (*val) = 3;//修改变量
  printf(fmt,(*val));// 原来为  printf(fmt,val);
}

int main(int argc, const char * argv[]) {
    static int val = 10;
    ...
    //原来为直接传递 val ,使用 static 声明后用 &val 传递
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val, fmt));
    ...
    return 0;
}

之前我们说到 Block 不能直接修改捕获的自动变量原因,就是因为只是捕获了值,并没有保存指针,所以无法修改外部的变量。

采用静态变量之后,Block 在捕获的时候传递了指针地址。修改的时候也是使用指针变量。

所以使用静态变量可以做到在 Block 内修改变量的值。

__block 说明符的实现

除了静态变量,使用 __block 修饰也可以在 Block 内修改变量的值:

int main(int argc, const char * argv[]) {
    __block int val = 10;
    const char *fmt = "val = %d \n";
    void (^block)(void) = ^{
    
       val = 3;
       printf(fmt,val);
    };

    val = 2;
    fmt = " the values were changed.val = %d \n";
    block();
    return 0;
}

经过转换,代码如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  const char *fmt = __cself->fmt; // bound by copy

       (val->__forwarding->val) = 3;
       printf(fmt,(val->__forwarding->val));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
  
    const char *fmt = "val = %d \n";

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));

    (val.__forwarding->val) = 2;
    fmt = " the values were changed.val = %d \n";

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
__Block 变量使用的结构体

看到一下子这么多代码,有点头晕,实际上最核心的就在于 __Block_byref_val_0 结构体的增加与使用。

整理代码,我们发现被 __block 修饰的 val , 类型从 int 变成了 __Block_byref_val_0 结构体:

image-20210328182239400

生成 __Block_byref_val_0 结构体变量 val

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
  (void*)0,// isa
  (__Block_byref_val_0 *)&val, //forwarding,指向自身的指针
  0, //flags
  sizeof(__Block_byref_val_0),//size
  10//val,相当于原来的自动变量
};

Block 进行捕获,则传递 val 指针作为参数:

  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
__Block 变量的访问和修改

Block 内对 val 做访问和修改:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  const char *fmt = __cself->fmt; // bound by copy

  (val->__forwarding->val) = 3;
  printf(fmt,(val->__forwarding->val));
}

通过 __cself->val 取出来变量,并且注释变为了bound by ref,说明是引用传递。

(val->__forwarding->val) 很容易看出是通过访问 val 结构体指针来做修改的:

不过确实存在疑问:

__block 变量的实现,为什么要通过 (val->__forwarding->val) 来访问和修改 ?

直接 val->val 应该也是可以的,通过 __forwarding 要多访问一步。

这里就涉及到 Block 的存储域问题了。

Block 存储域

一般 Block 根据存储位置分为3种:_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock

  • _NSConcreteGlobalBlock 没有捕获变量,或者只用到全局变量/全局静态/静态变量的 Block,生命周期从创建到应用程序结束。与全局变量一样,GlobalBlock 类对象设置在程序的数据区域(.data区)
  • _NSConcreteStackBlock 有捕获外部变量,但没有被强引用的 Block 。生命周期由系统控制。StackBlock 类对象设置在栈上。
  • _NSConcreteMallocBlock 有被强引用或者 copy 修饰的属性的 Block 会被复制一份到堆中成为 MallocBlock,生命周期由程序员控制。MallocBlock 类对象设置在由 malloc 函数分配到内存块(即堆)中。

NSConcreteStackBlock 和 NSConcreteMallocBlock 的关系

NSConcreteMallocBlock 是由 NSConcreteStackBlock 复制而来。

栈 Block 通过什么复制到堆 Block

复制 Block 使用 objc_retainBlock,而 objc_retainBlock 实际上就是 Block_copy 函数。

一般来说,编译器会自动判断。

为什么要有 NSConcreteMallocBlock ?

NSConcreteMallocBlock 的使用,是为解决这么一种情况:

设置在栈上的 Block,即 StackBlock,当 StackBlock 所属的变量作用域结束,该 Block 和栈上的 __block 变量也会随之被废弃。

StackBlock 作为栈对象的好处是内存分配快,有确切的生命周期,因为函数执行完就会自动销毁。但也因此无法在函数之外再使用它

Blocks 于是提供了将 Block/__block 变量从栈上复制到堆上来解决这个问题,即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在和被使用。

将栈 Block 复制的到堆上面创建堆 Block 对象,就可以通过引用计数来管理它。所以 NSConcreteMallocBlock 超出变量作用域还能存在。

而在 ARC 下,大多数情况编译器会恰当的判断,自动生成将 Block 从栈上复制到堆上的代码。

概念说起来可能不具体,下面通过 MRC / ARC 的两种情况来看一下 NSConcreteStackBlock 废弃的情况

MRC 下 NSConcreteStackBlock 废弃的情况

要理解为什么需要 MallocBlock ,就要理解 NSConcreteStackBlock 和栈上的 __block 变量被废弃的情况

因为上面说到了 ARC 下会做对 Block 一些自动的处理,现在来做一个对比,如果在 MRC 下不处理 Block,和 ARC 下处理 Block 的结果。

对下面代码分别在 ARC/MRC 下来进行测试:

typedef int (^blk_t)(int);

blk_t func(int rate) {
    blk_t blk = ^(int count){
        return rate * count;
    };
    return blk;
}

int main(int argc, const char * argv[]) {
    
    blk_t blk = func(3);
    int total = blk(6);
    printf("total = %d",total);
    return 0;
}

先在 ARC 下运行 ,下面的图里可以看到在 func 函数返回的为 NSConcreteMallocBlock

运行完成的结果如图:

可以看到在 ARC 下,main 函数里的 blk 为 NSConcreteMallocBlock ,total 的结果为18。正常。

然后使用 -fno-objc-arc 对文件做处理,变成 MRC 运行:

从上面的图里看到在 func 函数返回的为 NSConcreteStackBlock

运行完成的结果如图:

我们发现 blk 的类型 isa 变成了 0x0,确实属于 NSConcreteStackBlock 随着变量作用域结束而被废弃的情况。 total 得到也是一个奇怪的值。

解决方式就是把 StackBlock 变成 MallockBlock ,所以 ARC 下,编译器自动判断后帮我们做了一次转换

ARC 下 NSConcreteStackBlock 废弃的情况

大多数情况下 ARC 下会自动复制到堆上,但也存在例外,这时需要 手动copy 将 Block 复制到堆上。

需要手动 copy 的场景

有一种情况需要我们手动 copy ,是编译器不能判断的状况:

  • 向方法或函数传递 Block

下面是一个 Block 作为 参数传递 的例子:

id getBlockArray()
{
    NSString *val = @"test";
    NSArray *blockList =[[NSArray alloc] initWithObjects:
                         ^{NSLog(@"block0 %@",val);},
                         ^{NSLog(@"block1 %@",val);},
                         ^{NSLog(@"block2 %@",val);},
                         nil];
    return blockList;
}

int main(int argc, const char * argv[]) {
    
    NSArray *array = getBlockArray();
    blk_t blk = (blk_t)[array objectAtIndex:1];
    blk();
    return 0;
}

将代码在 ARC 下运行,发现数组里除了第 0 个是 NSMallocBlock ,后面的都是 NSStackBlock :

image-20210329194859630

接下来在 main 取数组里的 Block ,会发现崩溃了,原因是: StackBlock 随着函数作用域的结束而跟着被废弃了

image-20210329195115527

怎么解决 StackBlock 废弃呢?

那就是把 Block 复制到堆上,变成 MallocBlock 之后,即使超出了函数的作用域也可以被访问和使用。

对应到这段代码,就是代码手动调用 copy :

   NSArray *blockList =[[NSArray alloc] initWithObjects:
                         ^{NSLog(@"block0 %@",val);},
                         [^{NSLog(@"block1 %@",val);} copy],
                         [^{NSLog(@"block2 %@",val);} copy],
                         nil];

这也验证了需要 NSConcreteMallocBlock 在超出函数作用域时使用的原因,否则当 StackBlock 废弃,再去访问就会发生异常。

三种 Block 的 copy 效果

另外这里直接总结一下三种 Block 调用 copy 的效果:

Block 类型存储域复制的效果
_NSConcreteGlobalBlock程序的数据区域什么也不做
_NSConcreteStackBlock从栈复制到堆
_NSConcreteMallocBlock引用计数增加

__block 变量从栈上复制到堆上

当 Block 被复制到堆上,对应使用的 __Block 变量都是会一起复制到堆上的。

需要注意的是多个 Block 持有同 1 个 Block 变量 的情况是如何复制的。

例如:

栈上的 block1和 block2 同时都用到了栈上的 __block 变量

block1 复制到堆上,__block 变量也跟着被复制到了堆上,并且堆上的__block 变量被堆上的 block1 持有。如图:

image-20210403232407753

接着 block2 也复制到堆上,__block 变量 此时不会再复制 1 份,被复制到堆上的 block2 持有 __block 变量 ,增加 __block 变量 的引用计数。__block 变量 同时被堆上的 block1 和 block2 一起所持有。

__block 结构体使用 forwarding 成员变量的原因

之前提到了,使用 __block 变量 的修改变量值没直接用 var->var 的方式,而是使用 var->__forwarding->var 绕了一下来做访问。

例如下面的情况:

{
  __block int val = 0;
  
  void (^blk) (void) = [^{++val;} copy];

  ++val;
  blk()
  
  NSLog(@"%d",val);
}

代码里 2 处的 ++val 最终同样都用 val.__forwarding->val 来做修改。

重点在于,实际上这 2 处是不同的 __block 变量 类型:

  • Block 语法内的,因为被 copy 了,所以 Block 执行时是 堆上的 __block 变量

  • Block 语法外的,仍旧还是 栈上的 __block 变量

当栈上的 Block 被复制到堆上,此时可以 同时 访问栈上 __block 变量 堆上__block变量

如果直接使用 val->val 来访问,那么此时 2 个不同的 __block 变量 ,指向的是栈上和堆上不同的地址,显然会产生 错误的结果

关键的解决方式,就是使用 __forwarding 来做指向:

如图,有了 __forwarding 来做指向的话,那么在 栈上的__block 变量 ,最终也会指向 堆上的__block 变量,来保证访问到的都是同一个变量,即此时访问栈 __block 变量 实际访问的是堆 __block 变量

截获对象的使用和废弃

被 MallocBlock 所截获的对象(这里指 OC 对象),就算没有使用 __block 修饰,由于被堆上的 Block 持有,也会延长对象的生命周期,使该对象能够超出其变量作用域而存在。等到堆上的 Block 释放才会调用 dispose 函数去释放对象。

例如下面的情况:

    blk_t blk ;
    {
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj){
            [array addObject:obj];
            NSLog(@"array count = %ld",[array count]);
        } copy];
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

输出:

array count = 1
array count = 2
array count = 3

array 按照道理应该是随着变量作用域结束就会被废弃了,但是调用 blk([NSObject new]) 发现 array 仍旧存在,且运行正常。

原因就是 Block 在堆上 ,对象因为被堆上的 Block 持有了,所以还能超出其变量作用域存在。

copy / dispose 函数

截获对象的一个关键点,就是用 copy 函数和 dispose 函数来进行引用和释放:

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

截获对象截获 __block 变量时,copy / dispose 函数的内部实现是不一样的。

截获 __block 变量:

// __block 变量使用  BLOCK_FIELD_IS_BYREF (值为8)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
   _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
 	 _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

截获对象:

// 对象使用  BLOCK_FIELD_IS_OBJECT (值为3)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
  _Block_object_assign((void*)&dst->val, (void*)src->val, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
  _Block_object_dispose((void*)src->val, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
栈上 Block 复制到堆

在下列情况,栈上的 Block 会自动复制到堆:

  • 调用Block 的 copy 实例方法
  • Block 作为函数返回值返回时
  • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或Block 类型成员变量时
  • 在方法名含有有 usingBlock 的 cocoa 框架方法或 GCD 的 API 传递 Block 时 (会在方法内部调用 copy)

总结

这次将 Block 的相关知识梳理了一下,主要针对 Block 的实现,__block 变量的实现, Block/__block 变量 怎么从栈复制到堆,以及回答为什么需要堆 Block,为什么 __block 变量 的结构体要通过 __forwarding 访问等问题。

在整理和画图的同时,相比只读一遍文字和代码,记忆更加深刻了。建议大家也可以在读书时画图整理关系。

如有错误,欢迎指正 !