理清 Block 底层结构及其捕获行为

2,630 阅读6分钟

Block 的本质

本质

  1. Block 的本质是一个 Objective-C 对象,它内部也拥有一个 isa 指针。
  2. Block 是封装了函数及其调用环境的 Objective-C 对象

底层数据结构

一个简单示例:

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

    void (^block)(void) = ^{
        NSLog(@"hey");
    };
    block();
    return 0;
}

将以上 Objective-C 源码转换成 c++ 相关源码,使用命令行 : xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名

c++ 的结构体与一般的类相似。

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

其中 Block 的数据结构为:

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

impl 变量数据结构:

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

FuncPtr:函数实际调用的地址,因为 Block 可看作是捕获自动变量的匿名函数。

Desc 变量数据结构:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

Block 的类型

Objective-C 中 Block 有三种类型,其最终类型都是 NSBlock 。

  • NSGlobalBlock (_NSConcreteGlobalBlock)
  • NSStackBlock (_NSConcreteStackBlock)
  • NSMallocBlock (_NSConcreteMallocBlock)

Block 类型的不同,主要根据捕获变量的不同行为产生:

Block 类型 行为
NSGlobalBlock 没有访问 auto 变量
NSStackBlock 访问 auto 变量
NSMallocBlock NSStackBlock 调用 copy

在内存中的存储位置

内存五大区:栈、堆、静态区(BSS 段)、常量区(数据段)、代码段

copy 行为

不同类型的 Block 调用 copy 操作,也会产生不同的复制效果:

Block 类型 副本源的配置存储域 复制效果
__NSConcreteStackBlock 从栈复制到堆
__NSConcreteGlobalBlock 数据段(常量区) 什么也不做
__NSConcreteMallocBlock 引用计数增加
  • 在 ARC 环境下,编译器会在以下情况自动将栈上的 Block 复制到堆上:
  1. Block 作为函数返回值
  2. 将 Block 赋值给 __strong 指针
  3. 苹果 Cocoa、GCD 等 api 中方法参数是 block 类型

在 ARC 环境下,声明的 block 属性用 copy 或 strong 修饰的效果是一样的,但在 MRC 环境下,则用 copy 修饰。

捕获变量

为了保证在 Block 内部能够正常访问外部变量,Block 有一套变量捕获机制:

变量类型 是否捕获到 Block 内部 访问方式
局部 auto 变量 值传递
局部 static 变量 指针传递
全局变量 直接访问

若局部 static 变量是基础类型 int val ,则访问方式为 int *val 若局部 static 变量是对象类型 JAObject *obj ,则访问方式为 JAObject **obj

基础类型变量

一个简单示例:

int age = 10;
// static int age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", age);
};
block();
  • 捕获局部 auto 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int age; // 传递值
}
  • 捕获局部 static 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int *age; // 传递指针
}
  • 捕获全局基础类型变量生成的结构体 struct __main_block_impl_0 没有包含 age ,因为作用域为全局,可直接访问。

对象类型变量

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
  • 捕获局部 auto 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
  • 捕获局部 static 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson **person;
}
  • 捕获全局对象类型变量生成的结构体 struct __main_block_impl_0 没有包含 person ,因为作用域为全局,可直接访问。

copy 和 dispose 函数

当捕获的变量是对象类型或者使用 __Block 将变量包装成一个 __Block_byref_变量名_0 类型的 Objective-C 对象时,会产生 copydispose 函数。

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();

其中生成的 Block 的数据结构中多了 JAPerson 类型指针变量 person :

struct __main_block_impl_0 {
  ···
  JAPerson *person;
}

Desc 变量数据结构多了内存管理相关的函数:

static struct __main_block_desc_0 {
  ···
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}

这两个函数的调用时机:

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

copy 和 dispose 底层相关源码

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

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

当 Block 内部访问了对象类型的 auto 变量时:

  • 如果 Block 是在栈上,将不会对 auto 变量产生强引用。
  • 如果 Block 被拷贝到堆上,会调用 Block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretain)作出相应的内存管理操作。

注意:若此时变量类型为对象类型,这里仅限于 ARC 时会 retain ,MRC 时不会 retain 。

  • 如果 Block 从堆上移除,会调用 Block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动 release 引用的 auto 变量。

使用 __weak 修饰的 OC 代码转换对应的 c++ 代码会报错: error: cannot create __weak reference because the current deployment target does not support weak references 此时终端命令需支持 ARC 并指定 Runtime 版本: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

内存管理

修改局部 auto 变量

局部 static 变量(指针访问)、全局变量(直接访问)都可以在 Block 内部直接修改捕获的变量,而局部 auto 变量则主要通过使用 __block 存储域修饰符来修改捕获的变量。

  • __block 修饰符可以用于解决 Block 内部无法修改局部 auto 变量值的问题
  • __block 修饰符不能用于修饰全局变量、静态变量(static)

编译器会将 __block 修饰的变量包装成一个 Objective-C 对象。

一个简单示例:

__block int age = 10;
void (^block)(void) = ^{
   NSLog(@"age is %d", age);
};
block();

其中 Block 的数据结构多了一个 __Block_byref_age_0 类型的指针:

struct __main_block_impl_0 {
  ···
  __Block_byref_age_0 *age; // by ref
}

__Block_byref_age_0 结构体:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age; // age 真正存储的地方
};

两个注意点:

    1. 此处指针 val 是指向 age 的指针,而第二个 val 指的是 age 的值。

    1. 源码里面通过 age->__forwarding->age 的方式去取值,是因为这两个 age 都可能仍在栈上,此时直接 age->age 访问会有问题,而 copy 操作时 __forwarding 会指向堆上的 __Block_byref_age_0 ,此时就算第一个 age 仍在栈上,通过 age->__forwarding 会重新指向堆上的 __Block_byref_age_0 ,此时再访问 age 便不会有问题 age->__forwarding->age

__block 的内存管理

使用 __block 修饰符时的内存管理情况:

  • 当 Block 存储在栈上时,并不会对 __block 变量强引用。
  • 当 Block 被 copy 到堆上时,会调用 Block 内部的 copy 函数,copy 函数会调用 __main_block_copy_0 函数对 __block 变量产生一个强引用。如下图

  • 当 Block 从堆上被移除时,会调用 Block 内部的 dispose 函数,dispose 函数会调用 _Block_object_dispose 函数自动 release __block 变量。如下图

__weak 和 __block 修饰时的引用情况

    1. 仅用 __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
    NSLog(@"person‘s age is %d", weakPerson.age);
};

    1. 使用 __block __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__block __weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
     NSLog(@"person‘s age is %d", weakPerson.age);
};
block();
return 0;

循环引用

常见的循环引用问题:

ARC 环境下解决循环引用

    1. 弱引用持有:使用 __weak 或 __unsafe__unretain 解决

    1. 手动将一方置为 nil :使用 __block 解决,在 block 内部将一方置为 nil ,因此必须执行该 block

MRC 环境下解决循环引用

    1. 弱引用持有:使用 __unsafe__unretain 解决
    1. 直接使用 __block 解决,无需手动将一方置为 nil ,因为底层 _Block_object_assign 函数在 MRC 环境下对 block 内部的对象不会进行 retain 操作。