iOS——【Blocks】

Blocks概要

Blocks是C语言的扩充功能,即带有自动变量的匿名函数。匿名函数就是不带函数名的函数。这一概念同样被称为"闭包",lambda计算等。

自动变量是在函数内部声明的变量,其作用域仅限于声明它的函数内部。这意味着它们只能在其声明的函数内部使用,并且在函数执行完毕后会被自动销毁。

Blocks模式

block语法的完整形式:

objectivec 复制代码
^void (int event) {
    //...
}

即为:

^返回值类型 参数列表 表达式

与C语言不同的地方有:

  1. 没有"^"(插入记号):插入该记号便于查找。
  2. 没有函数名:因为为匿名函数。

block的返回值类型是可以省略的,省略返回值类型后,如果有return语句就返回该返回值类型,没有的话就使用void类型。

其次,如果不使用参数,参数列表也可以省略。

Block类型变量

同样的,在Block语法下,可将Block语法赋值给声明为Block类型的变量中。在有关Blocks的文档中,"Block"既指源代码中的Block语法,也指由Blcok语法所生成的值。

声明Block类型变量的语法如下:

objectivec 复制代码
int (^blk)(int);

该Block类型变量与一般的C语言变量完全相同,可以用于:自动变量、函数参数、静态变量、静态全局变量、全局变量。

下面使用Block语法将Block赋值为Block变量:

objectivec 复制代码
int (^blk)(int) = ^(int count)(return count+1);

由"^"开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所以也可以由Block类型变量向Block类型变量赋值。

objectivec 复制代码
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;

在函数参数中使用Block类型变量可以向函数传递Block:

objectivec 复制代码
void func (int (^blk)(int)) {
  
}

在函数返回值中指定Block类型,可以将Block作为返回值返回:

objectivec 复制代码
int (^func()(int)) {
  return ^(int count)(return count+1);
}

还可以使用typedef简化块的记述方式,见EOF学习的博客第38条。

将赋值给Block类型变量的Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。变量funcptr为函数指针类型的时候,像下面这样调用函数指针类型变量:

objectivec 复制代码
int result = (*funcptr)(10);

变量blk为Block类型的情况下,这样调用Block类型变量:

objectivec 复制代码
int result = blk(10);

通过Block类型变量调用Block与C语言通常的函数调用没有区别:

objectivec 复制代码
// blk_t blk就是一个块对象
int func(blk_t blk, int rate) {
  return blk(rate);
}

//块对象在OC中也可以当参数
- (int) useBlock: (blk_t)blk rate:(int)rate;

也可以使用指向Block类型变量的指针,即Block指针类型变量。

objectivec 复制代码
typedef int (^blk_t)(int);
blk_t blk = ^(int count)(return count+1);
blk_t *blkptr = &blk;
(*blktr)(10);

截获自动变量值

Blocks中,Blocks表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。

因为Block表达式保存了自动变量的值,所以在执行Blocks语法后,即使改写Block中所使用的自动变量的值也不会影响执行时自动变量的值。

objectivec 复制代码
int main() {
  int dmy = 256;
  int val = 10;
  const char *fmt = "val = %d\n";
  //这里声明了块就是在截获变量,此时捕获的fmt和val的值就是在该块被创建之前那一瞬间的值,哪怕后面已经改变了fmt和val的值,这里截获的结果还是改变之前的值,因为那才是这个块创建那一瞬间的时候变量的值。
  void(^blk)(void) = ^{printf(fmt, val);};
  val = 2;
  fmt = "These value were changed. val = %d\n";
  blk();
  return 0;
}

__block说明符

实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能修改该值。如果尝试改写截获的自动变量的值:

objectivec 复制代码
int val = 0;
void (^blk)(void) = ^{val = 1};

我们发现编译的时候会报错。

若想在Block语法表达式中给自动变量赋值,需要在该自动变量上附加__block说明符:

objectivec 复制代码
__block int val = 0;
void (^blk)(void) = ^{val = 1};

使用附有_ _blcok说明符的可在Block中赋值,该变量称为__block变量。

截获的自动变量

已知如果尝试改写截获的自动变量的值,编译的时候会报错。那么截获OC对象,调用变更该对象的方法也会产生编译错误吗?

objectivec 复制代码
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
  id obj = [[NSObject alloc] init];
  [array addObject: obj];
}

这样是没有问题的,而向截获的变量array赋值就会编译错误。

虽然向截获的变量array赋值会产生错误,但是使用截获的值不会产生错误。

这种情况下,需要给截获的自动变量附加 _block说明符。

objectivec 复制代码
__block id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
  array = [[NSMutableArray alloc] init];
};

Blocks的实现

Block的实质

clang (LLVM 编译器)具有转换为我们可读源代码的功能 。通过"-rewrite-objo"选项就能将含有Block语法的

源代码变换为C ++的源代码。说是C ++,其实也仅是使用 了str uc t 结构,其本质是C 语言源代码。

objectivec 复制代码
clang -rewrite-objc 源代码文件名

我们转换如下的block代码:

c 复制代码
int main() {
  void (^blk)(void) = ^{printf("Block\n");};
  blk(); 
  return 0;
}

此代码的Block语法最为简单,它省略了返回值类型以及参数列表。该源代码通过clang可变换为以下形式:

objectivec 复制代码
//经过clang转换后的C++代码
struct __block_impl {
    void *isa; // 指向 block 的类的指针
    int Flags; // 标志位
    int Reserved; // 保留字段
    void *FuncPtr; // 指向 block 函数的指针
};

struct __main_block_impl_0 {
    struct __block_impl impl; // block 的实现
    struct __main_block_desc_0 *Desc; // block 的描述
    // 构造函数,初始化 impl 和 Desc 字段
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; // 设置 isa 指针
        impl.Flags = flags; // 设置标志位
        impl.FuncPtr = fp; // 设置函数指针
        Desc = desc; // 设置描述指针
    }
};

// block 函数的实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n"); // 打印信息
}

// block 的描述结构
static struct __main_block_desc_0 {
    size_t reserved; // 保留字段
    size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = {
    0, // 保留字段为 0
    sizeof(struct __main_block_impl_0) // block 实现的大小
};

int main(int argc, const char * argv[]) {
    // 定义一个函数指针 blk,指向 __main_block_impl_0 结构体的实例,该实例通过 __main_block_impl_0 构造函数初始化
    void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

    // 调用 block 函数指针
    ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    return 0; // 返回
}

struct _ _block_impl:这个结构体定义了一个 block 的实现,包含了指向 block 类的指针 isa、标志位 Flags、保留字段 Reserved 和指向 block 函数的指针 FuncPtr。

struct _ _main_block_impl_0:这个结构体扩展了 __block_impl,定义了一个特定的 block 实现。它包含一个 _ _block_impl 的实例、一个指向 block 描述的指针 Desc,并且有一个构造函数用于初始化这些字段。

_ _main_block_impl_0(void *fp, struct _ _main_block_desc_0 *desc, int flags=0):这是 _ _main_block_impl_0 的构造函数,用于初始化 impl 和 Desc 字段。

_ _main_block_func_0(struct _ main_block_impl_0 * _cself):这是 block 的实际函数实现,在本例中只是简单地打印 "Block" 信息。

struct _ _main_block_desc_0:这个结构体描述了 block 的大小和保留空间。

_ _main_block_desc_0_DATA:这是 block 描述的实际数据,包括大小信息。

main 函数中,首先定义了一个函数指针 blk,它指向一个 _ _main_block_impl_0 结构体的实例,该实例通过 _ main_block_impl_0 的构造函数初始化,并且调用了这个 block 函数指针,打印 "Block" 信息。
该函数的参数
cself相当于C++实例方法中所指的自身变量this,或是OC实例方法中指向对象自身的变量self,即参数 _cself为指向Block值的变量。

由这次Block语法变换而来的_main_block_func_0 函数并不使用__cself。我们先来看看该参数的声明:

objectivec 复制代码
struct __main_block_impl_0* __cself

//结构体声明:
struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
}

第一个成员变量是impl,其__block_impl结构体的声明:

c 复制代码
struct __block_impl {
	void *isa;
	int Flags;
	int Reserved;
	void *FuncPtr;
}

第二个成员变量是Desc指针,其__main_block_desc_0结构体的声明:

objectivec 复制代码
static struct __main_block_desc_0 {
  	size_t reserved;
  	size_t Block_size;
}

以上就是初始化__main_block_impl_0结构体成员的源代码。我们刚刚跳过了_NSConcreteStackBlock的说明。_NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。虽然大家很想了解它,但在进行讲解之前,我们先来看看该构造函数的调用。

c 复制代码
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

因为转换较多,看起来不是很清楚,所以我们去掉转换的部分,具体如下:

c 复制代码
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

struct __main_block_impl_0 *blk = &tmp;

这样就容易理解了。该源代码将_ main_block_impl0结构体类型的自动变量,即栈上生成的 main_block_impl_0 结构体实例的指针,赋值给 _main_block_impl_0结构体指针类型的变量 blk。以下为这部分代码对应的最初源代码。

c 复制代码
void(^blk)(void)=^{printf("Block\n");};

将 Block 语法生成的Block赋给Block 类型变量blk。它等同于将_ main_block_impl_0 结构体实例的指针赋给变量blk。该源代码中的 Block 就是 main_block_impl_0 结构体类型的自动变量,即栈上生成的 main_block_impl_0结构体实例。
下面就来看看
_main_block_impl_0结构体实例构造参数。

c 复制代码
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

第一个参数是由Block语法转换的C语言函数指针。第二个参数是作为静态全局变量初始化的_ main_block_desc_0 结构体实例指针。以下为 _main_block_desc_0 结构体实例的初始化部分代码。

c 复制代码
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
	0,
	sizeof(struct __main_block_impl_0)
};

我们来确认一下使用该 Block的部分。

objectivec 复制代码
blk();

这部分可变换为以下源代码:

c 复制代码
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk);

去掉转换部分。

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

这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的_ main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了, _main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。

其实,所谓Block 就是Objective-C 对象。

截获自动变量

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;
}
c 复制代码
struct __block_impl {
  	void *isa;
  	int Flags;
  	int Reserved;
  	void *FuncPtr;
};

struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
  	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;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val; 
  
	printf(fmt, val);
}

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

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    return 0;
}

其中Block语法表达式中使用的自动变量被作为成员变量追加到了__main_block_impl_0结构体中。

objectivec 复制代码
struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
  	const char *fmt;
  	int val;
}

_ _main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同。

请注意 Block 语法表达式中没有使用的自动变量不会被追加

初始化该结构体实例的构造函数的差异:

objectivec 复制代码
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
objectivec 复制代码
//通过构造函数调用确认其参数
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

使用执行Block语法时的自动变量fmt 和 val来初始化__main_block_impl_0结构体实例。即在该源代码中,__main_block_impl_0结构体实例的初始化如下:

c 复制代码
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;

初始化时对fmt和val进行了赋值。由此可知,在__main_block_impl_0结构体实例中(即Block),自动变量被截获。

再看一下使用Block的匿名函数的实现:

c 复制代码
^{printf(fmt, val)};

转换为:

c 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val;
  	
	printf(fmt, val);
}

在转换后的源代码中,截获到__main_block_impl_0 结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动便可使用截获的自动变量值执行。

在Block 中利用C语言数组类型的变量时有可能使用到的源代码。首先来看将数组传递给Block的结构体构造函数的情况。

c 复制代码
void func(char a[10]) {
	printf("%d\n",a[0]);
}
int main() {
	char a[10] = {2};
	func(a);
}

该源代码可以顺利编译,并正常执行。在之后的构造函数中,将参数赋给成员变量中,这样在变换了Block语法的函数内可由成员变量赋值给自动变量。源代码预测如下。

c 复制代码
void func(char a[10]) {
	char b[10] = a;
	printf("%d\n", b[0]);
}
int main() {
	char a[10] = {2};
	func(a);
}

该源代码将C语言数组类型变量赋值给C语言数组类型变量中,这是不能编译的。虽然变量的类型以及数组的大小都相同,但C语言规范不允许这种赋值。当然,有许多方法可以截获值,但Blocks 似乎更遵循C语言规范。

__Block说明符

Block中使用自动变量后,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。

以下源代码试图改变Block中的自动变量val。该代码会产生编译错误。

c 复制代码
int val = 0;
void(^blk)(void) = ^{val = 1;};

因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。

不过这样一来就无法在Block中保存值了,极为不便。解决这个问题有两种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:

  • 静态变量
  • 静态全局变量
  • 全局变量
    静态变量的这种方法似乎也适用于自动变量的访问。但是我们为什么没有这么做呢?
    实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此 Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。这些在下节详细说明。
    解决Block中不能保存值这一问题的第二种方法是使用"_ block说明符"。更准确的表述方式为" block存储域类说明符"( _block storage-class-specifier)。C语言中有以下存储域说明符:
  • typedef
  • extern
  • static
  • auto
  • register
    _block 说明符类似于 static、auto 和 register 说明符,它们用于指定将变量值设置到哪个存储域中。例如,auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。
    下面我们来实际使用
    block说明符,用它来指定Block中想变更值的自动变量。我们在前面编译错误的源代码的自动变量声明上追加 _block 说明符。
objectivec 复制代码
__block int val = 10;
void (^blk)(void) = ^{val = 1;};

变换后:

c 复制代码
struct __Block_byref_val_0 {
	void *__isa;
	__Block_byref_val_0 *__forwarding;
	int __flags;
	int __size;
	int val;
};
struct __main_block impl_0 {
	struct __block_impl impl;
	struct __main block desc 0* Desc;
	__Block_byref_val_0 *val;
	__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;
};
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) {
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0*src) {
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;
	unsigned long Block_size;
	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(structmain_block_impl_0),
	__main_block_copy_O,
	__main_block_dispose_0
};
int main() {
	__Block_byref_val_0 val = {
		0,
		&val,
		0,
		sizeof(__Block_byref_val_0),
		10
	};
	blk = &__mainblock_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

这个__block变量val是怎样转换过来的呢?

objectivec 复制代码
__Block_byref_val_0 val = {
	0,
	&val,
	0,
	sizeof(_Block_byref_val_0),
	10
};

它竟然变为了结构体实例。__block变量也同 Block一样变成__Block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0 结构体实例。该变量初始化为10,且这个值也出现在结构体实例的初始化中,这意味着该结构体持有相当于原自动变量的成员变量。

相关推荐
allanGold14 分钟前
【xcode 16.2】升级xcode后mac端flutter版的sentry报错
macos·xcode16.2
幽夜落雨26 分钟前
ios老版本应用安装方法
ios
gxhlh7 小时前
局域网中 Windows 与 Mac 互相远程连接的最佳方案
windows·macos
宏基骑士7 小时前
mac 电脑上安装adb命令
macos·adb
胖虎18 小时前
实现 iOS 自定义高斯模糊文字效果的 UILabel(文末有Demo)
ios·高斯模糊文字·模糊文字
水银嘻嘻13 小时前
【Mac】Python相关知识经验
开发语言·python·macos
梦魇梦狸º1 天前
mac 配置 python 环境变量
chrome·python·macos
丁总学Java1 天前
macOS如何进入 Application Support 目录(cd: string not in pwd: Application)
macos
qdprobot1 天前
Mixly米思齐1.0 2.0 3.0 软件windows版本MAC苹果电脑系统安装使用常见问题与解决
windows·macos
麦克Mapp1 天前
不用安装双系统,如何在mac上玩windows游戏呢?
macos