温故而知新-ObjC Block

657 阅读5分钟

Block

简介

本文主要是回答以下几个问题:

  1. Block 是什么?
  2. Block 不同的类型出现场景是什么?又分别存储在哪?
  3. 截获的变量是如何存储的?
  4. 如何解决循环引用问题?

简洁版的回答:

  1. Block 又称为匿名函数,本质是一个 ObjC 对象,其结构体里会有一个指针指向具体的函数实现。
  2. 它有 3 种类型,分别存储在静态数据区、栈区、堆区。
  3. 截获的变量会直接拷贝到 Block 结构体里,或捕捞其指针。
  4. 一般可使用 weak 和 __block 修饰符来解决循环引用问题。

接下来针对每个答案进行详细阐述。

Block 是什么?

一句话描述:能捕捞局部变量的匿名函数。

但内部实现是怎样的呢?

先写一个简单的 Block

#import <Foundation/Foundation.h>

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

再使用 clang 转换成 c++

clang -rewrite-objc xx.m

得到结果的关键代码如下:

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

// 源码中的 ^{printf("BLOCK\n",);} 转换成以下代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("BLOCK\n");
}

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() {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

再除去构造函数,Block 就变成:

// Block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

// __block_impl 的声明
struct __block_impl {
  void *isa;
  int Flags; // 按 bit 保留表示一些 block 附加信息
  int Reserved; // 保留变量
  void *FuncPtr;
};

// __main_block_desc_0 的声明
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)};

对比一下 ObjC 对象的声明,不难发现 Block 本质也是一种 ObjC 对象。

因为它也有那标志性的 isa 指针。

// Objective-C 对象的声明
// id 为 objc_object 结构体的指针类型
typedef struct objc_object {
  Class isa;
} *id;
// Class 为 objc_class 结构体的指针类型
typedef struct objc_class *Class;
struct objc_class {
  Class *isa;
};

Block 的 3 种类型

Block 的 isa 指向是这 3 个类之一,其实从名字上也可以看出各自存储的区域。

  • __NSStackBlock__ 存储在栈上
  • __NSGlobalBlock__ 存储在静态数据区
  • __NSMallocBlock__ 堆上

各种类型出现的场景如下:

int e = 3;
void (^block)() = ^{
    printf("%c\n", e);
};

NSLog(@"%@", [block class]); // __NSMallocBlock__
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__
NSLog(@"%@", [^{ int a = 4; printf("%c\n", a); } class]); // __NSGlobalBlock__

简单理解,未截获变量是 __NSGlobalBlock__

若截获了变量,则是 __NSStackBlock__,但它却很容易被复制到堆上。

比如以下情况:

  • 当对 Block 对象调用 copy 时。
  • 当 Block 作为返回值返回时。(前提是 ARC 环境)
  • 当 Block 赋值给 strong 对象时。
  • 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。

有时也需要手动添加 copy 方法,来将 block 添加到堆上。

根据不同的 Block,copy 会有不同效果:

Block 类副本源的配置存储域复制效果
_NSConcreteStackBlock从栈复制到堆
_NSConcreteGlobalBlock程序的数据区域Nothing
_NSConcreteMallocBlock引用计数增加

截获的变量是如何存储的?

截获的变量会被添加到 __main_block_impl_0 结构体中。

大概就是这样子:

__block_impl
isa *
Flags
Reserved
FuncPtr
截获的变量 1
截获的变量 2
...

转换后的代码:

int main() {
    int count = 3;

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  int count; // 使用的自动变量被作为成员变量追加到结构体中

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int count = __cself->count; // bound by copy
  printf("BLOCK%d\n", count);
}

__block 说明符

添加__block 之后,数据若改变了,执行 block 也会打印出最新的数据:

没有添加 __block

int n = 3;
void (^block2)() = ^{
    NSLog(@"%d", n);
};
n = 4;
block2(); // 3

添加了__block,会捕获变量的指针。

所以数值改变了,block 执行时,也能看到改变后的数据。

__block int n = 3;
void (^block2)() = ^{
    NSLog(@"%d", n);
};
n = 4;
block2(); // 4

具体的可以看看,转换后的代码。

增加的 __Block_byref_count_0 结构体里就捕获了变量的指针(__forwarding)。

int main() {
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 3};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

// 不在 __main_block_impl_0 中声明的原因是,这样可以在多个 block 中使用
struct __Block_byref_count_0 {
  void *__isa;
  __Block_byref_count_0 *__forwarding;
  int __flags;
  int __size;
  int count;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  __Block_byref_count_0 *count; // by ref

  ...
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref
    (count->__forwarding->count) = 2;
    printf("BLOCK%d\n", (count->__forwarding->count));
}

__forwarding

__forwarding 是添加 __block 后的关键实现。

(为方便叙述,以下将 __forwarding 变量用 f 代替)

当 __block 变量在栈上时,f 指向自己本身的「count」。 image 当 __block 变量复制到堆上时,f 会被复制到堆上,假设为 fm。 此时栈上的 f 会变成指向堆上的「count」,而 fm 也指向堆上的「count」。 image 所以使用 __forwarding 变量,无论在 Block 内外,修改访问的都是堆上的变量数据。

如何解决 Block 循环引用问题

使用 Block 容易引起循环引用,主要通过修饰符 __weak 和 __block 避免。

__weak 比较常见,不再赘述。

使用 __block 的主要优点:

可自由控制将变量转为 nil 的时机

使用 __block 的主要缺点:

必须执行 Block,并在 Block 中,将变量置为 nil,才能避免循环引用。

疑问

以下是笔者在整理这篇文章时,仍有疑惑的地方,若有读者知道,麻烦告知一声。

为什么以下代码,打印出来的类型不同?

int e = 3;
NSLog(@"%@", ^{ printf("%c\n", e); }); // <__NSMallocBlock__: 0x10046b050>
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__

以下分析汇编代码的思路来源于 哈就是我26593

可以看到前一行代码,会调用 objc_retainBlock(),而后者并没有看到相关操作。

picture 2

picture 3

根据 objc_retainBlock — Clang 12 documentation 的解释,该函数会 copy 栈上的 block 到堆上。

至于为什么要调用它,评论区已有几位大佬给出见解,但笔者仍未找到佐证的资料😂。

那为什么要将栈上的 block 复制到堆上呢?

个人理解是,它被堆上的对象「所需要了」,也就是说,需要控制其被销毁的时机了,所以复制到堆上是为了便于管理。

感谢

Block 小测验

苹果官方文档

谈 Objective-C block 的实现 · 唐巧的博客

Block Programming Topics

Block Implementation Specification — Clang 12 documentation

ARC 下 NSStackBlock 去哪了 - 简书

Block 里面的 weak-strong 理解 - 简书