【iOS】——Block底层实现和捕获机制

Block的实质

Block的定义是带有自动变量的匿名函数,下面从源码的角度探究下Block究竟是什么

下面是一个Block的简单实现:

objective-c 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void (^blk)(void) = ^{
            printf("Block\n");
        };
        blk();
    }
    return 0;
}

通过clang编译器转换为如下的C++代码:

objective-c 复制代码
// 定义Block的实现结构体
struct __block_impl {
  void *isa;      // 指向所属类的指针
  int Flags;      // 标志性参数,暂时没用到所以默认为0
  int Reserved;   // 今后版本升级所需的区域大小。
  void *FuncPtr;  // 函数指针,指向实际执行的函数,也就是Block中花括号里面的代码内容。
};

// 定义具体的Block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; // 上面定义的Block实现结构体的变量
  struct __main_block_desc_0* Desc; // Block描述符的指针
  
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags = 0) {
    impl.isa = &_NSConcreteStackBlock; // 设置Block的类型标识
    impl.Flags = flags;                // 设置标志
    impl.FuncPtr = fp;                 // 设置Block的执行函数指针
    Desc = desc;                       // 设置Block的描述符
  }
};

// Block的实际执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("Block\n"); // Block执行时输出的内容
}

// Block描述符结构体
static struct __main_block_desc_0 {
  size_t reserved;  // 今后版本升级所需区域的大小(一般填0)
  size_t Block_size;   // Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; // 初始化Block描述符

// 主函数
int main(int argc, const char * argv[]) {
  // 创建Block
  void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  
  // 调用Block
  ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
  
  return 0;
}

可以看到^{printf("Block\n");转换成了

objective-c 复制代码
// Block的实际执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("Block\n"); // Block执行时输出的内容
}

block的匿名函数被转换成普通C语言函数,函数命名按照block语法所属的函数名(此处为main函数)和在该函数中出现的顺序(此处为0)。

这里的_ self相当于OC中的指向对象自身的self,也就是_ self指向block值的变量

_ self参数是__main_block_impl_0的参数

__main_block_impl_0

objective-c 复制代码
// 定义具体的Block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; // 上面定义的Block实现结构体的变量
  struct __main_block_desc_0* Desc; // Block描述符的指针
  
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags = 0) {
    impl.isa = &_NSConcreteStackBlock; // 设置Block的类型标识
    impl.Flags = flags;                // 设置标志
    impl.FuncPtr = fp;                 // 设置Block的执行函数指针
    Desc = desc;                       // 设置Block的描述符
  }
};

这里主要定义类两个结构体,第一个成员是__block_impl 结构体,第二个是__main_block_desc_0,和实现构造函数

__block_impl

objective-c 复制代码
struct __block_impl {
  void *isa;//指向所属类的指针
  int Flags;//标志性参数,暂时没用到所以默认为0
  int Reserved;//今后版本升级所需的区域大小。
  void *FuncPtr;//函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。
};

如果block变量为空的话,这里的FuncPtr指针就会指向错误的地址而不是实际执行的函数,此时就会报错。

__main_block_desc_0

objective-c 复制代码
static struct __main_block_desc_0 {
  size_t reserved;  //今后版本升级所需区域的大小(一般填0)
  size_t Block_size;   //Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

第一个成员变量指的是今后版本升级所需区域的大小(一般填0)。

第二个成员变量是Block的大小。

__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};:

  • 这就是和我们平时用结构体一样,在定义完最后写一个结构体实例变量,变量名就是__main_block_desc_0_DATA。

  • 其中reserved为0,Block_size是sizeof(struct __main_block_impl_0)。

__main_block_impl_0()构造函数

objective-c 复制代码
__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指针保持这所属类的结构体的实例的指针。_mainblockimlp0结构体就相当于Objective-C类对象的结构体,这里的_NSConcreteStackBlock相当于Block的结构体实例,也就是说block其实就是Objective-C对于闭包的对象实现

main函数

objective-c 复制代码
// 主函数
int main(int argc, const char * argv[]) {
  // 创建Block
  // 使用类型转换将Block结构体的地址转换成函数指针类型
  void (*blk)(void) = 
    // 将Block结构体转换为函数指针类型
    ((void (*)())&__main_block_impl_0(
      // Block的执行函数指针
      (void *)__main_block_func_0, 
      // Block描述符的地址
      &__main_block_desc_0_DATA
    ));

  // 调用Block
  // 获取Block结构体中的FuncPtr成员,并进行必要的类型转换后调用
  ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

  return 0;
}

第一部分是将__main_block_impl_0结构体类型的自动变量,也就是栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量blk。

第二部分是相当于源代码中的blk(),即使用该Block部分。去掉转换部分就是:

objective-c 复制代码
(*blk->impl.FuncPtr)(blk);

使用函数指针调用函数。由Block语法转换的__main_block_impl_0函数的指针被赋值成员变量FunPtr中。

Block捕获机制

block变量捕获就是在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。

如果Block外面还有很多自动变量,静态变,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

下面是对于不同变量时Block的捕获:

  • 全局变量: 不捕获
  • 局部变量: 捕获值
  • 静态全局变量: 不捕获
  • 静态局部变量: 捕获指针
  • const修饰的局部常量:捕获值
  • const修饰的静态局部常量:捕获指针

当Block捕获的是变量的值时,在block内部不能修改变量,当Block捕获的是变量的指针时,在block内部可以修改变量

对于全局变量Block是进行直接访问的

全局变量:

objective-c 复制代码
int max = 3;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void (^blk)(void) = ^{
            printf("%d\n", max);
        };
        max = 60;
        blk();
    }
    
    return 0;
}

局部变量:

objective-c 复制代码
    int dmy = 256;
   	int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    val = 2;
    fmt = "These values were changed. val = %d\n";
    blk();

静态全局变量

objective-c 复制代码
static int max = 3;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void (^blk)(void) = ^{
            printf("%d\n", max);
        };
        max = 60;
        blk();
    }
    
    return 0;
}

静态局部变量

objective-c 复制代码
static int val = 10;
        const char *fmt = "val = %d\n";
        void (^blk)(void) = ^{
            val = 2;
            
            printf(fmt, val);
        };
        fmt = "These values were changed. val = %d\n";
        blk();

Block捕获普通变量

objective-c 复制代码
int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10;
    const char  *fmt = "val = %d\n";
    void (^blk)(void) = ^{
    	printf(fmt, val);
    };
    blk();
    return 0;
}

转化后的代码:

objective-c 复制代码
// Block结构体定义
struct __block_impl {
  void *isa;        // 指向Block类型的标识
  int Flags;        // Block标志位
  int Reserved;     // 保留字段
  void *FuncPtr;    // 指向Block执行函数的指针
};

// 定义特定于main函数的Block实现
struct __main_block_impl_0 {
  struct __block_impl impl; // 包含Block的基本实现
  struct __main_block_desc_0* Desc; // 指向Block描述符
  const char *fmt;            // 格式字符串
  int val;                    // 值

  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0)
    : fmt(_fmt), val(_val) {  // 初始化成员变量
    impl.isa = &_NSConcreteStackBlock; // 设置Block类型标识
    impl.Flags = flags;               // 设置Block标志位
    impl.FuncPtr = fp;                // 设置Block执行函数的指针
    Desc = desc;                      // 设置Block描述符
  }
};

// Block执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt;  // 获取格式字符串
  int val = __cself->val;          // 获取值

  printf(fmt, val);                // 执行打印操作
}

// Block描述符
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[]) {
  int dmy = 256;                   // 未使用的变量
  int val = 10;                    // 用于输出的整数值
  const char *fmt = "val = %d\n";  // 输出格式字符串

  // 创建Block
  void (*blk)(void) = 
    // 类型转换为函数指针
    ((void (*)())&__main_block_impl_0(
      // Block执行函数指针
      (void *)__main_block_func_0, 
      // Block描述符地址
      &__main_block_desc_0_DATA, 
      // 格式字符串
      fmt, 
      // 用于输出的整数值
      val
    ));

  return 0;
}

与上次不同的是在block内部语法表达式中使用的自动变量(fmt,val)被作为成员变量追加到了_mainblockimpl0结构体中(注意:block没有使用的自动变量不会被追加,如dmy变量)。

objective-c 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl; // 包含Block的基本实现
  struct __main_block_desc_0* Desc; // 指向Block描述符
  const char *fmt;            // 格式字符串
  int val;                    // 值

  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0)
    : fmt(_fmt), val(_val) {  // 初始化成员变量
    impl.isa = &_NSConcreteStackBlock; // 设置Block类型标识
    impl.Flags = flags;               // 设置Block标志位
    impl.FuncPtr = fp;                // 设置Block执行函数的指针
    Desc = desc;                      // 设置Block描述符
  }
};

在Block执行函数中,fmt,var都是从__cself里面获取的,更说明了二者是属于block的.

这两个变量是值传递,而不是指针传递,也就是说Block仅仅截获自动变量的值,所以这就解释了即使改变了外部的自动变量的值,也不会影响Block内部的值。

objective-c 复制代码
// Block执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt;  // 获取格式字符串
  int val = __cself->val;          // 获取值

  printf(fmt, val);                // 执行打印操作
}

Block捕获_ _block修饰的变量

编译器会将_ _block修饰的变量包装成一个对象,变成对象后就可以根据指针地址在block内部去修改外部的变量,block通过**__forwarding**指针去修改变量的值

objective-c 复制代码
int main(int argc, const char * argv[]) {
    
    __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
    };
    
    return 0;
}

转换后的代码:

objective-c 复制代码
//__block说明符修饰后的变量的结构体
struct __Block_byref_val_0 {
	void *__isa;  //指向所属类的指针
	__Block_byref_val_0 *__forwarding;  //指向自己的内存地址的指针
	int __flags;  //标志性参数,暂时没用到所以默认为0
	int __size;  //该结构体所占用的大小
	int val;  //该结构体存储的值,即原变量的赋的值
};

//block本体
struct __main_block_impl_0 {
	struct __block_impl impl;  //block的主体
	struct __main block desc 0* Desc;  //存储该block的大小
	__Block_byref_val_0 *val;  //__block修饰的变量的值
	//构造函数
	__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
	impl.isa = &_NSConcreteStackBlock;
	impl.Flags = flags;
	impl.FuncPtr = fp;
	Desc = desc;
};

//封装的block逻辑,存储了block的代码块
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
	__Block_byref_val_0 *val =__cself->val;
	
	(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0* src) {
    //根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0* src) {
    //自动释放引用的auto变量(相当于release)
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;  //保留字段
	unsigned long Block_size;  //block大小
	void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);  //copy的函数指针,下面使用构造函数进行了初始化
	void (*dispose)(struct __main_block_impl_0*);  //dispose的函数指针,下面使用构造函数进行了初始化
}
    //构造函数,初始化保留字段、block大小及两个函数
    __main_block_desc_0_DATA = {
	0,
	sizeof(structmain_block_impl_0),
	__main_block_copy_O, 
	__main_block_dispose_0
};
int main() {
    //之前的 __block int val = 10;变成了结构体实例
	struct __Block_byref_val_0 val = {
		0,  //isa指针
		&val,  //指向自身地址的指针
		0,  //标志变量
		sizeof(__Block_byref_val_0),  //block大小
		10  //该数据的值
	};
	blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

通过源代码发现__block说明符修饰的变量的结构体__多了个指针_ _Block_byref_val_0 *__forwarding,指向自己的内存地址的指针

objective-c 复制代码
struct __Block_byref_val_0 {
	void *__isa;  //指向所属类的指针
	__Block_byref_val_0 *__forwarding;  //指向自己的内存地址的指针
	int __flags;  //标志性参数,暂时没用到所以默认为0
	int __size;  //该结构体所占用的大小
	int val;  //该结构体存储的值,即原变量的赋的值
};

__main_block_func_0打印的并不是_ _Block_byref_val_0* val而是(val->__forwarding->val)并且 _ _block说明符修饰的变量变成了一个结构体

复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; 
  (val->__forwarding->val) = 1;
  printf("val = %d\n", (val->__forwarding->val));
}

那么为什么会有成员变量__forwarding呢?__

因为__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。

总结

  • Block的本质是一个对象,其内部的第一个成员是isa指针,通过内部的FuncPtr指针指向实际执行的函数,也就是Block中花括号里面的代码内容。
  • Block只会捕获自己需要使用的自动变量并将其保存进了Block的结构体实例中,也就是Block自身中。
  • Block捕获普通的局部变量只是通过构造函数将它的值传递进了Block的结构体实例中的成员,因此不能在Block内部修改值,并且在Block外修改的值也不会影响Block捕获时的值
  • Block捕获static修饰的局部变量是捕获它的指针,因此可以在Block内部修改值
  • Block捕获_ _Block变量时会将其转换成结构体并且生成_ _Block_byref_val_0 *__forwarding指针,指向自己的内存地址的指针,通过该指针就能访问和修改 _ _Blcok变量
相关推荐
HarderCoder14 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
goodSleep18 小时前
更新Mac OS Tahoe26用命令恢复 Mac 启动台时不小心禁用了聚焦搜索
macos
叽哥1 天前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚3 天前
阿权的开发经验小集
git·ios·xcode
用户093 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸3 天前
macOS自带截图命令ScreenCapture
macos
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview