新建一个命令行项目,代码如下:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
void test(){
int a = 2;
int b = 3;
long (^myBlock)(void) = ^long() {
return a * b;
};
long r = myBlock();
}
使用编译指令编译成 cpp 文件查看源码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
得到的原始代码中 test
函数的源码为:
void test(){
int a = 2;
int b = 3;
long (*myBlock)(void) = ((long (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, b));
long r = ((long (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
去掉强制类型转换,简化代码:
void test(){
int a = 2;
int b = 3;
long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
long r = (myBlock->FuncPtr)(myBlock);
}
其实还是有点懵逼的,再认真分析下第一行代码:
long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
从代码中可以看到这行代码定义了一个函数类型的指针myBlock
,并且将其指向了__test_block_impl_0
返回值的地址。这里把内存地址赋值给指针是指针用法的常规操作,就不多说了。于是乎就会想看看__test_block_impl_0
这是个什么东东,源码如下:
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里需要解释的是:
-
__test_block_impl_0()
函数和结构体同名,这里是 C++ 语法,C语言的结构体不允许存在函数; -
: a(_a), b(_b)
冒号这种语法是 C++ 中的构造函数中初始化成员列表,也就是将入参直接赋值给成员 a 和 b,简化了代码。类似的语法在很多其他语言中也存在,比如 Dart;
所以就可以得出结果,第一行代码的作用就是:创建一个__test_block_impl_0
变量,并且赋值给myBlock
。
这里预留个疑问:
这个long (*myBlock)(void)
是声明一个指向函数的指针。可是明明是指向结构体变量的指针,可以使用struct __test_block_impl_0 *p
来写,代码如下:
struct __test_block_impl_0 block;
struct __test_block_impl_0 *myBlock = █
可是问什么要声明成一个函数指针呢?先留着吧~
继续,第一行看完了,知道是在做啥了,那现在就应该看看__test_block_impl_0
到底是个啥玩意了,还是先看源码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __test_block_impl_0 {
// 特别注意,这里impl不是指针哦,所以相当于直接将__block_impl中的四个成员复制过来
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
// 默认赋值为_NSConcreteStackBlock,但是还有其他的类对象类型
impl.isa = &_NSConcreteStackBlock;
// 默认是0,不用理他
impl.Flags = flags;
// 这里就是封装了block内部逻辑的函数
impl.FuncPtr = fp;
// 计算size用的,不用理
Desc = desc;
}
};
long (*myBlock)(void) = ((long (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, b));
其实第一行代码可以简化成:
// 原来的代码
long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
// 简化:
struct __test_block_impl_0 block;
struct __test_block_impl_0 *myBlock = █
myBlock->impl.isa = &_NSConcreteStackBlock;
myBlock->impl.Flags = 0;
myBlock->impl.FuncPtr = __test_block_func_0;
myBlock->Desc = &__test_block_desc_0_DATA;
myBlock->a = a;
myBlock->b = b;
这样就很清晰了,继续看__test_block_func_0
和__test_block_desc_0_DATA
,看源码:
// __test_block_func_0源码:
static long __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a;
int b = __cself->b;
return a * b;
}
// __test_block_desc_0_DATA的源码:
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
从源码可知:
-
__test_block_func_0
将 block 内部的逻辑封装成了一个函数; -
__test_block_desc_0
生成了一个__test_block_desc_0_DATA
的变量,reserved
为占位保留字段,不用管,而__test_block_impl_0
结构体(也就是 block 结构体的第一个成员),的内存大小被赋值给了__test_block_impl_0
;
这样就可以看到本质了:
- block 本质是一个 C++ 结构体
block 是通过 C++ 的结构体而不是 C 的结构体实现的。被捕获的变量会生成为 block 的成员变量。内部的成员变量impl
包含了面向对象的基础和 block 内部代码逻辑的实现。而构造函数则执行了赋值操作,完成了结构体变量的初始化。 - block 也是一个对象
block 的第一个成员是__block_impl
结构体,而__block_impl
第一个成员就是isa
指针,所以,block 的本质就是对象,默认指向_NSConcreteStackBlock
类,也就是 block 本质是一个_NSConcreteStackBlock
类的对象(默认情况下,后面还会解释)
继续 go on,再看看第二行代码:
long r = ((long (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
去掉强制类型转换后:
long r = myBlock->FuncPtr(myBlock);
也就是直接调用FuncPtr
这个函数,参数为myBlock
。
这里有两个疑问:
- 函数指针类型为什么不是直接调用而需要通过
->
来访问内部成员变量调用呢?
正常来讲,函数指针的使用如下:
long func(int a) {
return a;
}
long (*p)(int) = &func;
printf("%li\n", p(10)); // 结果为10
但是呢,通过上面的源码分析可以看到 myBlock
这个变量并不是一个指向函数的指针,而是一个 struct __test_block_impl_0
类型的指针,所以如果不看他的类型,正常使用->
访问成员变量是逻辑正确的,所以这个问题本质仍然是为什么要用long (*myBlock)(void)
类型的指针来指向struct __test_block_impl_0
类型的指针,同上,暂时先不管~~~
-
FuncPtr
是内部成员变量impl
的成员变量,为什么可以直接访问?
正常来讲应该这样访问:
long r = myBlock->impl.FuncPtr(myBlock);
因为impl
是结构体的第一个成员变量,所以其内存地址就是结构体变量的地址,也就是说这个地址就是impl
成员变量的地址,三者是等价的,也就是说:myBlock = &myBlock.impl = &block(参照上文的最终简化的代码逻辑),所以这样直接调用impl.FuncPtr
就是正确的。
这个结论还可以通过内存地址来验证,test()
函数中内容修改如下:
void test(){
int a = 2;
int b = 3;
long (^myBlock)(void) = ^long() {
return a * b;
};
struct __test_block_impl_0 *p = (__bridge struct __test_block_impl_0 *)myBlock;
long r = myBlock();
}
运行后打断点:
继续断点至
return a * b;
,并设置 Debug Workflow为 Always Sow Disassembly,的到如下结果:FuncPtr内存地址
也就是说三个地址相互吻合。
备注
- myBlock 中的
isa
指向变成了NSMallocBlock
,这个以后会继续讲解struct __test_block_impl_0 *p = (__bridge struct __test_block_impl_0 *)myBlock;
这里在myBlock
前面没有加 & ,因为myBlock
本身就是一个指针,其值就是它指向的内存地址,如果加了 & 意思是取myBlock
的内存地址,这样就不对了~
总结
- Block 的本质是一个含有 isa 指针的结构体,且继承链为 xxxBlock -> NSBlock -> NSObject。所以Block可以直接当成对象来使用,调用各种对象方法;
- Block 内部的代码被封装成函数由 FuncPtr 指向并调用;
至此,Block的基本原理完毕,下一篇Block进阶~
网友评论