Objective-C block 底层详解

笔者前几天做了一道笔试题:默认情况下 block 是不能修改外面的 auto 变量的,解决办法? 思索了好久才想起可以用 __block 或者 static 修饰符来修饰 auto 变量。看来有必要好好探索一下 block 的相关知识。

目录

1. block 的使用

1.1 什么是 block ?

block 的声明

block 的定义

block 的调用

2. block 的底层数据结构

3. block 的变量捕获机制 

3.1 auto 自动变量

3.2 static 类型的局部变量 

3.3 全局变量

为什么局部变量需要捕获,而全局变量不用呢?

3.4 __block 修饰的变量

3.4.1 __block 的使用:

3.4.2  __block 修饰符


​​​​​​​

1. block 的使用


1.1 什么是 block ?

块,封装了函数调用以及调用环境的 OC 对象。

  • block 的声明

// 1.
@property (nonatomic, copy) void(^myBlock1)(void);
// 2.BlockType:类型别名
typedef void(^BlockType)(void);
@property (nonatomic, copy) BlockType myBlock2;
// 3.
 // 返回值类型(^block变量名)(参数1类型,参数2类型,...)
 void(^block)(void);
  • block 的定义

 // ^返回值类型(参数1,参数2,...){};
 // 1.无返回值,无参数
 void(^block1)(void) = ^{
 
 };
 // 2.无返回值,有参数
 void(^block2)(int) = ^(int a){
 
 };
 // 3.有返回值,无参数(不管有没有返回值,定义的返回值类型都可以省略)
 int(^block3)(void) = ^int{
 return 3;
 };
 // 以上Block的定义也可以这样写:
 int(^block4)(void) = ^{
 return 3;
 };
 // 4.有返回值,有参数
 int(^block5)(int) = ^int(int a){
 return 3 * a;
 };
  • block 的调用

 // 1.无返回值,无参数
 block1();
 // 2.有返回值,有参数
 int a = block5(2);

2. block 的底层数据结构


通过 Clang 将以下的 OC 代码转换为 C++代码

// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
//main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
 @autoreleasepool {
 void(^block)(void) = ^{
 NSLog(@"我是 block");
 };
 block();
 }
 return 0;
}

转换为 C++ 代码 : 

struct __main_block_impl_0 {
 struct __block_impl impl;// block 结构体
 struct __main_block_desc_0* Desc;// block 的描述对象
/*
block 的构造函数
 ** 返回值:__main_block_impl_0 结构体
 ** 参数一:__main_block_func_0 结构体
 ** 参数二:__main_block_desc_0 结构体的地址
 ** 参数三:flags 标识位
*/
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
 impl.isa = &_NSConcreteStackBlock;// &_NSConcreteStackBlock 表示存储在栈上
 impl.Flags = flags;
 impl.FuncPtr = fp;
 Desc = desc;
 }
};
//block 结构体
struct __block_impl {
 void *isa;//block 的类型
 int Flags;
 int Reserved;
 void *FuncPtr;// block的执行函数指针,指向__main_block_func_0
};
//封装了 block 中的代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_03dcda_mi_0);
 }
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)};
int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
/*
 ** void(^block)(void) = ^{
 NSLog(@"调用了block");
 };
 ** 定义block的本质:
 ** 调用__main_block_impl_0()构造函数
 ** 并且给它传了两个参数 __main_block_func_0 和 &__main_block_desc_0_DATA
 ** __main_block_func_0 封装了block里的代码
 ** 拿到函数的返回值,再取返回值的地址 &__main_block_impl_0,
 ** 把这个地址赋值给 block
 */
 void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
/*
 ** block();
 ** 调用block的本质:
 ** 通过 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函数地址,直接调用
 */ 
 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
 }
 return 0;
}

3. block 的变量捕获机制 

  • 对于全局变量,不会捕获到 block 内部,访问方式为直接访问;
  • 对于 auto 类型的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为值传递;
  • 对于 static 修饰的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为指针传递。
  • 对于对象类型的局部变量,block 会连同其所有权修饰符一起捕获。

3.1 auto 自动变量

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int main(int argc, const char * argv[]) {
 @autoreleasepool {
 int age = 10;
 void(^block)(void) = ^{
 NSLog(@"%d",age);
 };
 block();
 }
 return 0;
}
struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 int age;
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
 impl.isa = &_NSConcreteStackBlock;
 impl.Flags = flags;
 impl.FuncPtr = fp;
 Desc = desc;
 }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 int age = __cself->age; // bound by copy
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_40c716_mi_0,age);
 }

可以看出:

  • 在 __main_block_impl_0 结构体中会自动生成一个相同的 age 变量
  • 构造函数 __main_block_impl_0( ) 中多出了一个 age 参数,用来捕获外部的变量

由于是传递方式为值传递,所以我们在 block 外部修饰 age 变量时,不会影响到 block 中的 age 变量

3.2 static 类型的局部变量 

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

 static int age = 10;
 void(^block)(void) = ^{
 NSLog(@"%d",age);
 };
 age = 20;
 block();
 // 20
struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 int *age;
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
 impl.isa = &_NSConcreteStackBlock;
 impl.Flags = flags;
 impl.FuncPtr = fp;
 Desc = desc;
 }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 int *age = __cself->age; // bound by copy
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_cb7943_mi_0,(*age));
 }

可以看出:

  • __main_block_impl_0 结构体中生成了一个相同类型的 age 指针
  • __main_block_impl_0 构造函数多了个参数,用来捕获外部的 age 变量的地址

由于传递方式是指针传递,所以修改 局部变量 age 时,age 的值会随之变化

3.3 全局变量

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int height = 10;
static int age = 20;
int main(int argc, const char * argv[]) {
 @autoreleasepool {
 void(^block)(void) = ^{
 NSLog(@"%d,%d",height,age);
 };
 block();
 }
 return 0;
}
int height = 10;
static int age = 20;
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;
 }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_7a340f_mi_0,height,age);
 }

可以看出:

  • __main_block_impl_0 结构体中,并没有自动生成 age 或 height 全局变量,也就是说没有将变量捕获到 block 内部

那么就有一个问题:

为什么局部变量需要捕获,而全局变量不用呢?

  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static 变量会一直保存在内存中, 所以捕获其地址即可。

3.4 __block 修饰的变量


3.4.1 __block 的使用:

终于来到那个问题:

默认情况下 block 是不能修改外面的 auto 变量的,解决办法?

  • 变量用 static 修饰(原因:捕获 static 类型的局部变量是指针传递,可以访问到该变量的内存地址)
  • 全局变量
  • __block(我们只希望临时用一下这个变量临时改一下而已,而改为 static 变量和全局变量会一直在内存中)

3.4.2  __block 修饰符

  • __block 可以用于解决 block 内部无法修改 auto 变量值的问题;
  • __block 不能修饰全局变量、静态变量;
  • 编译器会将 __block 变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));
  • 加 __block 修饰不会修改变量的性质,它还是 auto 变量;
  • 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;
  • 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。

将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

int main(int argc, const char * argv[]) {
 @autoreleasepool {
 __block int age = 10;
 void(^block)(void) = ^{
 age = 20;
 NSLog(@"block-%d",age);
 };
 block();
 NSLog(@"%d",age);
 }
 return 0;
}
struct __Block_byref_age_0 {
 void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 __Block_byref_age_0 *age; // by ref
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
 impl.isa = &_NSConcreteStackBlock;
 impl.Flags = flags;
 impl.FuncPtr = fp;
 Desc = desc;
 }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 __Block_byref_age_0 *age = __cself->age; // bound by ref
 (age->__forwarding->age) = 20;
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_75529b_mi_0,(age->__forwarding->age));
 }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

可以看出:

  • 编译器会将 __block 修饰的变量包装成一个__Block_byref_age_0对象;
  • 以上age = 20;的赋值过程为:通过 block 结构体里的(__Block_byref_age_0)类型的 age 指针,找到__Block_byref_age_0结构体的内存(即被 __block 包装成对象的内存),把__Block_byref_age_0结构体里的 age 变量的值改为20。

作者:bqiss原文地址:https://blog.csdn.net/bqiss/article/details/120282837

%s 个评论

要回复文章请先登录注册