Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

block #70

Open
ShannonChenCHN opened this issue Jul 9, 2017 · 13 comments
Open

block #70

ShannonChenCHN opened this issue Jul 9, 2017 · 13 comments

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Jul 9, 2017

目录

  • 什么是 block
  • block 的使用
  • block 与循环引用
  • block 的实现
  • 经典案例
@ShannonChenCHN ShannonChenCHN changed the title block 详解 block Oct 19, 2017
@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 5, 2021

Block 总结(《Objective-C 高级编程》学习总结)

示例代码:

目录

一、Block 简介

  1. 什么是 block?
  2. block 值的语法
  3. block 类型名的语法
  4. block 捕获自动变量的值
  5. block 中如何修改捕获到的变量值(__block
  6. block 捕获 Objective-C 对象

二、Block 的实现

  1. block 的本质
  2. block 是如何捕获自动变量的
  3. block 中是如何捕获静态变量、静态全局变量和全局变量的?
  4. __block 的实现
  5. block 是怎么存储的(”block 对象“的三种类型)
  6. __block 变量是怎么存储的
  7. block 是如何捕获 Objective-C 对象的
  8. 在对象类型的变量上使用 __block 时发生了什么

三、Block 循环引用
四、非 ARC 下的 block 与 copy/release
五、Q&A

延伸阅读

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

一、Block 简介

1. 什么是 block?

block 是可以在函数内定义的匿名函数。

2. block 值的语法

block 的语法跟函数相比,多了 ^,少了函数名:

BOOL isOdd(int num) {
    return num % 2 != 0;
}

// block 的语法跟函数相比,多了 ^,少了函数名
BOOL (^isOdd)(int) = ^BOOL (int num) {
    return num % 2 != 0;
};

返回值类型可以省略:

BOOL (^isOdd)(int) = ^(int num) {
    return (BOOL)(num % 2 != 0);
};

如果不需要传参数,那么参数列表也可以省略:

void (^foo)(void) = ^void (void) {
    printf("This block has no arguments.");
};

// 如果不需要传参数,那么参数列表也可以省略
void (^foo)(void) = ^void {
    printf("This block has no arguments.");
};

// 进一步简化
void (^foo)(void) = ^{
    printf("This block has no arguments.");
};

3. block 类型名的语法

函数指针:

BOOL isOdd(int num) {
    return num % 2 != 0;
}

BOOL (*funcPtr)(int) = &isOdd;

block 类型的声明跟函数指针相比,只是把 * 换成了 ^

BOOL (^isOdd)(int) = ^BOOL (int num) {
    return num % 2 != 0;
};

block 作为方法和函数的参数和返回值:

// 当 block 作为方法的参数时,参数类型括号里只写类型 BOOL(^)(int),参数名放到了括号后面
- (void)doSomething:(BOOL(^)(int))isOdd;

// 当 block 作为方法的返回值,不写参数名,只有类型 BOOL(^)(int)
- (BOOL(^)(int))doSomething;

// 当 block 作为函数的返回值,跟 block 变量类型名定义的类似,将函数名加 `()`放到中间
BOOL(^doSomething())(int) {
    return ^BOOL(int num) {
        return num % 2 != 0;
    };
}

使用 typedef 可以让 block 类型名的可读性更好:

// 写法跟定义一个 block 类型的自动变量很相似,变量名变成了新的类型名
typedef BOOL(^NumberValidator)(void);

- (void)doSomething:(NumberValidator)isOdd;

- (NumberValidator)doSomething;

4. block 捕获自动变量的值

int count = 20;  
void (^foo)(void) = ^void() {
	printf("%d\n", count); // 输出:20
};

foo();

知识点:什么是自动变量?

C 语言中主要有以下 5 种变量:

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

(1)在计算机编程领域,自动变量(Automatic Variable)指的是局部作用域变量,具体来说即是在控制流进入变量作用域时系统自动为其分配存储空间,并在离开作用域时释放空间的一类变量。在许多程序语言中,自动变量与术语“局部变量”(Local Variable)所指的变量实际上是同一种变量,所以通常情况下“自动变量”与“局部变量”是同义的。

(2)默认情况下,在代码块内声明的变量都是自动变量,函数中的局部变量,如果不用关键字static加以声明,编译系统会对他们自动分配存储空间的,函数的形参和在函数中定义的变量,在调用函数时,系统给形参和定义的变量分配存储空间,数据存储在动态区中。在函数调用结束时就自动释放这些空间。如果是在复合语句(比如 for 循环)中定义的变量,则在变量定义时分配存储空间,在复合语句结束时自动释放空间。

(3)而静态存储类型的局部变量是在静态存储区内分配内存单元,在程序的整个运行期间内都不释放空间。

(4)静态局部变量是在编译时赋初始值,并且只赋一次初值,在以后每次调用函数时,只是使用上一次函数被调用结束时变量的值。而自动局部变量的初值不是在编译时赋予的,而是在函数调用时赋予的,每调用一次函数都会对变量重新赋一次初值。

5. block 中如何修改捕获到的自动变量值(__block

block 中可以修改捕获到的非全局的静态变量、静态全局变量和全局变量的值。但是不能修改捕获到自动变量值。

如果想要在 block 中修改捕获到自动变量值,需要在变量前声明 __block

__block int count = 20;  // 需要声明 __block,才能在 block 中改变值
void (^foo)(void) = ^void() {
	count = 25;
};

printf("%d\n", count); // 输出:20
foo();
printf("%d\n", count); // 输出:25

6. block 捕获 Objective-C 对象

block 可以捕获 Objective-C 对象类型的变量,我们可以在 block 内修改可变对象的内容:

NSMutableArray *array = [NSMutableArray array];
void (^blockCaptureObject)(void) = ^void(void) {
  [array addObject:@"shannon"];
  [array addObject:@"chen"];
};
blockCaptureObject();
NSLog(@"%@", array); // 输出:( shannon, chen )

但是如果要在 block 内修改变量的值,还是需要加 __block

__block NSMutableArray *array = [NSMutableArray array]; 
void (^blockCaptureObject)(void) = ^void(void) {
	array = @[@"Tony"].mutableCopy;
};
blockCaptureObject();
NSLog(@"%@", array); // 输出:( Tony )

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(一)

首先,关于 block 的数据结构和 runtime 是开源的,可以在LLVM 开源项目中的 Blocks Runtime 中看到,或者下载苹果的 libclosure 库的源码来看(也可以在线查看),其中包含了很多示例和文档说明。

另外,在公开的 usr/include/Block.h 头文件中,和 objc4 runtime 源码的 Block_private.h 中也可以看到 block 相关的结构定义和 api。

除此之外,我们还可以使用 clang -rewrite-objc main.m 命令,将含有 block 定义的代码转成 C++ 代码,也可以看到 block 的真实面貌。

1. block 的本质

main.m 中的代码如下:

int main() {
  void (^blk)(void) = ^void(void) {
  	printf("Block\n");
  };
  blk();
  return 0;
}

使用 clang -rewrite-objc main.m 命令将上面的转成 C++ 代码:

/// block 的执行内容转成的静态函数
/// 参数 __cself 为指向 __main_block_impl_0 结构实例本身的变量
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n");
}

int main() {
		
    // 这里实际上是初始化一个 __main_block_impl_0 结构体实例,然后再将这个实例的指针赋值给变量 blk
    // 第一个参数是一个函数指针,指向由 block 语法转换的 C 函数 __main_block_func_0
    // 第二个参数是一个结构体指针,指向这个 block 结构存储相关的结构体 __main_block_desc_0_DATA 
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
    // 执行 blk 时,是从 __main_block_impl_0 实例中取出函数指针,调用该函数
    // 函数参数是这个 __main_block_impl_0 实例本身
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

下面是 __main_block_impl_0__block_impl__main_block_desc_0_DATA 的实现:

/// 存储 block 关键信息的结构体
struct __block_impl {
  void *isa;     // 像 Objective-C 对象一样,识别身份的 isa 指针
  int Flags;     // 记录某些标记,比如引用计数
  int Reserved;  // 今后版本升级所需的区域
  void *FuncPtr; // 存储 block 执行函数的指针
};

/// 存储 block 描述信息的结构体
/// 这里直接创建了一个静态全局变量
static struct __main_block_desc_0 {
    size_t reserved;    // 今后版本升级所需的区域
    size_t Block_size;  // block 的大小
} __main_block_desc_0_DATA = { 
    0, 
    sizeof(struct __main_block_impl_0)
}; 

/// block 转成的结构体,这个就是 block 的真实面貌
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) {
	  // 该 block 的类型为 _NSConcreteStackBlock
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • block 本质上是一个 Objective-C 对象。因为在 block 的真实结构中,跟 objc_objectobjc_class 一样,都有一个 isa 成员变量。
  • 将 block 赋值给一个 Block 类型的变量,本质上是将一个 __main_block_impl_0 结构体实例的指针赋值给变量 blk
  • block 语法中的匿名函数,被保存到了这个 __main_block_impl_0 结构体实例中了。

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(二)

2. block 是如何捕获自动变量的

main.m 中的代码如下:

int main () {
 
     int val1 = 256;
     int val2 = 10;
     const char *fmt = "val2 = %d\n";
     
     void (^myBlock)(void) = ^void(void) {
         printf(fmt, val2);
     };
     
     val2 = 2;
     fmt = "These values were changed. val2 = %d\n";
     
     myBlock();
     
     return 0;
 }

使用 clang -rewrite-objc main.m 命令将上面的转成 C++ 代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    const char *fmt = __cself->fmt; // bound by copy,获取捕获到的自动变量(保存在成员变量中)
    int val2 = __cself->val2; // bound by copy,获取捕获到的自动变量(保存在成员变量中)

    printf(fmt, val2);
}

int main () {

    int val1 = 256;  // block 中没用到,也就不会截获
    int val2 = 10;
    const char *fmt = "val2 = %d\n";

    // 构造函数多了两个参数,用于保存捕获到的自动变量
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val2));

    val2 = 2;
    fmt = "These values were changed. val2 = %d\n";

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

    return 0;
}

下面是 __main_block_impl_0__block_impl__main_block_desc_0_DATA 的实现:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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)
};


// block 对应的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
    // block 中捕获的自动变量被声明成了成员变量
  const char *fmt;
  int val2;
    
    // 构造函数中也多了两个参数,用来传入捕获的自动变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val2, int flags=0) : fmt(_fmt), val2(_val2) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • block 捕获自动变量跟没捕获自动变量的区别,就是 block 的存储结构里多了保存捕获变量的成员变量,构造函数也多了对应的参数。
  • block 捕获自动变量的本质,其实就是将 block 中使用到的自动变量值保存到了 block 的结构体实例(也就是 block 本身)中。
  • 我们可以将 block 看成是一个普通的 Objective-C 类,它有一个必传的属性,接收函数指针,其他参数就是 block 匿名函数中保存捕获到的变量。

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(三)

3. block 中是如何捕获静态变量、静态全局变量和全局变量的?

block 中不能修改捕获到自动变量值。但是可以修改捕获到的非全局的静态变量、静态全局变量和全局变量的值。这是怎么做到的呢?

main.m 中的代码如下:

int global_val = 60;
static int static_global_val = 10;

int main() {
    
    int val = 10;
    static int static_val = 4;
    
    void (^myBlock)(void) = ^void(void) {
        static_val = 30;
        global_val = 6;
        static_global_val = 50;
        printf("myBlock:val(10) = %d,\n static_val(4) = %d,\n global_val(60) = %d,\n static_global_val(10) = %d\n", val, static_val, global_val, static_global_val);
    };
    
    myBlock();
    
}

使用 clang -rewrite-objc main.m 命令将上面的转成 C++ 代码:

int global_val = 60;
static int static_global_val = 10;

// block 的实现对应的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val; // bound by copy,
    int val = __cself->val; // bound by copy

    // 通过访问静态局部变量的指针来改变变量值
    (*static_val) = 30;

    // 全局变量和静态变量可以直接访问
    global_val = 6;
    static_global_val = 50;

    // 自动变量无法访问,只能读取变量值
    printf("myBlock:val(10) = %d,\n static_val(4) = %d,\n global_val(60) = %d,\n static_global_val(10) = %d\n", val, (*static_val), global_val, static_global_val);
}

int main() {

    int val = 10;
    static int static_val = 4;
    
    // 这里把静态变量的指针传入了 __main_block_impl_0 结构体的构造函数并保存
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0(
                                                              (void *)__main_block_func_0,
                                                              &__main_block_desc_0_DATA,
                                                              &static_val,
                                                              val));

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

}

下面是 __main_block_impl_0__block_impl__main_block_desc_0_DATA 的实现:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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)
};

// block 的数据结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;  // 捕获进来的 static 变量的地址
  int val;  // 捕获进来的自动变量
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • block 中可以直接访问静态全局变量和全局变量。
  • 因为 block 匿名函数转换后的函数访问不到定义 block 的函数中的变量,所以,block 不能直接访问局部静态变量,而是通过保存静态变量的指针来实现修改静态变量。

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(四)

4. __block 的实现

__block 说明符类似于 staticautoregister 说明符用于指定将变量值存储到哪个存储域中。

知识点:C 语言中的存储域类说明符
C 语言中的存储域类说明符主要有以下几个:

  • typedef
  • extern
  • static:存储在静态存储区
  • auto:局部变量,存储在栈区
  • registerregister 变量存储在寄存器中,可以实现快速访问

image

参考:

int main() {
    __block int block_val = 8;
    
    void (^myBlock)(void) = ^void(void) {
        block_val = 9;
        printf("myBlock: block_val(8) = %d\n", block_val);
    };
    
    myBlock();
    printf("block_val = %d\n", block_val);
}

使用 clang -rewrite-objc main.m 命令将上面的转成 C++ 代码:

/// 捕获的 __block 变量转为了结构体
struct __Block_byref_block_val_0 {
  void *__isa;                           // 结构体中带有 isa,说明它也是一个对象
  __Block_byref_block_val_0 *__forwarding; //  __forwarding ,存储 __block 结构体的地址
 int __flags;
 int __size;
 int block_val; // 捕获的参数值
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_block_val_0 *block_val = __cself->block_val; // bound by ref

    (block_val->__forwarding->block_val) = 9;  // 通过 __forwarding 成员变量来获取捕获的 __block 变量
    printf("myBlock: block_val(8) = %d\n", (block_val->__forwarding->block_val));
    
}

int main() {

    // __block 变量被转成了 __Block_byref_block_val_0 结构体类型
    // 其中第二个参数是把 block_val 的地址传给了成员变量 __forwarding
    // 第三个参数是把__block 变量的值传给了保存数值的成员变量
    __attribute__((__blocks__(byref))) __Block_byref_block_val_0 block_val = {(void*)0,
                                                                              (__Block_byref_block_val_0 *)&block_val,
                                                                              0,
                                                                              sizeof(__Block_byref_block_val_0),
                                                                              8};
    // __block 的结构体变量的地址被传进了 block 结构体构造函数,这样就可以达到修改外部变量的作用。
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_block_val_0 *)&block_val, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    // 后面继续使用 __block 变量时,是用 block_val 来访问的
    printf("block_val = %d\n", (block_val.__forwarding->block_val));
}

下面是 __main_block_impl_0__block_impl__main_block_desc_0_DATA 的实现:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

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

// 我们需要负责 __Block_byref_block_val_0 结构体相关的内存管理,
// 所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。
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
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_block_val_0 *block_val; // by ref,存储 __block 结构体变量的成员变量
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_block_val_0 *_block_val, int flags=0) : block_val(_block_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • 被捕获的 __block 变量被转成了一个 __Block_byref_block_val_0 结构体,其中一个成员变量 block_val 用来保存捕获的值,还有一个成员变量 __forwarding 用来存储__Block_byref_block_val_0 结构体变量的指针。(关于 __forwarding 存在的意义,更多细节在后面两节讨论。)
  • 另外,__Block_byref_block_val_0 结构体中也有一个 isa 指针,所以我们也可以把__block 变量看做是一个对象。
  • block 之所以能修改 __block 变量的值,是因为它通过成员变量保存了 __Block_byref_block_val_0 结构体变量的指针,通过这个指针来间接访问其中的 block_val
  • __Block_byref_block_val_0 结构中的字段没有直接定义在 __main_block_impl_0 中,是因为一个 __block 变量可能会在多个 block 中使用。
  • 因为我们需要负责 __Block_byref_block_val_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,用于在调用前后修改相应变量的引用计数。(更多细节见第5,6, 7节)
  • block 后面继续使用 __block 变量时,也是通过访问 __Block_byref_block_val_0 结构体变量中的 block_val 来访问的。
  • 为什么不能直接向捕获静态局部变量那样,将捕获的 __block 变量直接保存成指针呢?因为 block 的生命周期是可以超过被截获的自动变量的,当被截获的自动变量因作用域结束被销毁后,如果再继续通过指针访问这个自动变量的话,就会出现异常。(至于为什么 block 超出变量作用域仍然存活,更多细节在后面两节讨论。)

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(五)

5. block 本身是怎么存储的(”block 对象“的三种类型)

block 的存储域主要有三种:

  • _NSConcreteStackBlock:存储在栈上
  • _NSConcreteGlobalBlock::存储在数据区域,也就是.data区域
  • _NSConcreteMallocBlock::存储在堆上

image

5.1 _NSConcreteStackBlock_NSConcreteGlobalBlock

我们先来看看_NSConcreteStackBlock_NSConcreteGlobalBlock 这两种类型。

ARC 下执行下面的代码:

void (^globalBlock)(void) = ^ { };
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", ^{});  // 输出:<__NSGlobalBlock__: 0x100004188>
        NSLog(@"%@", globalBlock);  // 输出:<__NSGlobalBlock__: 0x1000040f8>
        int num = 1;
        int (^stackBlock)(int) = ^int(int a) {
            return a + num;
        };

        stackBlock(1);
        NSLog(@"%@", stackBlock); // 输出:<__NSMallocBlock__: 0x10058da30>
    }
    return 0;
}

非 ARC 下执行下面的代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", ^{});  //输出:<__NSGlobalBlock__: 0x100004188>
        NSLog(@"%@", globalBlock);  // 输出:<__NSGlobalBlock__: 0x1000040f8>
        
        int num = 1;
        int (^stackBlock)(int) = ^int(int a) {
            return a + num;
        };

        stackBlock(1);
        NSLog(@"%@", stackBlock); // 输出:<__NSStackBlock__: 0x7ffeeee4d5b8>
    }
    return 0;
}

5.2 _NSConcreteMallocBlock

那么什么时候 block 会出现在堆上呢?

ARC 下,编译器会自动将 block 从栈上复制到堆上。其实在上面的示例代码中已经看到了一些表现了。

(1)block 作为函数返回值

// block 作为函数返回值
typedef int (^BlockType)(int);
BlockType foo() {
    return ^int(int a) {
        return a + 1;
    };
}

转成汇编代码后:

_foo:                                   ## @foo
	.cfi_startproc
## %bb.0:
  ...
	leaq	___block_literal_global(%rip), %rdi
	callq	_objc_retainBlock
	...
	jmp	_objc_autoreleaseReturnValue ## TAILCALL
	.cfi_endproc

(2)block 作为函数参数

// block 作为函数参数
void foo() {
    int val = 10;
    id a = [[NSArray alloc] initWithObjects:^{ printf("block1: val = %d", val); }, nil];
    printf("%p", a);
}

转成汇编代码后:

_foo:                                   ## @foo
	.cfi_startproc
## %bb.0:
	...
	callq	*_objc_msgSend@GOTPCREL(%rip)
	leaq	"___block_descriptor_36_e5_v8�?0l"(%rip), %rcx
	leaq	___foo_block_invoke(%rip), %rdx
	movq	__NSConcreteStackBlock@GOTPCREL(%rip), %rsi
	...
	callq	_objc_retainBlock
	...
	callq	*_objc_msgSend@GOTPCREL(%rip)
	...
	callq	*_objc_release@GOTPCREL(%rip)
	...
	callq	_printf
	...
	callq	_objc_storeStrong
	...
	.cfi_endproc

(3)将 block 赋值给一个 __strong 变量

typedef int (^BlockType)(void);
int foo(int base) {
    
    BlockType blk = ^int() {
        return base + 5;
    };
    
    return blk();
}

转成汇编代码后:

_foo:                                   ## @foo
	.cfi_startproc
## %bb.0:
	...
	leaq	"___block_descriptor_36_e5_i8�?0l"(%rip), %rax
	leaq	___foo_block_invoke(%rip), %rcx
	movq	__NSConcreteStackBlock@GOTPCREL(%rip), %rdx
	...
	callq	_objc_retainBlock
	...
	callq	_objc_storeStrong
	...
	.cfi_endproc

(4)将 block 赋值给一个 __weak 变量,并使用了这个变量

typedef int (^BlockType)(void);
int foo(int base) {
    __weak BlockType blk = ^int() {
        return base + 5;
    };
    return blk();
}

转成汇编代码后:

_foo:                                   ## @foo
Lfunc_begin0:
	.cfi_startproc
	...
	movq	__NSConcreteStackBlock@GOTPCREL(%rip), %rax
	...
	leaq	___foo_block_invoke(%rip), %rax
	...
	leaq	"___block_descriptor_36_e5_i8�?0l"(%rip), %rax
	...
	callq	_objc_initWeak
	...
	callq	_objc_loadWeakRetained
	...
	callq	*_objc_release@GOTPCREL(%rip)
	...
	callq	_objc_destroyWeak

(5)将 block 赋值给一个 __unsafe_unretained 变量

typedef int (^BlockType)(void);
int foo(int base) {
    
    __unsafe_unretained BlockType blk = ^int() {
        return base + 5;
    };
    
    return blk();
}

转成汇编代码后:

_foo:                                   ## @foo
	.cfi_startproc
## %bb.0:
	...
	leaq	"___block_descriptor_36_e5_i8�?0l"(%rip), %rax
	leaq	___foo_block_invoke(%rip), %rcx
	movq	__NSConcreteStackBlock@GOTPCREL(%rip), %rdx
	...
	.cfi_endproc

在 objc4 runtime 源码中可以看到 objc_retainBlock 函数的实现:

//
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
//
id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

在苹果开源的 libclosure 中可以看到 _Block_copy 的实现。

_Block_copy 的调用逻辑如下:

_Block_copy
  _Block_copy_internal
      // if it‘s a stack block.  Make a copy.
      malloc
      memmove
      _Block_call_copy_helper

_Block_call_copy_helper的实现:

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

5.3 block 作为属性

block 类型的属性一般声明为 copy

@interface XYZObject : NSObject

@property (copy) void (^blockProperty)(void);

@end

苹果官方文档中的建议:

Note: You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior. For more information, see Blocks Programming Topics.

下面是 Clang 文档中的描述:

Blocks as Objects

The compiler will treat Blocks as objects when synthesizing property setters and getters, will characterize them as objects when generating garbage collection strong and weak layout information in the same manner as objects, and will issue strong and weak write-barrier assignments in the same manner as objects.

5.4 小结

  • block 的存储域主要有三种:
    • _NSConcreteStackBlock:存储在栈上
    • _NSConcreteGlobalBlock::存储在数据区域,也就是.data区域
    • _NSConcreteMallocBlock::存储在堆上
  • _NSConcreteGlobalBlock 类型主要在以下两种情况出现
    • 定义为全局变量的 block
    • 定义为局部变量且不捕获自动变量的 block
  • 定义为局部变量且捕获自动变量的 block 一般都是 _NSConcreteStackBlock 类型
  • 栈上的 block 会随着作用域结束而被销毁,通过将 block 从栈上复制到堆上可以延长 block 的寿命
  • ARC 下,只要 block 被赋值给一个__strong变量或者当做返回值,编译器就会自动插入 objc_retainBlock 函数将 block 从栈上复制到堆上。ARC 下,编译器对 block 的处理基本跟对普通 Objective-C 的处理一致。
  • 书中提到的“block作为函数/方法的参数时,需要手动调用 copy”,经实践验证,实际上,这种场景编译器也会自动插入 objc_retainBlock 函数,所以现在基本上不存在需要手动将 block 从栈上复制到堆上的情况了
  • 三种 block 经复制后的效果
    • _NSConcreteStackBlock:从栈中复制到了堆上
    • _NSConcreteGlobalBlock:什么也不做
    • _NSConcreteMallocBlock:引用计数增加

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(六)

6. __block 变量是怎么存储的

6.1 __block 变量的存储域与 Block 的关系

Block 从栈复制到堆时,对 __block变量产生的影响:

初始时 __block变量的存储域 Block 从栈复制到堆时的影响
从栈复制到堆上,并被 Block 持有
被 Block 持有(引用计数加1?)

(1)一个 block 中使用一个 __block变量

如果在一个 block 中使用了 __block变量,那么当这个 block 被复制到堆上时,它使用的所有的 __block 变量也都会被复制到堆上,并且被这个 block 所持有(如下图所示)。

如果 block 中使用的这个__block变量已经在堆上了,当这个 block 被复制到堆上时,那么这个 __block变量就会被持有,引用计数增加。

如果对一个已经被复制到堆上的 block 进行再次 copy,该操作对 block 中使用的__block变量没有影响。

image

(2)在多个 block 中使用同一个 __block 变量

一开始所有的 block 和 __block 变量都存储在栈上,当其中一个 block 被复制到堆上时,__block 变量也会一并从栈中复制到堆上,并被这个 block 所持有。当后面其他的 block 被复制到堆上时,会持有这个 __block 变量,并增加它的引用计数(如下图所示)。

image

如果堆上的 block 被销毁,那么它所持有的 __block 变量也会被释放。

image

6. 2 __block变量转成的结构体__Block_byref_block_val_0中的成员变量 __forwarding 的目的是什么?

第 4 节中的 C++ 代码中,我们可以看到被捕获的 __block 变量最终被转成了 __Block_byref_block_val_0 结构体类型的变量,不论是在 block 中,还是在 block 后面的代码中,所有的 int 类型都被替换成了这个 __Block_byref_block_val_0 类型。

int main() {

    // int 类型被转成了 __Block_byref_block_val_0 结构体类型
    // 第二个参数是把 block_val 的地址传给了成员变量 __forwarding
    __attribute__((__blocks__(byref))) __Block_byref_block_val_0 block_val = {(void*)0,
                                                                              (__Block_byref_block_val_0 *)&block_val,
                                                                              0,
                                                                              sizeof(__Block_byref_block_val_0),
                                                                              8};
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_block_val_0 *)&block_val, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    // 后面继续使用 __block 变量时,是用 block_val 来访问的
    printf("block_val = %d\n", (block_val.__forwarding->block_val));
}

__block变量被复制到堆上之后,block 内使用的 block_val是复制到堆上的 __Block_byref_block_val_0实例,而 block 后面使用的 block_val仍然是栈上的 __Block_byref_block_val_0实例。

只是栈上的 __Block_byref_block_val_0实例在 __block变量被复制到堆上时,会将 __forwarding指针修改成被复制到堆上的 __Block_byref_block_val_0实例的地址(如下图所示)。

image

在上一节中我们通过苹果开源的 libclosure 源码,看到了_Block_copy 最终会调用 _Block_call_copy_helper函数,而这个函数做的就是取出 block 结构体中的 descriptor,调用它的 copy 函数:

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

在第4节 block 捕获 __block 变量的代码转成 C++ 代码后,我们可以看到这个 copy 函数调用的是 _Block_object_assign。该函数在 libclosure 中的实现是判断它的第三个参数 flag,比如,如果捕获的是栈上的 __block 变量,那就是 BLOCK_FIELD_IS_BYREF,这种 case 下调用的就是 _Block_byref_assign_copy 函数:

// Runtime entry points for maintaining the sharing knowledge of byref data blocks.

// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment it.
// Otherwise we need to copy it and update the stack forwarding pointer
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
        
    ...
      
      // src points to stack
      bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
    // if its weak ask for an object (only matters under GC)
    struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
    copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
    copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
    src->forwarding = copy;  // patch stack to point to heap copy
    copy->size = src->size;
  
    ...
      _Block_memmove(copy+1, src+1,
                           src->size - sizeof(struct Block_byref));
	
  	...
      
    // assign byref data block pointer into new Block
    _Block_assign(src->forwarding, (void **)destp);
}

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 6, 2021

二、Block 的实现(七)

7. block 是如何捕获 Objective-C 对象的

(1)block 捕获 Objective-C 对象的实现

main.m 中的代码如下:

int main() {
    
    typedef void(^blk_t) (id obj);
    
    blk_t blk;
    {
        // 捕获 Objective-C  对象
        id array = [[NSMutableArray alloc] init];

        blk = ^void(id obj) {
            [array addObject:obj];
            
            NSLog(@"array count = %ld", [array count]);
        }; // 在 ARC 中这里并不需要手动 copy,在 MRR 中就需要手动 copy(p125)
    }
    
    blk([[NSObject alloc] init]);
    
    return 0;
}

使用 clang -rewrite-objc main.m 命令将上面的转成 C++ 代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
    id array = __cself->array; // bound by copy
    ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_nt_bkkycgbs2hv63tthr5dbd2g40000gn_T_main8_c8bb70_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}

int main() {

    typedef void(*blk_t) (id obj);

    blk_t blk;

    {
    		// Objective-C 对象没有变化
        id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
				
				// Objective-C 对象直接当成参数传了
        blk = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
    }


    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));

    return 0;
}

下面是 __main_block_impl_0__block_impl__main_block_desc_0_DATA 的实现:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

// 其中有两个管理内存的函数指针 copy 和 dispose
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
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array; // 捕获到的 Objective-C 对象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • block 捕获 Objective-C 对象时,是直接将 Objective-C 对象保存到成员变量中了,同时 __main_block_desc_0 中有两个函数指针,分别指向__main_block_copy_0__main_block_dispose_0

  • 一般来讲,在 Objective-C 中,C 语言结构体不能含有 Objective-C 对象类型的成员变量,因为编译器不知道什么时候初始化和销毁结构体,不能很好地管理内存。但是 objc runtime 运行时库知道block 从栈复制到堆上以及从堆上被销毁的时机,也就是 __main_block_copy_0__main_block_dispose_0 的回调,所以 block 的结构中可以直接包含对象,并进行内存管理。

  • 第二点进一步证明了 “block 本质上是一个 Objective-C” 这一事实,既有 isa 指针,又有相应的内存管理方法

(2)__main_block_desc_0 中保存的 copydispose 函数指针什么时候调用

在前面的第5节中,我们知道 block 从栈上复制到堆上时,会调用_Block_copy,通过苹果开源的 libclosure 源码,看到了 _Block_copy 的调用逻辑:

_Block_copy
  _Block_copy_internal  
      // if it‘s a stack block.  Make a copy. 
      malloc
      memmove
      _Block_call_copy_helper

_Block_call_copy_helper 函数会取出 block 结构体中的 descriptor,调用它的 copy 函数指针指向的函数,也就是上面转换出来的 C++ 代码中的__main_block_copy_0 函数。

__main_block_copy_0 函数内部调用了 _Block_object_assign ,上一节中我们看到了当 block 捕获的是栈上的__block 变量时,那就走 BLOCK_FIELD_IS_BYREF 的 case。而这里捕获的是 Objective-C 对象,_Block_object_assign 的第三个参数就变成了 BLOCK_FIELD_IS_OBJECT
objc4 runtime 源码中的定义:

// Runtime entry point called by compiler when assigning objects inside copy helper routines
BLOCK_EXPORT void _Block_object_assign(void *destAddr, const void *object, const int flags);
    // BLOCK_FIELD_IS_BYREF is only used from within block copy helpers

// runtime entry point called by the compiler when disposing of objects inside dispose helper routine
BLOCK_EXPORT void _Block_object_dispose(const void *object, const int flags);

libclosure 中的源码实现:

void _Block_object_assign(void *destAddr, const void *object, const int flags) {
    switch (osx_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
            
        _Block_retain_object(object);
        _Block_assign((void *)object, destAddr);
        break;
        ...
        ...
}

objc4 runtime 源码中定义了一个枚举:

// Runtime support functions used by compiler when generating copy/dispose helpers

enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

结论

  • 当 Block 从栈复制到堆上时,_Block_object_assign 被调用,_Block_object_assign 将捕获到的对象赋值给 block 的结构体成员变量 array,并持有该对象,这个函数相当于 objc_retain
  • 当 Block 从堆中被销毁掉时,_Block_object_dispose被调用,_Block_object_dispose 会释放 block 的结构体成员变量 array,也就是捕获到的对象,这个函数相当于 objc_release

补充
在示例代码中添加符号断点进行调试可以看到 _Block_object_assign_Block_object_dispose 的调用堆栈(如下图所示)。
image

image

另外,在 iOS 中的 block 是如何持有对象的这篇文章中,也提到了在 block 被销毁时,dispose_helper 会负责向所有捕获的变量发送 release 消息:

其实最开始笔者对这个 dispose_helper 实现的机制并不是特别的肯定,只是有一个猜测,但是在询问了 FBBlockStrongRelationDetector 的作者之后,才确定 dispose_helper 确实会负责向所有捕获的变量发送 release 消息,如果有兴趣可以看这个 issue。这部分的代码其实最开始源于 mikeash 大神的 Circle,不过对于他是如何发现这一点的,笔者并不清楚,如果各位有相关的资料或者合理的解释,可以随时联系我。

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 7, 2021

二、Block 的实现(八)

8. 在对象类型的变量上使用 __block 时发生了什么

int main() {
    
    typedef void(^blk_t) (id obj);
    
    blk_t blk;
    {
        // 带有 __block 的  Objective-C 对象
        __block id array = [[NSMutableArray alloc] init];
        blk = ^void(id obj) {
            [array addObject:obj];
            
            NSLog(@"array count = %ld", [array count]);
        };
    }
    
    blk([[NSObject alloc] init]);
    return 0;
}

转成 C++ 代码:

/// 捕获的 __block 对象类型变量被转成了结构体
struct __Block_byref_array_0 {
  void *__isa;
__Block_byref_array_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);  // 比 __block C 语言变量多了两个管理内存的函数指针
 void (*__Block_byref_id_object_dispose)(void*);
 id array;    // 保存原来的 array 对象的引用
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
  __Block_byref_array_0 *array = __cself->array; // bound by ref

            ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)(array->__forwarding->array), sel_registerName("addObject:"), (id)obj);

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_nt_bkkycgbs2hv63tthr5dbd2g40000gn_T_main8_cb5b3d_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)(array->__forwarding->array), sel_registerName("count")));
}

int main() {

    typedef void(*blk_t) (id obj);

    blk_t blk;

    {
        __attribute__((__blocks__(byref))) __Block_byref_array_0 array = {(void*)0,(__Block_byref_array_0 *)&array, 33554432, sizeof(__Block_byref_array_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"))};

        blk = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_array_0 *)&array, 570425344));
    }


    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));

    return 0;
}

block 被转成了结构体 __main_block_impl_0

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 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
};

// block 被转成结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_array_0 *array; // 保存了指向捕获对象 array 转换后的结构体的指针
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_array_0 *_array, int flags=0) : array(_array->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结论

  • block 捕获 __block 对象类型变量,跟 block 捕获 __block C 语言变量的实现差不多,都是用一个新的”对象“去保存原来的变量值,不通点在于,block 捕获 __block 对象类型变量时,这个新的对象 __Block_byref_array_0 多了两个管理内存的函数指针
  • block 从栈上拷贝到堆上时,block 会持有 __block 对象类型变量(就是__Block_byref_array_0对象),__block 对象类型变量会持有被赋值的对象。

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 7, 2021

三、Block 循环引用

1. Block 循环引用

(书 p128~p134)

  • 两种 case
    • self 持有 block,block 捕获并持有 self
    • self 持有 block,block 捕获 __block 临时变量,__block 临时变量持有 self
  • 如何解决
    • 使用 __weak(非 ARC 下使用 __unsafe_unretained),编译器会自动不持有捕获的变量
    • block 调用完成后手动将 block 置为 nil
    • 使用 __block 时,需要在 block 回调中将 __block 变量赋值为 nil,手动打断循环引用环

苹果的官方文档 "Transitioning to ARC Release Notes" 中的建议如下:

In manual reference counting mode, __block id x; has the effect of not retaining x. In ARC mode, __block id x; defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use __unsafe_unretained __block id x;. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use __weak (if you don’t need to support iOS 4 or OS X v10.6), or set the __block value to nil to break the retain cycle.

2. Strong-Weak Dance

"Transitioning to ARC Release Notes" 中有一条这样的建议:

For non-trivial cycles, however, you should use:

MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES > completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

官方文档也没有讲清楚为什么要在 block 里面用 strongMyController

不过在 bs 的这篇文章中讲了一个场景:self 在主线程中被释放了,但是捕获了 self 的 block 是在子线程中调用的,这个时候就需要使用 strong 来“保活”了。

__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
  __strong __typeof(wself) sself = wself; // 强引用一次
  [sself.property removeObserver: sself forKeyPath:@"pathName"];
};

我们可不可以换种方式,不用 storngSelf,而是在 block 内部通过判断 self 是不是 nil 来决定要不要继续往下走?答案是不行的,因为可能在子线程执行判断 if 语句时还没有被释放,真正去用的时候却释放了。

顺便讲一下另外一个 strongSelf 的问题,虽然跟 block 没有关系, sunnyxx 在 ARC对self的内存管理 这篇文章中,就 YTKNetwork 中使用 self 出现崩溃的真实案例进行了分析,原因是 ARC 下,为了优化性能,针对方法中的 self 参数做了内存管理简化:

ARC下,self 既不是 strong 也不是weak,而是 unsafe_unretained 的,也就是说,入参的 self被表示为:(init 系列方法的 self 除外)

- (void)start {
   const __unsafe_unretained YTKRequest *self;
   // ...
}

所以,有可能方法执行到一般,当前的对象已经被释放了,但是传参进来的 self 没有被置为 nil,所以就出现 BAD_ACCESS(野指针?垂悬指针?)错误了。

3. RAC 中的 @weakify@strongify

这两个宏展开后实际上就是:

// DEBUG
autoreleasepool {} __weak __typeof__(self) self_weak_ = self;
autoreleasepool {} __strong __typeof__(self) self_strong_ = self;
// 非 DEBUG
try {} @catch (...) {}  __weak __typeof__(self) self_weak_ = self;
try {} @catch (...) {}  __strong __typeof__(self) self_strong_ = self;

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 7, 2021

四、非 ARC 下的 block 与 copy/release

(1)手动调用 copy/release
非 ARC 下,可以通过调用 copy 方法或者 Block_copy 函数来将 block 从栈上复制到堆上,以及持有 block:

void (^blk_on_heap)(void) = [blk_on_stack copy];  // 从栈上复制到堆上,blk_on_heap 持有该 block
void (^blk_on_heap_1)(void) = [blk_on_heap copy];   // 引用计数增加

非 ARC 下,可以通过调用 release 方法或者 Block_release 函数来释放堆上的 block:

[blk_on_heap release];

(2)非 ARC 下,__block 对象类型的变量

不管是 ARC 和非 ARC,下面的代码都会导致循环引用,因为 self 持有 block,block 持有 self:

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
    blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^{
        NSLog(@"self = %@", self);
    };
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end

int main() {
    id o = [[MyObject alloc] init];
    NSLog(@"%@", o);
    return 0;
}

不过非 ARC 下,可以用 __block 来避免循环引用。当 block 从栈复制到堆中时,block 会持有捕获的对象类型变量,但是如果对象类型变量带有 __block, block 就不会持有该__block变量,这样就打破循环引用了,不管__block变量是否持有被赋值的对象。

- (id)init {
    self = [super init];
    __block id tmp = self;
    blk_ = ^{
        NSLog(@"self = %@", tmp);
    };
    return self;
}

苹果的官方文档 "Transitioning to ARC Release Notes" 中的建议如下:

In manual reference counting mode, __block id x; has the effect of not retaining x. In ARC mode, __block id x; defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use __unsafe_unretained __block id x;. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use __weak (if you don’t need to support iOS 4 or OS X v10.6), or set the __block value to nil to break the retain cycle.

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 7, 2021

五、Q&A

1. block 的嵌套

Clang 文档中有如下描述:

Nesting

Blocks may contain Block literal expressions. Any variables used within inner blocks are imported into all enclosing Block scopes even if the variables are not used. This includes const imports as well as __block variables.

2. block 的递归使用问题

    int (^completion)(int);
    __block __weak int (^weakCompletion)(int);
    weakCompletion = completion = ^int(int a) {
        if (a == 0) {
            return 1;
        } else {
            return a * weakCompletion(a-1);
        }
    };

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant