作者:哈哈哈哈哈你是逗比吗
链接:https://juejin.cn/post/6917133202851102728
Blocks可以用一句话来概括:带有自动变量的匿名函数。关于Blocks的语法和用法,本文不在过度赘述。而是聚集于Blocks的本质到底是什么?他是怎么实现的?
Block结构与实质
Block实际上是C语言的扩充,也就是说,Block语法源代码是会被编译为普通的C语言源代码的。通过clang可以将其转换为我们可读代码,例如下面代码:
int main(int argc, const char * argv[]) {
void (^blk)(void) = ^{
printf("hello world");
};
blk();
return 0;
}
通过clang转换后的代码:
//block实现结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//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;
}
};
//block代码块中的实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello world");
}
//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(int argc, const char * argv[]) {
//block实现
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//block调用
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
简单的几行代码转换后竟然增加了这么多,但是仔细看,其实并不难理解。可以分为两部分:实现block、调用block。
实现block
转换后是通过下面代码实现block的:
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
它调用了__main_block_impl_0结构体来实现,而该结构体又是分别包含__block_impl结构体和__main_block_desc_0结构体2个成员变量
这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
// impl结构体
struct __block_impl {
void *isa; // 存储位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
int Flags; // 按位表示一些 block 的附加信息
int Reserved; // 保留变量
void *FuncPtr; // 函数指针,指向 Block 要执行的函数,即__main_block_func_0
};
// Desc结构体
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)};
再来看__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;
}
其中第一个参数需要传入函数指针,第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针,第三个参数flags有默认值0。重点看第一个参数,他其实就是block语法生成的block函数:
^{ printf("hello world"); };
经过转换后__main_block_func_0函数指针:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello world");
}
再来重点看__main_block_impl_0构造函数的一行代码:
impl.isa = &_NSConcreteStackBlock;
将_NSConcreteStackBlock地址赋值给isa。我们再回顾下objc_object的实现,其也包含isa指针。__main_block_impl_0结构体相当于基于objc_object结构体的oc类对象的几多题。其成员变量isa通过_NSConcreteStackBlock初始化。即_NSConcreteStackBlock相当于class_t结构体实例。在将block作为对象处理时,其类信息放置于_NSConcreteStackBlock中。
调用block
调用block就相对简单多了。将第一步生成的block作为参数传入FucPtr(也即_main_block_func_0函数),就能访问block实现位置的上下文。
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
总结:block实际上是通过block_impl结构体实现的,而该结构体的首地址是isa,因此在objc中,block实际上就算是对象。
捕获外部变量
通过上面我们已经理解block匿名函数的本质了,那么带有自动变量又是指什么呢?先看下面这段代码:
int main(int argc, const char * argv[]) {
int dmy = 256;
int val = 10
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
printf("val = %d", val);
[array addObject:@"obj"];
printf("%lu", (unsigned long)[array count]);
};
blk();
return 0;
}
通过clang转换后的代码,我们主要来看其中的不同之处,首先来看__main_block_impl_0结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;//编译时就自动生成了相应的变量
__strong id array; //注意这里
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, __strong id _array, int flags=0) : val(_val) , array(_array) {
impl.isa = &_NSConcreteStackBlock;//block的isa默认是stackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
首先来看__main_block_impl_0结构体内申明的成员变量类型与自动截取的变量类型完全相同。block表达式未使用的自动变量不会追加到结构中,例如dmy。另外,该结构体的构造函数中,加入了自动变量的初始化。 再来看看匿名函数的实现:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy 值拷贝,即 val = 10,此时的a与传入的__cself的val并不是同一个
printf("val = %d", val);
__strong id array = __cself->array; // bound by copy
[array addObject:@"obj"];
printf("%lu", (unsigned long)[array count]);
}
同时,转换后的代码里还多了下面这些代码,他们是用来干什么的?我们暂且不表,将在__block说明符章节中做详细说明:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->array,
(void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->array,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);//注意这里
void (*dispose)(struct __main_block_impl_0*);//注意这里
}
__main_block_desc_0_DATA =
{
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
通过以上代码我们可以知道:
- 变量截获的就是它的值,对象类型的自动变量,本身是指向对象的指针,指针的值是地址
- 对象类型的类型连同其修饰符一起截获
- 自动变量值被保存到Block的结构体实例中
block的存储域
前面我们说到block也是oc对象,按照isa对应的class_t信息来分类,block可以分为以下三种:
- _NSConcreteGlobalBlock
- _NSConcreteStackBlock
- _NSConcreteMallocBlock
通过命名我们可以猜测出,他们对应的block对象分别存储在全局、栈、堆上 [图片上传中...(image-307f91-1610779914606-1)]
_NSConcreteGlobalBlock
block是_NSConcreteGlobalBlock的情况有以下两种:
- 全局block:
void (^glo_blk)(void) = ^{
NSLog(@"global");
};
int main(int argc, const char * argv[]) {
glo_blk();
NSLog(@"%@",[glo_blk class]);
}
- 在函数栈上创建但没有截获自动变量:
int glo_a = 1;
static int sglo_b =2;
int main(int argc, const char * argv[]) {
void (^glo_blk1)(void) = ^{//没有使用任何外部变量
NSLog(@"glo_blk1");
};
glo_blk1();
NSLog(@"glo_blk1 : %@",[glo_blk1 class]);
static int c = 3;
void(^glo_blk2)(void) = ^() {//只用到了静态变量、全局变量、静态全局变量
NSLog(@"glo_a = %d,sglo_b = %d,c = %d",glo_a,sglo_b,c);
};
glo_blk2();
NSLog(@"glo_blk2 : %@",[glo_blk2 class]);
}
_NSConcreteStackBlock和_NSConcreteMallocBlock
- _NSConcreteStackBlock是设置在栈上的block对象,生命周期由系统控制的,一旦所属作用域结束,就被系统销毁了。
- _NSConcreteMallocBlock是设置在堆上的block对象,生命周期由程序员控制的。
ARC有效时,以下几种情况,编译器会进行判断,自动将栈上的Block复制到堆上:
- 调用Block的copy方法
- 将Block作为函数返回值时
- 将Block赋值给__strong修饰的变量或Block类型成员变量时
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
例如下面这段代码:
int num = 10;
//输出_NSConcreteStackBlock
NSLog(@"%@",[^{
NSLog(@"%d",num);
} class]);
void (^block)(void) = ^{
NSLog(@"%d",num);
};
//输出_NSConcreteMallocBlock
NSLog(@"%@",[block class]);
除此之外,都推荐使用block的copy实例方法把block复制到堆上。例如下面这个例子:
id getBlockArray()
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{NSLog(@"blk0:%d", val);},
^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
id obj = getBlockArray();
void (^blk)(void) = [obj objectAtIndex:1];
blk();
return 0;
}
运行程序崩溃,因为NSArray内的block类型为_NSConcreteStackBlock,getBlockArray函数执行完成后,就被自动释放废弃了,再执行[obj objectAtIndex:1]时,就发生异常。
为了解决上述问题,通过手动copy复制到堆上即可:
id getBlockArray()
{
int val = 10;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"blk0:%d", val) ;} copy],
[^{NSLog(@"blk1:%d", val);} copy], nil];
}
__block说明符
再回到截获自动变量值的例子,假如我们在block中试图改变自动变量val。
int main(int argc, const char * argv[]) {
NSMutableArray *array = nil;
int val = 10
void (^blk)(void) = ^{
val = 1;
array = [NSMutableArray array];
printf("val = %d", val);
};
blk();
return 0;
}
结果编译器报错。因为不能改写被截获的自动变量的值,个人猜测,如果在block内修改自动变量的值是可行的,那么修改的应该是结构体内的临时变量,与自动变量互不影响。这很容易引起开发者犯错,为了避免这种情况,编译器不允许在block中修改自动变量的值,否则报错。
为了解决这个问题有两种方法。第一种,将截获的自动变量改写成下列类型:
- 静态局部变量
- 静态全局变量
- 全局变量
静态全局变量、全局变量这两种外部变量,因为其作用域是全局的,在block内可以直接访问。所以不需要截获。 静态局部变量本身在block语法的函数外,他是怎么做到可以修改值的,先来看下面代码:
int main(int argc, const char * argv[]) {
statc int val = 10
void (^blk)(void) = ^{
val = 1;
};
blk();
return 0;
}
转化后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;//编译时就自动生成了相应的变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;//block的isa默认是stackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 从结构体成员变量int * static_val看出,block截获静态变量为结构体成员变量,截获的是静态变量的指针。
- 这看起来似乎和 自动变量是指向对象的指针 的情况差不多,但不同的是,在block内修改静态变量的值是通过修改指针所指变量的来做的:(* static_val) = 1。而这也是为什么block内能修改自动变量的原因。
但是实际使用中,我们很少采用这种方案。因为block中可以存放超过变量作用域的自动变量,而当使用超过作用域的静态局部变量时,无法通过指针访问。
为了解决此类问题,我们采用第二种方案"__block说明符",他又是怎么实现的呢?
__block的实现
我们将上述代码改为:
int main(int argc, const char * argv[]) {
__block int val = 10
void (^blk)(void) = ^{
val = 1;
printf("val = %d", val);
};
blk();
return 0;
}
转换后的代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_i_0 *__forwarding; // 注意这里!!!!!
int __flags;
int __size;
int val;
};
/ * Block结构体 */
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // 注意这里!!!!!
构造函数
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
__Block_byref_val_0 *_val,
int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/ * Block方法 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // 注意这里!!!!!
(val->__forwarding->val) = 1;// 注意这里!!!!!
}
//捕获的变量的copy和release
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
//block的描述结构体
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
/ * __block int val = 0; 转换后代码*/
__Block_byref_val_0 val = {
0, //__isa
(__Block_byref_val_0 *)&val, // __forwarding 注意这里!!!!
0, // flag
sizeof(__Block_byref_val_0), // size
10 // 变量i
};
blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
只是在自动变量上附加了__block说明符,源代码就急剧增加。我们先来看看__block变量val是怎么转换的?
/ * __block int val = 0; 转换后代码*/
__Block_byref_val_0 val = {
0, //__isa
(__Block_byref_val_0 *)&val, // __forwarding 注意这里!!!!
0, // flag
sizeof(__Block_byref_val_0), // size
10 // 变量i
};
我们发现他竟然变为了结构体实例,用__block修饰的变量变成了__Block_byref_val_0结构体类型,其定义如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_i_0 *__forwarding; // 注意这里!!!!!
int __flags;
int __size;
int val;
};
通过block结构体的初始化,我们可以看出,block捕获的实际是__Block_byref_val_0结构体的地址:
blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);
再来看看__block变量的赋值代码又是如何实现的?
//^{val = 1;}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // 注意这里!!!!!
(val->__forwarding->val) = 1;// 注意这里!!!!!
}
- 取到指向__Block_byref_val_0结构体类型的变量val的指针
- 通过__forwarding访问到自动变量val,对其进行赋值操作。
那么问题来了,他是如何解决局部自动变量超出作用域后,还能正常使用的问题?为什么要设计__forwarding?
block变量的内存管理
捕获对象类型的变量和使用__block 修饰自动变量时,都在clang转换的代码中,看到了这样两个函数。简单来说他们都是用来做block结构体变量的复制和释放的。
//捕获的变量的copy和release
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
以__block修饰的自动变量具体,栈block通过copy复制到了了堆上。此时,block使用到的__block变量也会被复制到堆上并被block持有。如果是多个block使用了同一个__block变量,那么,有多少个block被复制到堆上,堆上的__block变量就被多少个block持有。当__block变量没有被任何block持有时(block被废弃了),它就会被释放。
栈上__block变量被复制到堆上后,会将成员变量__forwarding指针从指向自己换成指向堆上的__block,而堆上__block的__forwarding才是指向自己。 [图片上传中...(image-cdc89c-1610779914605-0)]
这样,不管__block变量是在栈上还是在堆上,都可以通过__forwarding来访问到变量值。
总结:
- block捕获__block变量,捕获的是对应结构体的变量的地址,该结构体也哟isa指针,也可以理解为对象。
- 当block复制到堆上,block使用到的__block变量也会被复制到堆上并被block持有。
最后,简单提下block循环引用,其产生循环引用的原理根普通循环引用一样,解决的办法也一样:
- 使用弱引用,避免产生相互循环引用
- 在合适的时机手动断环
不在过多介绍。
参考文章:
Objective-c高级编程-ios与OS X多线程和内存管理
iOS Block原理探究以及循环引用的问题
文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。