本文主要整理了Objective-C的Block实现方式。
iOS 其他相关博文链接iOS-day-by-day
目录
- 1.Objective-C与其他语言中对比
- 2.Block模式
- 3.Block底层实现
一. Objective-C和其他语言对比
在其他许多编程语言中,也存在Block被称为闭包(Closure)、lambda计算等。
程序语言 | Block的名称 |
---|---|
C+ Blocks | Block |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
C++ | Lambda |
Javascript | Anonymous function |
二.Block常用术语
1.Block语法结构
Block是带有自动变量值得匿名函数。
例如:
// 1
^ int (int count) { return count + 1;}
// 2
^ (int count) {printf("Block");}
// 3
^{printf("Block");}
Block字面值的写法:
^ (double firstValue, double secondValue) {
return firstValue *secondValue;
}
上面写法省略了返回值的类型,可以显示的指出返回值类型。通过使用typedef
可以声明“blk_t”类型变量
typedef double (^blk_t)(double, double);
blk_t blk = ^(double firstValue, double secondValue){
return firstValue * secondValue;
};
NSLog(@"%f", blk(3, 4));
Block也是一个Objective-C对象,可以用于赋值,当参数传递,也可以放在集合中。
2.截获自动变量值
int val = 10;
void (^blk_t) (void) = ^ {
NSLog(@"val = %d", val);
};
val = 2;
blk_t(); // val = 10
执行结果,不是val = 2,而是val = 10。捕获的是执行Block语法时的的瞬间值。这就是自动变量值的捕获。
3.__block说明符
自动变量值截获只能保存执行Block语法瞬间值。保存后就不能修改该值。否则,会产生编译错误。如果想要修改截获的自动变量,需要使用__block
修饰。
__block int val = 10;
void (^blk_t) (void) = ^ {
val = 11;
NSLog(@"val = %d", val );
};
blk_t(); // val = 11
使用附有__block
说明符的自动变量可在Block中赋值,该变量称为_block
变量。
三.Block底层实现
1. Block的实质
Block是“带有自动变量值得匿名函数”。但Block究竟是什么呢?接下来我们一起来分析。
为了研究编译器是如何实现block的,需要使用clang命令,可以将Objective-C的源码改写成C++源代码,命令如下:
clang -rewrite-objc xxx.c
新建一个main.c的源文件:
int main(int argc, const char * argv[]) {
void (^blk) (void) = ^ {
NSLog(@"Hello, Block!");
};
blk();
return 0;
}
在文件所在的文件夹,使用命令:clang -rewrite-objc main.m
,在目录中得到一个名为main.cpp
的文件,区区七八行代码,转换后变成10万行,其中关键代码:
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) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d3_9wynvv910yz9wv20fw56jz0h0000gn_T_main_dd5e8c_mi_0);
}
//附加信息,如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)};
// main
int main(int argc, const char * argv[]) {
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);
return 0;
}
接下来我们分析一下,这几行关键代码是什么意思?
从int main()
中开始。简化一下发现:
// block
void (*blk)(void) = __main_block_impl_0(_main_block_func_0, &_mian_block_desc_0_DATA);
//调用
(((*blk)->FuncPtr)blk);
第一条语句中出现的__main_block_impl_0()
,__main_block_func_0
和__mian_block_desc_0_DATA
是什么意思?别急,我们一条条来分析。
__main_block_impl_0()
实际上是__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
声明如下:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
预留区域和Block的大小。接下来是构造函数:
__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;
}
通过前面调用构造函数,可以简化为:
isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
其中:
isa = &__NSConcreteStackBlock;
将Block指针赋给Block的结构体成员变量isa。其实,Block就是Objective-C对象。想要理解上句代码的意思,需要理解Objective-C类和对象的实质。
id 类型
typedef struct objc_object *id;
typedef struct objc_class *Class;
id为objc_object
结构体的指针类型。Class 为objc_class
结构体的指针类型。objc_class
结构体在objc4/runtime.h
中声明:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
两者很相似,objc_object
结构体和objc_class
结构体归根结底实在各个对象和类的实现中使用的最基本的结构体。
下面通过简单的MyObject
来看一下。
@implementation MyObject
{
int val0;
int val1;
}
@end
转换为C++源代码,MyObject结构体
struct MyObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int val0;
int val1;
};
实例变量val0和val1被直接声明为结构体的成员。上述中,结构体都是基于objc_class结构体的class_t结构体。class_t声明如下:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
该结构体实例中,持有声明的成员变量、方法的名称、方法的实现、属性以及父类的指针。回到
isa = &__NSConcreteStackBlock;
__NSConcreteStackBlock
相当于class_t结构体实例,关于该类的信息都存放在__NSConcreteStackBlock中。
2. 截获自动变量
int main(int argc, const char * argv[]) {
int val = 10;
void (^blk) (void) = ^ {
printf("%d", val);
};
blk();
return 0;
}
使用clang命令转为C++代码,看看和之前的有什么区别。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val; // <== val
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : 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) {
int val = __cself->val; // bound by copy
printf("%d", val);
}
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[]) {
int val = 10;
void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
对比之前的代码发现,Block语法表达式中使用的自动变量被作为成员变量追加到__main_block_impl_0
结构体中。并且类型完全相同,另外注意,未使用的变量并不会追加。
__main_block_impl_0
结构体实例初始化:
isa = &__NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
val = 10;
这时,在__main_block_impl_0
结构体实例中,自动变量值被截获。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中。
3.__block说明符
在Block中修改捕获的外部变量,需要使用__block修饰符(__block存储域类说明符)。在C中存储域类:typedef、extern、static、auto、register。__block与他们的作用相同,用来指定将变量值设置到哪个存储域中。
下面看一个例子:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int val = 10;
void (^blk) (void) = ^{
val = 11;
};
blk();
}
return 0;
}
使用clang命令,转为C++源码如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref <=======注意
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 11;
}
// <====== 相当于retain
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*/);}
// <====== 相当于release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
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[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
上面是含有__block修饰符的代码。对比一下和之前的差别。
__block int val = 10;
__block修饰的变量变成结构体
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding; //
int __flags;
int __size;
int val; // <=== val 作为结构体的成员变量
};
变量val
被作为结构体的成员变量。_forwarding指针什么作用?后面在分析。
那变量赋值的代码呢?
^{val = 11;}
如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 11;
}
变量的复制
- 对于block外部变量的引用,block默认是将其复制到其数据结构中来实现访问的。
- 对于__block修饰的外部变量引用,block是复制其引用地址来实现访问的。
另外,可以参考《招聘一个靠谱的iOS (下)13、14题》。
ARC,MRC 对Block类型的影响
在ARC开启的情况下,将只会有NSConcreteGlobalBlock和NSConcreteMallocBlock类型的block。原本NSConreteStackBlock被NSConcreteMallocBlock类型替代。
在Block中,如果只使用全局或静态变量或者不是用外部变量,那么Block块的代码会存储在全局区。
在ARC中
- 如果使用外部变量,Block块的代码会存储在堆区。
在MRC中
- 如果使用外部变量,Block块的代码会存储在栈区。
Block默认情况下不能修改外部变量,只能读取外部变量。
在ARC中
- 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
- 外部变量在栈中,这个变量会被copy到为Block代码块所分配的堆中。
在MRC中
- 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
- 外部变量在栈中,这个变量会被copy到为Block代码块所分配的栈中。
如果需要修改外部变量,需要在外部变量前声明__block。
在ARC中
- 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
- 外部变量存在栈中,这个变量会被转移到堆中,不是复制。
在MRC中
- 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
- 外部变量存在栈中,这个变量在Block块内与Block块外地址相同。
关于Block的面试题
参考链接
谈Objective-C block的实现
Block教程系列
对Objective-C中Block的追探
iOS中block实现的探究
Block 编程