关于block,现在也是烂大街的技术点了,随便一搜,什么样式的都有,加上前几天在一个技术交流群里面被一个大佬教育了一番,就按照自己的方式重新学习了一下block。 (我本以为我懂block,c语言函数嘛,最多还是个匿名函数,正经点回答那不就是个带有自动变量的匿名函数嘛,结果人家的问题是-block如何封装函数调用跟函数调用环境的)
为了重新认识block,我选择我自己最不擅长的方式--学习源码 以下便是笔者的学习过程,特意记录一下,方便日后查看。
一般的理解,本质为c语言函数---带有自动变量值的匿名函数 例子:
void (^blk)(void) = ^{
printf("block\n");
};
blk();
使用clang -rewrite-objc
文件名 将c语言代码转换成底层实现代码
按文件中的顺序依次如下
62行
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa指针指向其父类 flags标志 reserved后续版本升级所需区域 funcpstr函数指针
最底下部分
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 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
desc结构体的内容也是今后版本升级所需区域以及block的大小
__main_block_desc_0_DATA
作为_main_block_impl_0
结构体实例构造过程中的第二个参数
构造函数的调用如下
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语言函数指针
__main_block_desc_0_DATA
第二个参数是作为静态全局变量初始化的__main_block_desc_0
结构体实例指针
blk()
调用如下
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
blk()
去掉转换部分
(*blk->impl.FuncPtr)(blk);
这个其实就是简单的使用函数指针调用函数
关于impl.isa = &_NSConcreteStackBlock;
将Block指针赋值给Block的结构体成员变量isa。首先要知道oc中的类和对象的实质,在oc的runtime.h文件中是如下声明的:
typedef struct objc_object{
Class isa
} *id;
typedef struct objc_class *Class;
struct objc_class{
Class isa;
};
这两个结构体都是最基本的结构体,假如有个Person类,并且声明了两个int类型的成员变量val0,val1,基于objc_object
,那么Person的结构体将会是:
struct Person{
Class isa;
int val0;
int val1;
}
可以看到,实例变量被直接声明为对象的结构体成员
各类的结构体就是基于objc_class
结构体的class_t
结构体,class_t
声明如下
struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
uintptr_t data_NEVER_USE;
};
class_t
结构体实例持有声明的成员变量,方法的名称,方法的实现(函数指针),属性以及父类的指针,到此基本可以理解oc的类与对象的实质
回头再看Block的结构体
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reverse;
void *Funcptr;
struct __main_block_desc_0* Desc;
}
此__main_block_impl_0
结构体相当于基于objc_object
结构体的oc类对象的结构体,其中对成员变量isa初始化就是
isa = &_NSConcreteStackBlock;
也就是说,_NSConcreteStackBlock
相当于class_t
结构体实例,在将Block作为oc对象处理时,关于该类的信息放置于_NSConcreteStackBlock
中。
关于截获自动变量值
int a = 3;//注意下面所有出现a的部分
void (^blk2)(void) = ^{
printf("%d",a);
};
blk2();
clang转换后:
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
int a; //注意这里
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
int a = __cself->a; // bound by copy
//__cself->a的值为3 在__main_block_impl_1构造中被初始化
printf("%d",a);
}
static struct __main_block_desc_1 {
size_t reserved;
size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
通过转换 可以看到,Block语法表达式中使用的自动变量被作为成员变量追加到__main_block_impl_1
结构体中,需要注意的是,仅仅在Block语法表达式中使用到的自动变量才会被追加进去,Block的自动变量截获只针对Block中使用的自动变量(如果是id类型对象,只使用而不是赋值,也会被追加进去)
另外,在函数调用实现中,生成了临时变量a,并将外部捕获的a(也就是block结构体的成员变量a)赋值给临时变量,随后进行使用(打印),这也是为什么在block体后面修改a不会影响打印结果的原因
构造函数以及调用部分如下
int a = 3;
void (*blk2)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, a));
((void (*)(__block_impl *))((__block_impl *)blk2)->FuncPtr)((__block_impl *)blk2);
这里也可以清楚看到a被传入到结构体构造过程中,使用a来初始化__main_block_impl_1
结构体实例,由此可知,自动变量被截获--Block语法表达式所使用的自动变量值被保存在Block的结构体实例(即Block自身)中
__block修饰符
block体内修改自动变量值,编译无法通过,在给自动变量加上__block
修饰符后便可以实现改写操作,或者将自动变量声明为 静态变量/静态全局变量/全局变量
假如分别声明
int global_val = 1;
static int static_globle_val = 2;
static int static_val = 3;
然后
void(^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
};
经转换后 只有static_val
会被Block截获,表现与自动变量一致
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
int *static_val = __cself->static_val;
global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
}
使用静态变量static_val
的指针对其进行访问,将静态变量static_val
的指针传递给__main_block_impl_0
结构体的构造函数并保存。这是超出作用域使用变量的最简单方法
接着使用__block改写自动变量
__block int b = 10;
void (^blk3)(void) = ^{
b = 20;
};
blk3();
printf("%d",b);
转换后
block结构体结构不变
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__block int b = 10;
转换成
struct __Block_byref_b_0 {
void *__isa;
__Block_byref_b_0 *__forwarding;
int __flags;
int __size;
int b;
};
可以看到,自动变量b被__block
修饰后,不再是以单纯的int类型保存在__main_block_impl_2
结构体中,而是以__Block_byref_b_0
结构体实例保存的,这就意味着该结构体持有相当于原自动变量的成员变量
block结构体的构造过程如下
struct __main_block_impl_2 {
struct __block_impl impl;
struct __main_block_desc_2* Desc;
__Block_byref_b_0 *b; // by ref 不再是简单的int类型成员变量 而是结构体指针,且构造函数的参数也是结构体指针
__main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, __Block_byref_b_0 *_b, int flags=0) : b(_b->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
^{b=20;};
转换成了
static void __main_block_func_2(struct __main_block_impl_2 *__cself) {
__Block_byref_b_0 *b = __cself->b; // bound by ref
(b->__forwarding->b) = 20;
}
Block的__main_block_impl_2
结构体实例变量持有__block
变量的__Block_byref_b_0
结构体实例的指针
也就是说 b的指针指向__Block_byref_b_0
__Block_byref_b_0
结构体的实例变量__forwarding
的指针指向该实例变量自身
通过
__forwarding
访问成员变量b,这个b是该实例变量的成员变量,它相当于原自动变量,修改__Block_byref_b_0
结构体的实例变量中__forwarding
所指向的成员变量的值也就达到修改了原自动变量的值
Block超出变量作用域可存在的原因
Block分为三种,记述全局变量的地方有Block语法时或者Block表达式中不使用应截获的自动变量时,该Block为全局Block,Block用结构体的成员变量isa的初始化为
impl.isa = &_NSConcreteGlobalBlock
全局Block配置在数据区域
除去以上情况下的Block,其它都是_NSConcreteStactBlock
,配置在栈上
配置在全局变量上的Block,从变量作用域外也可以通过指针安全使用,但是设置在栈上的Block,如果所属的变量作用域结束,该Block就被废弃,由于__block变量也配置在栈上,作用域结束后也会被废弃,废弃后自然就不能再使用了,为了解决这个问题,可以将栈上的Block复制到堆上面,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续使用,isa初始化为
impl.isa = &_NSConcreteMallocBlock
关于如何复制
1.当Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码
blk tmp =__main_block_impl_0(构造参数)
构造过程中会调用
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp)
objc_retainBlock
其实就是_Block_copy
函数
objc_autoreleaseReturnValue
将复制到堆上后的Block当做oc对象注册到自动释放池当中 然后返回该对象
2.Block不是作为函数的返回值返回时,也就是当函数参数传递的时候,需要手动调用copy方法,将Block从栈上复制到堆上
编译器不能判断的情况,向方法或函数的参数中传递Block时
如果不进行copy操作,程序会崩溃,这是因为getBlockArray函数执行完后,配置在栈上的block被废弃
其实也可以不让编译器判断,所有地方进行copy操作,但是拷贝操作相当消耗CPU
系统带有usingBlock的api以及GCD是不需要手动复制的
以上copy操作都是将block从栈复制到堆,如果block本来就处于堆区域,再进行copy操作,会导致block的引用计数增加,如果是全局block进行copy,则什么都不会做
block的复制操作也是符合oc对象的引用计数式内存管理思维方式
复制的时机
1.调用block的copy方法
2.Block作为函数返回值的时候
3.将block赋值给附有__strong修饰符id类型的类或Block类型成员变量时候
4.在系统api带有usingblock和GCD的api中传递block的时候
__forwarding存在的原因
不管是栈block还是堆block,都能够正确访问该_block变量
通过该功能,就可以实现无论是在block语法中,block语法外使用__block变量,还是__block变量配置在栈或者堆,都可以正确访问同一个__block变量
上述使用__block改写自动变量还遗留了部分代码如下: 其实剩余部分陌生代码也是可以解释超出作用域能存在的原因—个人理解
在oc中,c语言结构体不能含有附有__strong
修饰符的变量,因为编译器不知道何时进行c语言结构体的初始化和废弃操作,不能很好的管理内存,但是oc的运行时库可以准确把握block从栈复制到堆以及堆上block被废弃的时机,为此需要在__main_block_desc_0
结构体中增加成员变量copy
和dispose
,以及作为指针赋值给该成员变量的__main_block_copy_0
函数和__main_block_dispose_0
函数。
static void __main_block_copy_2(struct __main_block_impl_2*dst, struct __main_block_impl_2*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
block_object_assign
函数调用相当于retain
,将对象赋值在对象类型的结构体成员变量中
如果block捕获的自动变量为__strong
修饰的id对象类型,注释部分应该是BLOCK_FIELD_IS_OBJECT
static void __main_block_dispose_2(struct __main_block_impl_2*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
block_object_dispose
函数相当于release
,释放对象类型的结构体成员变量中的对象
static struct __main_block_desc_2 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_2*, struct __main_block_impl_2*);
void (*dispose)(struct __main_block_impl_2*);
} __main_block_desc_2_DATA = { 0, sizeof(struct __main_block_impl_2), __main_block_copy_2, __main_block_dispose_2};
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
void (*blk3)(void) = ((void (*)())&__main_block_impl_2((void *)__main_block_func_2, &__main_block_desc_2_DATA, (__Block_byref_b_0 *)&b, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk3)->FuncPtr)((__block_impl *)blk3);
printf("%d",(b.__forwarding->b));
到此为止,block的运行机制应该有个比较明朗的认识了,而block如何封装函数调用,其实也就是block结构体构造过程中的第一个参数—函数指针,我们在调用block的时候,底层是通过c语言函数指针进行函数的调用
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
此处构造函数中的*fp是作为第一个参数传递进来并且被block结构体保存了
函数调用环境的封装就是这个c语言函数的入参与出参,block通过变量截获得到了
结论
1.Block是封装了函数调用与函数调用环境的C语言函数
2.Block能够捕获自动变量的瞬时值是因为自动变量的瞬时值被当做block结构体的成员变量保存起来了,构造函数在初始化的时候将瞬时值赋值给了该成员变量
3.__block修饰的变量能够实现改写是因为被修饰的变量转换成了结构体(block-byref)并被声明为block结构体的成员变量,block-byref结构体中保存了原自动变量的值,原自动变量的指针指向block-byref结构体,block-byref结构体的__forwarding
指向block-byref本身,通过成员变量__forwarding
访问使用值
4.ARC环境下,block属性关键字无论是copy
还是strong
都可以,使用copy
是因为历史遗留问题,使用strong
也可以是因为编译器已经能够正确判断block的内存管理
最后,如果本文侥幸入了你的法眼,如有描述不当之处希望能够不吝赐教,要是能帮到你加深对block的理解就是我的额外收获了,感谢大佬观看,欢迎批评指正
参考
《Object-C高级编程》