详细的探讨一下Block(讨论篇、基础篇、实质篇)
章节目录
- 关于Block的讨论篇
- Block的基础篇
- Block的实质篇
-
讨论篇:
为什么要看Block?
- 为了更熟练地使用Block
-
为了扩展程序编程的思维
-
了解底层的运作原理,有助于与他人探讨相关问题
-
公认的大神中基本都会,作为一个菜鸟要多向大牛学习
-
很多面试都需要
-
基础篇:
什么是block?
Block 一个带有自动变量的匿名函数。
匿名函数是因为Block没有函数名称,由^ 返回值类型 入参类型 表达式 组成。但可以赋值给Block类型变量。
自动变量在Block中表现为截获自动变量值,指Block内部调用外部变量时会捕获该变量在此瞬间的值。
Block长什么样?
通常: ^ 返回值类型 参数类型 表达式,如: ^int(int count){return count + 1; }
但无论是整型还是无返回值,返回值类型都可省略为: ^ 参数列表 表达式
省略返回值,根据表达式中有无return决定是否有返回值,如果有则返回该返回值类型,如果没有就是无返回值。所以,所有return返回值类型都必须相同。
如:^(int count){return count + 1;}
若没有参数,则参数类型也可省略,简化为:表达式。如:{NSLog(@“DrunkenMouse”);}
Block类型变量
int(^blk)(int) ; blk就是Block类型变量
简化Block声明与调用
typedef int (^blk_t)(int); 则用blk_t声明就代表此block
如int类型a 为 int a; 则int (^blk_t)(int)类型的blk 为 blk_t blk;
而调用则就 int result = blk(10);
__block说明符
若无__block修饰,则Block语法中只能捕获博并保存调用瞬间时的外部自动变量值,且不支持修改。
若想要修改外部自动变量就需要添加__block修饰符,此时此自动变量可称为__block变量。
但是对于OC对象来说,调用变更该对象的方法不会发生错误。
如NSMutableArray的对象array,调用其addObject方法修改其值时不会发生错误。但若对array进行赋值操作就会发生错误。
也就是赋值会报错,但使用不会报错。
另外,Block中无法截获C语言中的数组。因此对于C语言数组而言,无论调用,还是赋值都会报错。
-
实质篇:
Block是作为C语言源码来处理的。通过支持Block的编译器,将Block源码转换成C语言编译器能够处理的源代码,而后作为极为普通的C语言源码进行编译。
可手动通过clang的-rewrite-objc转换为C++源码。说是C++,其实只是使用了C++中的struct结构的C语言源码。
Block是OC对象,其结构体内部的isa指针放置该Block的信息的地址,形同OC对象中的元类
一个无参无反的最简单Block
void (^blk)(void) = ^{printf(“Block\n”)};
blk();
通过clang转换后会包括一大串代码,而这一大串代码等稍后总结时再看,现在先看下源码中的各个结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
此结构体包含当前Block的实例地址与相关信息。
首先可以看到其内部有两个结构体,struct __block_impl impl 与 struct __main_block_desc_0 *Desc
__block_impl结构体的声明为
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
存储着一个isa,指向一个存储着该Block信息的地址
Flags 一个标志
Reserved 该Block升级后所存放的区域
void *FuncPtr 一个无返回值的函数指针,指向由当前Block语法指向的C语言函数指针
__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 (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,Flags,FuncPtr都存储在struct __block_impl 中
该构造函数的调用:
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结构体指针类型变量blk
对应于我们书写的语句: void (^blk)(void) = ^{printf(“Block\n”);};
将Block语法生成的Block赋给Block类型变量blk
加个断句,更加通俗一点的说法:将 Block语法 生成 的Block 赋给 Block类型 变量 blk
等同于将__main_block_impl_0结构体实例的指针赋给变量blk
__main_block_impl_0结构体实例构造参数:
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
第一个参数是由Block语法转换的C语言函数指针
第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针
__main_block_desc_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)是如何根据这些参数初始化的,先看下该结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
优先展开struct __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;
相应的作用之前已说过,稍后还会再说一次。这里就不赘述,先往下看吧。
接下来看一下调用此实例的部分,也就是我们书写的blk();这一行代码,转化成以下源码:
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
去掉转换部分:
(*blk -> impl.FuncPtr)(blk);
简单的使用函数指针调用函数,入参为blk。
如刚才所说,由Block语法转换的__main_block_func_0 函数的指针被赋值给blk的struct __block_impl的成员变量void * FuncPtr中。
同时也说明了,__main_block_func_0函数的参数__cself指向Block值(blk)(__cself形同OC中的self,指自身。所以__cself指函数__main_block_func_0)。
到目前为止也可以看出Block(blk)是作为参数进行传递。
到此,我们来完整的总结一下关于一个无参无反,只输出一句话的Block
int main(){
void (^blk)(void) = ^{
printf("Block");
};
blk();
}
通过clang -rewrite-objc 转换成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;
__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");
}
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(){
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
在main函数中,会调用
void (*blk)(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结构体指针类型变量blk
而生成__main_block_impl_0结构体实例的第一个参数__main_block_func_0,是一个是由Block语法转换的C语言函数指针,内部就是我们书写的那句输出语句 printf(“Block”);
而第二个参数&__main_block_desc_0_DATA是struct __main_block_desc_0实例地址(该实例的指针所指向的地址)
__main_block_desc_0_DATA结构体的初始化是
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)};
第一个参数是今后升级所需的区域,为何是0不清楚。
第二个参数是Block的大小
struct __main_block_impl_0 相当于当前block,可将此结构体中的struct __block_impl 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;
isa指针指向的地址放置该Block的信息,形同OC对象中的元类
Flags 标志
Reserved 今后版本升级所存放的区域
FuncPtr是一个函数指针,指向__main_block_func_0 也就是当前Block语法转换的C语言函数指针
Desc就是struct __main_block_desc_0 实例,存放struct今后升级所需的区域与Block的大小
而后是main函数中的第二行
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉转换部分
(*blk -> impl.FuncPtr)(blk);
简单的使用函数指针调用结构体中的函数,入参为blk
在函数指针FuncPtr指向的函数中调用printf(“Block”);
完成Block输出
本次探讨到此结束,预知后事如何,且听下回分解。