目录
- Blocks概要
- 什么是Blocks
- OC转C++方法
- 关于几种变量的特点
- Blocks模式
- Block语法
- Block类型 变量
- 截获局部变量值
- __block说明符
- 截获的局部变量
- Blocks的实现
- Block的实质
Blocks概要
什么是Blocks
Blocks是C语言的扩充功能,即带有局部变量的匿名函数。
顾名思义,不带有名称的函数,C语言的标准可不允许存在这样的函数。就算是使用函数指针调用函数也需要知道函数名称。
OC转C++方法
因为需要看Block操作的C++源码,所以要知道转换的方法:
打开终端,cd到OC源文件
.m
所在的文件夹,输入clang -rewrite-objc 文件名称.m
,就会在当前文件夹内自动生成对应的.cpp
文件.
关于几种变量的特点
C语言函数中可能使用的变量:
- 函数参数
- 自动变量(局部变量)
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
而且,由于存储区域特殊,这其中有三种变量是可以在任何时候以任何状态调用的(在函数的多次调用之间能够传递值的变量):
- 静态变量
- 静态局部变量
- 全局变量
虽然这些变量的作用域不同,但在整个程序中,一个变量总保持在一个内存区域。而其他两种虽然有各自相应的作用域,超过作用域后,会被销毁。
Blocks模式
Block语法
完整形式的Block语法与一般的C语言函数定义相比,仅有两点不同:
- 没有函数名,即匿名函数;
- 带有
^
,因为iOS、Mac OS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。
Block语法的BN范式:^
返回值类型
参数列表
表达式
。例如:
^int (int value, int count) {return count * value;}
可省略返回值类型:
^(int value, int count) {return count * value;}
省略返回值类型的情况下:
- 表达式中
return
的类型就是返回类型; - 表达式中无
return
语句说明是void
类型; - 表达式中含有多个
return
语句时,所有return
的返回值类型必须相同。
可省略参数列表,如果不使用参数:
^void (void) {printf("Blocks\n");}
//省略形式
^{printf("Blocks\n");}
Block类型 变量
定义C语言函数时,可以将所定义的函数的地址赋值给函数指针类型的变量:
int func(int count) {
return count + 1;
}
int (*funcptr) (int) = &func;
同样的,Block是一种数据类型,可将Block语法赋值给声明为Block类型的变量:
//声明Block类型变量仅仅是将声明函数指针类型变量的*变为^
int (^blockName) (int);
//赋值(Block内容的实现)
int (^blockName) (int) = ^(int count) {
return count + 1;
};
如果我们在项目中经常使用某种相同类型的block,可以用typedef
抽象出这种类型的Block:
typedef int (^AddOneBlock) (int count);
AddOneBlock blockName = ^(int count) {return count + 1;};
用typedef
给Block起别名,使得block的赋值和传递变得相对方便,因为block一经抽象出来了:
typedef int (^block_t) (int);
//block作为参数
void func(int (^blockName) (int));
//简化后
void func(block_t blockName);
//block作为返回值
int (^func() (int)) {
return ^(int count) {return count + 1;};
}
//简化后
block_t func() { ... }
Block类型变量可完全像通常的C语言变量一样使用,因此也可以使用指向Block类型变量的指针,即Block指针类型变量:
typedef int (^block_t) (int);
block_t blockName = ^(int count) {return count + 1;};
block_t* blockptr = &blockName;
//int result = blockName(10);
int result = (*blockptr)(10);
截获局部变量值
int a = 20;
int b = 10;
void (^blockName)(void) = ^{
printf("%d, %d\n", a, b);
};
blockName();
a++;
b++;
printf("%d, %d\n", a, b); //21, 11
blockName(); //20, 10
可以看到,使用Block时,还可以使用Block外部的局部变量。而一旦在Block内部使用了其外部变量,这些变量就会被Block保存(即被截获),从而在执行块时使用。
__block说明符
实际上,局部变量值截获只能保存执行Block语法瞬间的值,保存后就不能改写改值:
可以看到,当修改截获的局部变量值时,会产生编译错误。
若想实现在Block内部将值赋给外部的局部变量,需要在该局部变量上附加__block
说明符:
__block int a = 20;
void (^blockName)(void) = ^{
a = 27;
printf("%d\n", a);
};
blockName(); //27
a++;
printf("%d\n", a); //28
blockName(); //27
小结:
- 修改Block外部的局部变量,Block内部被截获的局部变量不受影响;
- 修改Block内部的局部变量,编译不通过;
- 附有 __block说明符的局部变量可在Block中赋值,该变量也称__block变量。
截获的局部变量
截获变量为OC对象
从前面一部分可以得知,将值赋给Block中截获的局部变量会产生编译错误。
那么截获OC对象,调用变更该对象的方法也会产生编译错误吗?
id array = [[NSMutableArray alloc] init];
void (^blockName) (void) = ^{
id object = [[NSObject alloc] init];
[array addObject: object];
};
blockName();
截获的变量值array
为NSMutableArray类的对象,用C
语言描述,即是截获NSMutableArray类对象用的结构体实例指针。
使用截获的值,这是没有问题的,而向截获的变量array赋值则会产生编译错误:
这种情况下,需要给截获的局部变量附加__block说明符:
__block id array = [[NSMutableArray alloc] init];
截获对象为C语言数组
看似没有任何问题,只是使用了C语言的字符串字面量数组,而并没有截获的局部变量赋值。但由于在目前的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,所以无法通过编译。
使用指针就可以解决该问题:
const char* text = "hello";
void (^blockName) (void) = ^{
printf("%c\n", text[2]);
};
blockName(); //l
Blocks的实现
Block的实质
Block语法实际上是作为极普通的C语言源代码来处理的。
通过支持Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极为普通的C语言源代码被编译。
Block其实就是Objective-C对象,因为它的结构体中含有isa
指针。
下面在终端通过clang将OC中Block语法转换为C++代码:clang -rewrite-objc main.m
main.m:
int main(void) {
void (^blockName) (void) = ^{
printf("Block\n");
};
blockName();
return 0;
}
main.cpp:
下面,我们将源代码分成几个部分逐步理解:
-
源代码中的Block语法
//void (^blockName) (void) = ^{printf("Block\n");}; //通过Blocks使用的匿名函数被作为简单的C语言函数来处理 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); }
根据Block所属的函数名(此处为
main
)和该Block语法在该函数出现的顺序值(此处为0
)来给经clang变换的函数命名。
该函数的参数_cself
相当于C++实例方法中指向实例自身的变量this,或是OC实例方法中指向对象自身的变量self,即参数__cself为指向。C++的this,Objective-C的self
定义类的实例方法://C++ void MyClass::method(int arg) {printf("%p %d", this, arg);} MyClass cls; cls.method(10); //OC - (void)method: (int)arg {printf("%p %d", self, arg);} MyObject* obj = [[MyObject alloc] init]; [obj method: 10];
C++、Objective-C编译器将该方法作为C语言函数来处理:
//C++转成C void __ZN7MyClass6methodEi(MyClass* this, int arg) { printf("%p %d", this, arg); } struct MyClass cls; __ZN7MyClass6methodEi(&cls, 10); //OC转成C void _I_MyObject_method_(struct MyObject* self, SEL _cmd, int arg) { printf("%p %d", self, arg); } MyObject* obj = objc_msgSend(objc_getClass("MyObject"), sel_registerName("alloc")); obj = objc_msgSend(obj, sel_registerName("init")); objc_Send(obj, sel_registerName("method:"), 10);
objc_msgSend函数根据指定的对象和函数名,从对象持有类的结构体中检索_I_MyObject_method_函数的指针并调用。
objc_msgSend函数的第一个参数objc作为_I_MyObject_method_的第一个参数self进行传递。 -
来看看参数的声明:
struct __main_block_impl_0* __cself
,该结构体的声明如下:struct __main_block_impl { void* isa; int Flags; //标志 int Reserved; //今后版本升级所需的区域 void* FuncPtr; //指针函数 } struct __main_block_impl_desc_0 { unsigned long reserved; //今后版本升级所需的区域 unsigned long Block_size; //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; } }
来看看构造函数的调用,因为转换较多,看起来比较复杂,以下去掉转换的部分:
//void (*blockName) (void) = (void (*) void)&__main_block_impl_0 ((void *)__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0* blockName = &tmp;
该源代码将__main_block_impl_0结构体类型的局部变量,即栈上生成的__main_block_impl_0结构体实例的指针赋值给__main_block_impl_0结构体指针类型的变量
blockName
。这部分代码对应的最初源代码:
void (^blockName) (void) = ^{printf("Block\n");};
将Block语法生成的Block赋给Block类型变量blockName,它等同于将__main_block_impl_0结构体实例的指针赋给变量blockName。构造函数是C++中一种特殊的成员函数,用于在创建结构体对象时对其进行初始化操作,避免对象处于未定义状态。构造函数名称必须和类(包括结构体)的名称完全相同,无返回类型(包括void),若构造函数名称和结构体名不一致,编译器将不认为这是一个有效的构造函数,而是一个普通的成员函数。
-
下面来分析一下该构造函数
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)
中的参数:
第一个参数是由Block语法转换的C语言函数指针。第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针:static struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) //Block大小 };
-
接下来来看看调用Block的部分:
blockName();
这部分源代码:((void (*)(__block_impl *))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName);
去掉转换部分:
(*blockName->impl.FuncPtr)(blockName);
可以看出这是简单的函数指针调用函数。
-
最后探究一下上面没有提到的
_NSConcreteStackBlock
isa = &_NSConcreteStackBlock;
首先要理解OC类和对象的实质,所谓Block就是Objective-C对象。
“id”这一变量类型用于存储OC对象,在usr/include/objc/runtime.h中是如下进行声明的:typedef struct objc_object { Class isa; }* id; typedef struct objc_class { Class isa; }* Class;
这两种结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。
下面通过编写OC类来确认一下:@interface MyObject : NSObject { int val0; int val1; } //基于objc_object结构体,该类的对象的结构体如下: struct MyObject { Class isa; int val0; int val1; }
MyObject类的实例变量val0和val1被直接声明为对象的结构体成员。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa保持该类的结构体实例指针。
各类的结构体就是基于objc_class结构体的class_t结构体。class_t结构体在objc4运行时库的runtime/objc-runtime-new.h中声明如下:
struct class_t { struct class_t* isa; struct class_t* superclass; Cache cache; IMP* vtable; unitptr_t data_NEVER_USE; };