iOS 升级打怪 - block

1,083 阅读6分钟

block 底层结构

block,在日常开发中经常遇到。常用的使用场景就是用于两个对象之间的交互,比如 vc 之间的传值、网络接口的回调等。

先来一个最简单的例子来看下 block 的底层结构,代码示例:

^{
     NSLog(@"hello world");
}();

通过 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp编译成 C++ 代码:

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

通过上面的 isa 可以看出,block 是一个 _NSConcreteStackBlock 类型的对象。

变量捕获

局部变量

  • auto
int num = 1;
void (^block)(void) = ^(){
     NSLog(@"num value is: %d", num);
};

num = 2;
block();

上述代码的打印结果:num value is: 1,虽然在 block 调用之前修改了 num 的值,但在打印上可以看出 num 的值仍然为 1。下面将代码编译成 C++ 来看一下原因:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 结构与上面大致相同,只多了一个 num 的变量。
  int num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    // 实际调用的是 block 内部捕获的 num
    int num = __cself->num; // bound by copy
    
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_b7ef04_mi_0, num);
}

int num = 1;
// block 声明
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));

在 block 的声明,可以看到局部变量 num 的值已经传递给 block 结构体 __main_block_impl_0 ,内部的 num 变量。所以,后面即使修改 num 的值,也不影响 block 里面已捕获的值。

  • static
static int num = 1;
......
// 其余代码相同

输出结果:num value is: 2。可以看到这次 num 修改起作用了。还是编译成 C++ 看一下原因:

struct __main_block_impl_0 {
......
  int *num;
......
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  // 取出 num 的内存地址
  int *num = __cself->num; // bound by copy

             NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_cede7d_mi_0, (*num));
}

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

这次可以看到 num前面加了一个 * ,代表它是指针类型。从 &num 传入也可以得知它传入的是 static 修饰的 num 的内存地址。因为 __main_block_impl_0 里面的 num 和 static 修饰的 num 是指向一块内存,所以 num = 2 的修改是生效的。

全局变量

C++ 代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_933c32_mi_0, num);
}

可以看到,没有从 __main_block_impl_0 取值,而是直接访问的 num 。因为 num 是全局变量,所以可以直接访问。

局部变量 auto、 static;全局变量的捕获方式总结图:

变量捕获.png

block 类型

block 一共有三种类型,都继承自 NSBlock:

  • NSGlobalBlock:没有访问 auto 变量的即为 global。存放在数据段。
void (^block)(void) = ^(){
    NSLog(@"hello, world");
};

// __NSGlobalBlock__, NSBlock
NSLog(@"%@, %@", [block class], [[block class] superclass]); 
  • NSStackBlock:访问了 auto 变量的为 stack。存放在栈上。
// __NSStackBlock__, NSBlock
NSLog(@"%@,%@", [^(){ NSLog(@"num value is: %d", num); } class],
              [[^(){ NSLog(@"num value is: %d", num); } class] superclass]);
              
  • NSMallocBlock:stack 调用了 copy 函数的即为 malloc。存放在堆上。
// __NSMallocBlock__,NSBlock
NSLog(@"%@,%@", [[^(){ NSLog(@"num value is: %d", num); } copy] class],
              [[[^(){ NSLog(@"num value is: %d", num); } copy] class] superclass]);

block 的 copy 操作

在 ARC 环境下,如果出现以下情况,编译器会自动将栈上的 block 拷贝到堆上,即类型从 __NSStackBlock__ 变成 __NSMallocBlock__

  • block 为方法的返回值。
// .h
typedef void (^SomeBlock) (void);
@interface Goods : NSObject

- (SomeBlock)test;

@end
// .m
- (SomeBlock)test {
    int a = 1;
    return ^{
        NSLog(@"test -- %d", a);
    };
}
// main
Goods *g = [Goods new];
NSLog(@"%@", [[g test] class]); // __NSMallocBlock__
  • block 被强指针指向。
int num = 1;
void (^block)(void) = ^(){
    NSLog(@"num value is: %d", num);
};
NSLog(@"%@", [block class]); //__NSMallocBlock__
  • 系统方法中含有 usingBlock 的参数。
  • 作为 GCD 方法的参数。

block 内部访问对象类型的 auto 变量

如果在 block 内部访问了对象类型的 auto 变量,那么在 __main_block_desc_0 结构体里面会添加 copy 、dispose 函数来进行对象的内存管理。比如下面的代码:

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

编译成 C++ 的代码,结构体 __main_block_desc_0 代码如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  // 对比上面的基本类型的 auto 变量,多了如下两个函数
  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 是存在栈上,那么它并不会对变量进行强引用;如果 block 在堆上的话则会根据变量的修饰符(strong/weak)来进行强或者弱引用。

当 block 在堆上时,它会调用内部的 copy 函数,copy 内部调用 _Block_object_assign 函数来进行对象变量的内存管理。

当 block 在堆上移除时,它会调用内部的 dispose 函数,dispose 内部调用 _Block_object_dispose 函数来释放对象变量。

__block

使用场景:解决 block 内部无法修改 auto 变量值的问题,比如下面的代码:

NSObject *obj = [NSObject new];
void (^block)(void) = ^(){
    obj = nil;
    NSLog(@" %@", obj);
};
block();

编译器会报错:Variable is not assignable (missing __block type specifier)。obj 添加一个 __block 就可解决该问题。

如果没有添加 __block,block 内部会捕获 obj 的值,并在堆上开辟一块内存空间来存储。外部的 obj 存放在栈上,内部的 obj 存放在堆上,虽然它俩名字一样,但并不是一块地址。所以在内部不能修改 obj 的值。

未添加 __block 打印 obj 的内存地址:

NSObject *obj = [NSObject new];
NSLog(@"0000 %p", &obj); // 0x7ffeefbff3f8 栈
void (^block)(void) = ^(){
    NSLog(@"1111 %p", &obj); // 0x10304d6f0 堆
};
block();
NSLog(@"2222 %p", &obj); // 0x7ffeefbff3f8 栈

添加 __block 之后,__block 会捕获 obj 的内存地址,并在堆上开辟一块内存空间来存储 obj,然后将 block 外部的变量内存地址也改成堆上的。这样内部的 obj 和外部的 obj 指向的是同一块内存,自然就可以在内部修改 obj 的值了。

添加 __block 打印 obj 的内存地址:

__block NSObject *obj = [NSObject new];
NSLog(@"0000 %p", &obj); // 0x7ffeefbff3f8 栈
void (^block)(void) = ^(){
    NSLog(@"1111 %p", &obj); // 0x100507ce8 堆
};
block();
NSLog(@"2222 %p", &obj); // 0x100507ce8 栈

上述图示:

__block.png

需要注意的是,__block只能修饰 auto 变量不可修饰 static 和全局变量。

循环引用

block 的使用过程中,代码不规范可能会造成循环引用,从而导致内存泄漏。比如下面的代码:

@interface Goods : NSObject
@property(nonatomic, copy)SomeBlock block;
@property(nonatomic, copy)NSString *name;
@end
Goods *g = [Goods new];
g.block = ^{
    NSLog(@"%@", g.name);
};

因为 g 强引用了 block,而 block 内部又强引用了 g,形成循环引用导致 g 无法被释放。可以通过 __weak、__unsafe_unretained、__block 解决。再次只用 __weak 举例:

Goods *g = [Goods new];
__weak typeof(g) weakG = g;
g.block = ^{
    NSLog(@"%@", weakG.name);
};

总结

  • 局部 auto 变量为值捕获;局部 static 变量为指针捕获;全局变量直接访问。
  • block 三种类型 Global、Stack、Malloc,都继承自 NSBlock。
  • block 为返回值、强指针指向和某些系统方法的参数时,编译器会自动从栈上拷贝到堆上。
  • __block 修饰的 auto 变量, 可以在 block 内部修改 auto 变量值。
  • __weak/__unsafe_unretained/__block 用来解决循环引用问题。