前言:
以下文字都是摘自ios与osx多线程和内存管理一书,方便以后自己过来查阅的.
本文梗概:Block的实质:Block究竟是什么?本节将通过Block的实现进一步帮大家加深理解。。
1. Blocks的实现
:
1.1 Block的实质
:
在实际编译时无法转换成我们能够理解的代码,但clang(LLVM编译器)具有转换为我们可读代码的功能。通过”-rewrite-objc
“选项就能讲含有Block语法的代码变换为C++代码,其实也仅是使用了struct结构,本质是c语言代码。
clang -rewrite-objc 源代码文件名
转换Block语法。
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}
该Block省略了返回值类型以及参数列表,通过变换为一下形式:
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;
__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)
{
printf("Block\n");
}
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
void (*blk)(void) =
(void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}
8行的代码居然增加到了43行。
下面,将源码分成几个部分逐步理解。首先看最初源码中的Block语法。
^{printf("Block\n");};
变换后的代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("Block\n");
}
如上所示,通过Blocks使用的匿名函数实际上被作为简单的c语言函数来处理。另外,根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的顺序值(此处为0)来给经clang变换的函数命名。
该函数的参数__cself
相当于C++实例方法中指向实例自身的变量this,或是oc实例方法中指向对象自身的变量self,即参数__cself
为指向Block值的变量。
遗憾的是,由这次Block语法变换而来的__main_block_func_0
函数并没有使用参数__cself
.使用参数__cself
的例子将在后面介绍。我们先来看看该参数的声明。
struct __main_block_impl_0 *__cself
与c++的this和oc的self相同,参数__cself
是__main_block_impl_0
结构体的指针。
该结构体声明如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
由于转换后的代码中,也一并写入了其构造函数,所以看起来复杂。如果去除构造函数,就变的简单。
第一个成员变量是impl
,先看其__block_impl
结构体的声明。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
我们从其名称可以联想到某些标志,今后版本升级所需要的区域以及函数指针。(这些会在后面详细说明)。
第二个成员变量是Desc
指针,以下为其__main_block_desc_0
结构体的声明。
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
}
这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block的大小。
那么,下面我们来看看初始化含有这些结构体的__main_block_impl_0
结构体的构造函数。(初始化__main_block_impl_0
结构体成员的代码)
__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;
}
用于初始化__block_impl
结构体的isa
成员的_NSConcreteStackBlock
,在我们讲解它之前,先看看该构造函数的调用。
void (*blk)(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 *blk = &tmp;
由上可知,代码将__main_block_impl_0
结构体类型的自动变量,即栈上
生成的__main_block_impl_0
结构体实例的指针,赋值给__main_block_impl_0
结构体指针类型的变量blk
。以下为这部分代码对应的最初代码:
void (^blk)(void) = ^{printf("Block\n");};
将Block语法生成的Block赋给Block类型变量blk
。它等同于将__main_block_impl_0
结构体实例的指针赋给变量blk
。它等同于将__main_block_impl_0
结构体实例的指针赋给变量blk
。该源码中的Block就是__main_block_impl_0
结构体类型的自动变量,即栈上生成的__main_block_impl_0
结构体实例。
下面就看看__main_block_impl_0
结构体实例构造函数。
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
由此可知,该源码使用Block,即__main_block_impl_0
结构体实例的大小,进行初始化。
下面看栈上的__main_block_impl_0
结构体实例(即Block)是如何根据这些参数进行初始化的。
如果张开__main_block_impl_0
结构体的__block_impl
结构体,可记述为如下形式:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
该结构体根据构造函数会像下面这样进行初始化。
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
将__main_block_func_0
函数指针赋给成员变量FuncPtr。
对应的Block部分;blk();
这部分可变换为以下源码:
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
去掉转换部分。(*blk->impl.FuncPtr)(blk);
这就是简单地使用函数指针调用函数。正如我们刚才确认的,由Block语法转换的__main_block_func_0
函数的指针被赋值成员变量FuncPtr
中。另外也说明了,__main_block_func_0
函数的参数__cself
指向Block值。在调用该函数的代码中可以看出Block正是作为参数进行了传递。
到此总算是摸清了Block的实质。现在说说_NSConcreteStackBlock
到底是什么?isa = &_NSConcreteStackBlock;
将Block指针赋给Block的结构体成员变量isa。为了理解它,首先要了解oc类和对象的实质。其实所谓Block就是oc对象。
“id
“这一变量类型用于存储oc对象。在oc源码中,虽然可以像使用void *类型那样随意使用id
,但此id类型也能够在c语言中声明。在/usr/include/objc/runtime.h
中是这样子声明的:
typedef struct objc_object {
Class isa;
} *id;
id
为objc_object结构体的指针类型。我们再看看Class
。typedef struct objc_calss *Class;
Class
为objc_class
结构体的指针类型。objc_class
结构体在/usr/include/objc/runtime.h
中声明如下:
struct objc_class {
Class isa;
};
这与objc_object
结构体相同。然而,objc_object
结构体和objc_class
结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。
下面我们通过编写简单的oc类声明来确认下。
@interface MyObject : NSObject
{
int val0;
int val1;
}
@end
基于objc_object
结构体,该类的对象的结构体如下:
struct MyObject {
Class isa;
int val0;
int val1;
};
MyObject
类的实例变量val0和val1被直接声明为对象的结构体成员。“Objective-C中由类生成对象”意味着,像该结构体这样“生成由该类生成的对象的结构体实例”。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa保持该类的结构体实例指针。
如图2-1所示:
各类的结构体就是基于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;
uintptr_t data_NENER_USE;
};
在oc中,比如NSObject的class_t
结构体实例以及NSMutableArray的class_t
结构体实例等,均生成并保持各个类的class_t
结构体实例。该实例持有声明的成员变量,方法的名称,方法的实现(即函数指针),属性以及父类的指针,并被oc运行时库所使用。
到这里,就可以理解oc的类与对象的实质了。
那么回到刚才的Block结构体。
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
此__main_block_impl_0
结构体相当于基于objc_object
结构体的oc类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:isa = &_NSConcreteStackBlock;
即_NSConcreteStackBlock
相当于class_t
结构体实例。在将Block作为oc的对象处理时,关于该类的信息放置于_NSConcreteStackBlock
中。
现在大家就能理解Block的实质,知道Block即为oc的对象了。
专栏:
C++的this,Objective-c的self
C++中定义类的实例方法如下:
void MyClass::method(int arg)
{
printf("%p %d\n", this, arg);
}
C++编译器将该方法作为c语言函数来处理。
void __ZN7MyClass6methodEi(MyClass *this, int arg);
{
printf("%p %d\n", this, arg);
}
MyClass::method方法的实质就是__ZN7MyClass6methodEi函数。“this”作为第一个参数传递进去。
该方法的调用如下:
MyClass cls;
cls.method(10);
该源码通过c++编译器转换成c语言函数调用的形式:
struct MyClass cls;
__ZN7MyClass6methodEi(&cls, 10);
即this就是MyClass类(结构体)的实例。
同样,来看下oc的实例方法:
- (void) method:(int)arg
{
NSLog(@"%p %d\n", self, arg);
}
oc编译器同C++的方法一样,也将该方法作为C语言的函数来处理。
void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)
{
NSLog(@"%p %d\n", self, arg);
}
与C++中变换结果的this相同,“self”作为第一个参数被传递过去,以下为调用方代码。
MyObject *obj = [[MyObject alloc] init];
[obj method:10];
如果使用clang的-rewrite-objc选项,则上面源码会转换为:
MyObject *obj = objc_msgSend(
objc_getClass("MyObject"), sel_registerName("alloc"));
obj = objc_msgSend(obj, sel_registerName("init"));
obj_msgSend(obj, sel_registerName("method:"), 10);
objc_msgSend函数根据指定的对象和函数名,从对象持有类的结构体中检索_|_MyObject_method_函数的指针并调用。
此时,objc_msgSend函数的第一个参数obj作为_|_MyObject_method_函数的第一个参数self进行传递。同C++一样,
self就是MyObject类的对象。
1.2 截获自动变量:
本节主要讲解如何截获自动变量值。通过clang进行代码转换。
int main()
{
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();
return 0;
}
转换后的代码:
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 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA, fmt, val);
return 0;
}
着与前面转换的代码稍有不同。首先Block语法表达式中使用的自动变量被作为成员变量追加到了__main_block_impl_0
结构体中。
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语法表达式中没有使用的自动变量不会被追加,如代码中的变量dmy。_Blocks的自动变量截获只针对Block中使用的自动变量。下面看看初始化该结构体实例的构造函数的差异。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
在初始化结构体实例时,根据传递给构造函数的参数对自动变量追加的成员变量进行初始化。以下通过构造函数调用确认其参数。
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA, fmt, val);
使用执行Block语法时的自动变量fmt
和val
来初始化__main_block_impl_0
结构体实例。
即在该源码中,__main_block_impl_0
结构体实例的初始化如下。
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d\n";
val = 10;
由此可知,在__main_block_impl_0
结构体实例(即Block)中,自动变量值被截获。
下面再来看下使用Block的匿名函数的实现。最初源码的Block语法如下:{printf(fmt, val);}
该源码可转换为一下函数:
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语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block自身)中。
然而,前一篇文章中提到的,Block不能直接使用C语言数组类型的自动变量。如上所诉,截获自动变量时,将值传递给结构体的构造函数进行保存。
下面确认在Block中利用c语言数组类型的变量时有可能使用到的源码。首先来看数组传递给Block的结构体构造函数的情况。
void func(char a[10])
{
printf("%d\n", a[0]);
}
int main()
{
char a[10] = {2};
func(a);
}
该源码可以顺利编译,并正常执行。在之后的构造函数中,将参数赋给成员变量中,这样在变换了Block语法的函数内可由成员变量赋值给自动变量。代码预测如下:
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语言规范。