谈谈 Block

934 阅读8分钟
原文链接: www.jianshu.com

主要内容:

  1. 捕获基本类型或对象
  2. __block 修饰符
  3. _NSConcreteGlobalBlock_NSConcreteStackBlock 以及 _NSConcreteMallocBlock
  4. Block 的结构
  5. Block 的复制
  6. Block 的循环引用

先放上几篇我看的文章:

  1. Language Specification for Blocks
  2. Block Implementation Specification
  3. A look inside blocks: Episode 1
  4. A look inside blocks: Episode 2
  5. A look inside blocks: Episode 3 (Block_copy)

捕获变量

主要是关于捕获自动变量的值,如果是全局变量,因为作用域的原因也就没有必要去捕获它。

测试一下以下两个局部变量是如何被捕获的:

int main() {
  static int value1 = 10;
  int value2 = 20;

  void (^blockDemo1)(void) = ^{
    NSLog(@"%d %d", value1, value2);
  };
  blockDemo1();
}

借助 clang-rewrite-objc 命令可以得到 C++ 的代码(部分省略):

struct __main_block_impl_0 {
  ...
  int *value1;
  int value2;
  __main_block_impl_0(..., int *_value1, int _value2, ...) : value1(_value1),
                                                               value2(_value2) 
  {...}
};

从构造方法中可以看到,对于普通的自动变量,block 捕获了它的值(值传递),对于静态自动变量 block 捕获了对它的引用(引用传递)。也正因为这样,不能在 block 中对 value2 这种普通的自动变量的值做出改变。

无论是基本的数据类型的变量还是后文的对象,都成为了 block 结构体的成员变量。

捕获对象

之前的例子中 block 都是捕获基本类型的变量,下面是关于捕获对象的一些例子。

虽然在 block 中不能对捕获的自动变量做赋值、运算等等操作,但是如果该变量是一个对象,则可以调用该对象的实例方法:

NSMutableArray *array = [NSMutableArray array];
void (^blockDemo2)(id) = ^(id obj){
  [array addObject:obj];             // 可以
  // array = [NSMutableArray array]; 不可以
};
blockDemo2(obj);

使用 clang -rewrite-objc -fobjc-arc xxxx.m 命令之后得到部分代码如下:

struct __main_block_impl_0 {
  ...
  NSMutableArray *__strong array;
  ...
};

如果将 NSMutableArray *array = [NSMutableArray array] 改为 __weak 或者 __unsafe_unretained,该 block 结构体中也会有一样的修改。

也正是因为 block 对 array 有强引用,array 可以超出作用域存在,以下代码可以正常运行。

block blockDemo9;
{
  NSMutableArray *array = [NSMutableArray new];
  blockDemo9 = ^(id obj){
    [array addObject:obj];
    NSLog(@"%lu", [array count]);
  };  
}
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);

修改一下 blockDemo9

block blockDemo9;
{
  NSMutableArray *array = [NSMutableArray new];
  NSMutableArray __weak *array2 = array;
  blockDemo9 = ^(id obj){
    [array2 addObject:obj];
    NSLog(@"%lu", [array2 count]);
  };  
}
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);

因为 blockDemo9__weak 的形式持有对 array2 的弱引用,所以当 array 的作用域结束,强引用失效,该数组被销毁后 array2 被赋值为 nil,block 中打印的 count 都是0。

__block 修饰符

带有 __block 修饰符的自动变量可以在 block 中被修改:

__block int value = 10;
__block NSMutableArray *array = [NSMutableArray array];

void (^blockDemo3)(void) = ^{
  array = [NSMutableArray array];
  value++;
};
blockDemo3();

使用 clang -rewrite-objc -fobjc-arc xxxx.m 命令之后的代码如下:

struct __Block_byref_value_0 {
  ...
  int value;
};
struct __Block_byref_array_1 {
  ...
  NSMutableArray *__strong array;
};
struct __main_block_impl_0 {
  ...
  __Block_byref_array_1 *array; 
  __Block_byref_value_0 *value; 
  ...
};

__block 修饰的变量会产生一个该变量的结构体,在这里分别是 __Block_byref_value_0__Block_byref_array_1,各自有成员变量 valuearray。这两个结构体又作为 block 的成员变量。

Block 的类

block 有以下三种类型:

_NSConcreteStackBlock_NSConcreteMallocBlock 以及 _NSConcreteGlobalBlock,分别存储在栈、堆以及静态存储区。

1. _NSConcreteGlobalBlock

第一种情况:全局范围内的 block 是 _NSConcreteGlobalBlock

第二种情况:如果没有捕获自动变量或者 __block 修饰的自动变量,那就是 _NSConcreteGlobalBlock,前面那个 blockDemo1 因为捕获了自动变量 value2,所以就不是 _NSConcreteGlobalBlock

static int value1 = 10;         // Global
int value2 = 20;                // Global

int main() {
  static int value3 = 30;        // Global
  int value4 = 40;                 // 非 Global
  __block int value5 = 50;        // 非 Global
}

2. _NSConcreteStackBlock

在 ARC 的情况下,应该基本不会有该类型的 block 出现,而是会自动被拷贝到堆成为 _NSConcreteMallocBlock 的类型。

上文提到的 blockDemo1 在 MRC 下就是 _NSConcreteStackBlock 类型。

3. _NSConcreteMallocBlock

block invokeBlock() {
  int value = 10;
  block blk = ^{
    NSLog(@"%d", value);
  };
  return blk;
}
int main() {
  block blockDemo4 = invokeBlock();
  blockDemo4();
}

在 MRC 模式下,变量 blk 是一个存储在栈上的 block,也就是说,invokeBlock 的作用域结束之后,该变量随时会被销毁,这时候如果在 main 方法中调用 blockDemo4 可能会出现错误。

在这种情况下,需要让 block 超出作用域而继续存在,就需要将 block 拷贝到堆。在 MRC 模式下需要调用 copy 方法,在 ARC 模式下,blk 会自动被拷贝到堆。

int main() {
  int value = 10;
  block blockDemo5 = ^{
    NSLog(@"%d", value);
  };
  blockDemo5();
}

blockDemo5 默认是被 __strong 修饰的,所以会被自动拷贝到堆,即是 _NSConcreteMallocBlock 类型,如果是被 __weak 或者 __unsafe_unretained 修饰,则依然是 _NSConcreteStackBlock 类型。

Block 的结构

在整篇文章中,用 clang -rewrite-objc 得到的 C++ 代码只能作为一个参考,而不能当作 block 真正的实现,block 真正的实现可以在这里找到。

目前在网上别的博客看到的 block 结构体是这样的:

struct Block_descriptor {
  unsigned long int reserved;
  unsigned long int size;
  void (*copy)(void *dst, void *src);
  void (*dispose)(void *);
};
struct Block_layout {
  void *isa;
  int flags;
  int reserved;
  void (*invoke)(void *, ...);
  struct Block_descriptor *descriptor;
  /* Imported variables. */
};

但是翻了一下源码,现在 block 的结构体应该是发生了改变,上述代码中的 Block_descriptor 结构体被分成了3个结构体,部分代码像下面这样:

struct Block_descriptor_1 {
  uintptr_t reserved;
  uintptr_t size;
};
struct Block_layout {
  void *isa;
  volatile int32_t flags; // contains ref count
  int32_t reserved; 
  void (*invoke)(void *, ...);
  struct Block_descriptor_1 *descriptor;
  // imported variables
};

先给一个简单的 block:

int main() {
  block blk = ^{
    NSLog(@"block");
  };
  blk();
}

使用 clang -S -x objective-c -arch armv7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.0.sdk xxxx.m 命令将代码编译成汇编代码(个人不太懂汇编)。

首先看一下 ___block_literal_global,这个其实就是 blk 的实现:

___block_literal_global:
  .long   __NSConcreteGlobalBlock
  .long   1342177280              ## 0x50000000
  .long   0                       ## 0x0
  .long   ___main_block_invoke
  .long   ___block_descriptor_tmp
  1. _NSConcreteGlobalBlock 其实就是 Block_layoutisa 的值,该值也可以是 _NSConcreteMallocBlock 以及 _NSConcreteStackBlock
  2. ___main_block_invoke 就是调用 block 的函数指针。

Block 的复制

基本类型的变量

首先,以 blockDemo6 为例,当这个 block 从栈复制到堆的时候会调用 objc_retainBlock 方法,随后调用 _Block_copy 方法。

int main() {
  int value = 10;
  block blockDemo6 = ^{
    NSLog(@"%d", value);
  };
  blockDemo6();
}

value 仅仅是作为 block 的结构体变量从栈被拷贝到了堆。如果 block 捕获的是一个对象呢。

对象

int main() {
  NSObject *obj = [NSObject new];
  block blockDemo10 = ^{
    NSLog(@"%@", obj);
  };
  blockDemo10();
}

C++ 代码如下:

struct __main_block_impl_0 {
  ...
  NSObject *__strong obj;
  ...
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, 
                                struct __main_block_impl_0*src)
{
  _Block_object_assign((void*)&dst->obj, 
                       (void*)src->obj, 
                       3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
  _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

使用 clang -rewrite-objc 命令之后多了两个方法,分别是 __main_block_copy_0__main_block_dispose_0

不过,我用 clang -S -fobjc-arc xxxx.m 命令看了下,好像并没有调用这两个方法。obj 这个变量本身就在堆内存中,当 block 从栈拷贝到堆的时候应该也就没必要调用这两个方法了。

__block 变量

如果存在一个 __block 变量,又会是不一样的情况:

int main() {
  __block int value = 10;
  __block NSObject *obj = [NSObject new];
  block blockDemo7 = ^{
    NSLog(@"%d %@", value, obj);
  };
  blockDemo7();
}

value 变量会生成一个在栈上的结构体,该结构体中包含 value 这个成员变量,当 block 被拷贝到堆的时候,value 这个结构体也会被拷贝到堆。同理,obj 这个变量也是一样。

以下是比较完整的 clang -rewrite-objc 之后的代码:

// value 结构体
struct __Block_byref_value_0 {
  ...
  __Block_byref_value_0 *__forwarding;
  ...
  int value;
};
// obj 结构体
struct __Block_byref_obj_1 {
  ...
  __Block_byref_obj_1 *__forwarding;
  ...
  void (*__Block_byref_id_object_copy)(void*, void*);
  void (*__Block_byref_id_object_dispose)(void*);
  NSObject *__strong obj;
};
// block
struct __main_block_impl_0 {
  ...
  __Block_byref_value_0 *value;
  __Block_byref_obj_1 *obj; 
  ...         
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, 
                                struct __main_block_impl_0*src)
{
  _Block_object_assign((void*)&dst->value, 
                       (void*)src->value, 
                       8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->obj, 
                       (void*)src->obj, 
                       8/*BLOCK_FIELD_IS_BYREF*/);
}

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

如果 block 捕获了 __block 变量或者对象,就会产生 __main_blcok_copy_0__main_block_dispose_0 两个方法。当 __block 变量被拷贝到堆或者被销毁的时候就会调用这两个方法。

关于 __forwarding 变量

继续 blockDemo7 的例子,如果在 block 中需要访问变量,实际上会以 value->__forwarding->value 的形式访问。

blockDemo7 修改一下:

int main() {
  __block int value = 10;
  block blockDemo8 = ^{
    value++;
  };
  value++;
  blockDemo8();
}

blockDemo8 的 C++ 代码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; 
  (value->__forwarding->value)++;
}
int main() {
  ...
  (value.__forwarding->value)++;
  ...
}

无论在 block 内部还是外部,都是通过先访问 __forwarding,再访问成员变量 value 的方式,而 __forwarding 仅仅是一个指向自身的指针。但是为什么需要 __forwarding 呢?

因为通过 block 从栈复制到堆,被 block 持有的 __block 变量也会从栈复制到堆,也就是说这个时候在栈和堆上会同时存在 __block 变量的结构体。

如果不存在 __forwarding 这个变量,在 block 内部访问的是堆上的 __block 变量的结构体,而在 block 外部访问的则是在栈上的,而这两个结构体的值可能是不一样的。所以需要一个 __forwarding 变量,当 __block 变量从栈拷贝到堆的时候,栈上的 __forwarding 会指向堆上的结构体,保证 block 内外访问同一个变量。

Block 的循环引用

typedef void (^block)(void);

@interface Person : NSObject
@property (nonatomic, copy) block    personBlock;
@end

@implementation Person
- (void)dealloc {
  NSLog(@"dealloc");
}
- (void)invokeBlock {
  self.personBlock = ^{
    NSLog(@"%@", self);
  };
  self.personBlock();
}
@end

int main() {
  Person *p = [Person new];
  [p invokeBlock];
}

在上面的代码中,肯定不会调用 dealloc 方法,因为很明显存在循环引用。

以下是部分 C++ 代码:

struct __Person__invokeBlock_block_impl_0 {
  ...
  Person *const __strong self;
  __Person__invokeBlock_block_impl_0(..., Person *const __strong _self, ...) : self(_self) 
  {...}
};
static void __Person__invokeBlock_block_func_0(struct __Person__invokeBlock_block_impl_0 *__cself) {
  Person *const __strong self = __cself->self;
  ...
}

从上面的代码可以看到,selfconst __strong 的形式被 block 捕获,应该是编译器搞的鬼吧,因为 self 这个变量本身应该既没有被 __strong 修饰也没有被 __weak 修饰,没有受到 ARC 的管理(参考:Objective-C Automatic Reference Counting (ARC)ARC对self的内存管理)。

在第二篇文章中说到 self 是被 _unsafe_unretained 修饰的,但是我英语渣,貌似没有在原文中发现 self 是被哪个关键词修饰。