本篇博文介绍iOS4引入的C语言扩展功能的“Blocks”。如果学习过lambda表达式,那么对这个知识在使用上并不难。但是这次介绍的blocks和C++中lambda表达式又一些区别。
(博文较长,层层递进的方式阐述,如有需要,选择阅读)
1. 面试题
1)block的实质是什么?一共有几种block?都是什么情况下生成的?
2)为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
3)下面的代码输出是?
void test1() {
int a = 10;
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;//a的值改变之后,block应该输出什么?
block();
}
上述面试题会在后面的介绍中一一解决。
2. Blocks模式
2.1 block语法
在C++、Java等大多数编程语言中,都有lambda表达式,这是叫的名称不一样,如闭包,block等。
在objective-c中,其格式为:
^ 返回值 参数列表 {代码块/表达式};
例如:
^ BOOL (int left, int right){
return a > b;
}
如果没有返回值、参数列表,可以用void代替,也都可以将其省略。如:
^{NSLog(@"block的使用")};
2.2 block简单理解
下面来看一下C语言里面的指针。
int (*funcPtr)(int num);
int Func(int a) {
printf("%d",a);
return a;
}
funcPtr = Func;
(*funcPtr)(10);
现在把 * 变成 ^。
int (^funcPtr)(int num);
上述的int (^funcPtr)(int num)
就是一个block类型。于是可以这样使用。
int (^funcPtr)(int num) = ^int(int a) {
NSLog(@"%d",a);
return a;
}
funPtr(10);
于是对于block来说,很类似一个函数指针,它也可以使用typedef
来提高代码的阅读质量,便于理解。
对与函数指针来说:
typedef char (*FUNPTR)(int);
PTRFUN funPtr;//定义一个函数指针变量
对于block来说
typedef char (^blk_t)(int);
blk_t blk;//定义一个block变量
综上,block就是一个类型,这个类型组要是组织和描述代码块,这个被描述的对象就是一个block对象,但是这个对象有的特殊。
2.3 block的使用
介绍完基本的语法,现在来使用和学习block的特殊在哪。
2.3.1 无参无返回值的定义和使用
void (^blockNothing)(void) = ^ {
NSLog(@"无参无返回值的定义和使用");
};
2.3.2 无参有返回值的定义和使用
int (^blockWithRet)(void) = ^int {
NSLog(@"无参无返回值的定义和使用");
return 1;
};
2.3.3 有参无返回值的定义和使用
void (^blockWithParm)(int parm) = ^(int count) {
NSLog(@"无参无返回值的定义和使用");
};
2.3.4 有参有返回值的定义和使用
int (^blockNor)(int parm) = ^ int (int parm) {
NSLog(@"无参无返回值的定义和使用");
return parm;
};
2.3.5 做函数参数
void funcWithParm(int (^block)(int)) {
}
但是通常都会用typedef提高阅读品质
typedef int (^block_t)(int);
void funcWithParm(block_t blk) {
}
2.3.6 做返回值
typedef int (^block_t)(int);
block_t funcWithRet() {
return ^int(int parm) {
return parm;
};
}
有了上面的基础,就可以解决面试题(1)的一个小问题一共有几种block?
其实就是问一个变量可以有几种形式。对于一个变量来说,它可以存在数据段(如全局变量、静态变量等),也可以在栈上(临时变量、参数等),也可以在堆上(malloc,alloc等)。于是block变量也不例外,也是上述的三种。对于block来说,有三种标记来标示三种block。
类型 | 存储位置 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 数据段 |
_NSConcreteMallocBlock | 堆 |
3、block的实现
前面说过block经过组织和描述,现在就来看看是怎么组织和描述的,并且这到底是个什么变量。
3.1 C++代码
下面是Objectiv-C的代码
int main(int argc, const char * argv[]) {
void (^block)(void) = ^{
printf("Block\n");
};
block();
return 0;
}
将其转换为C++代码如下:(在当前目录下,在终端上输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m)
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) {
// __cself指针指向的是一个__main_block_impl_0对象,其实传入是一个block对象,
// 由于最终是block自己回调那个这个函数,因此这个__cself可以看作是self指针。
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)};
int main(int argc, const char * argv[]) {
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
3.2 简单block的数据结构
// block 类,类似一个抽象类,任何block都会有这个类的对象作为成员
struct __block_impl {
void *isa; // 指向存储域类型,类似指向类对象
int Flags; // 标志变量通常
int Reserved; // 保留变量
void *FuncPtr; // 函数指针
};
// 用来描述block的类
static struct __main_block_desc_0 {
size_t reserved; // 保留值
size_t Block_size; // block大小
}
// 可以理解为一个block子类,0表示第几个block
// 只是通过组合实现的类似继承的效果,是has-a,不是is-a
struct __main_block_impl_0 {
struct __block_impl impl; // 类似父类
struct __main_block_desc_0* Desc; // 描述,这个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;
}
};
下面在main函数中,找到这个block是怎么初始化的
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 简化一下,去除强转:
// 好像也没做啥,就是初始化了一个对象,然后调用内部函数指针执行的函数
void (*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
block->FuncPtr(block);
3.3 block截获变量的数据结构
先来贴上如下代码,将全局变量,静态局部变量,局部变量定义。
int globalVar = 10;
int main(int argc, const char * argv[]) {
static int staticVar = 20;
int norVar = 30;
void (^block)(void) = ^{
printf("%d %d %d\n",globalVar, staticVar, norVar);
};
block();
return 0;
}
转换为C++代码后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *staticVar; // 静态变量指针
int norVar; // 局部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticVar, int _norVar, int flags=0) : staticVar(_staticVar), norVar(_norVar) {
//构造函数多了两个参数
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 描述block的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 静态局部变量通过地址进行访问
int *staticVar = __cself->staticVar; // bound by copy
// 局部变量传值访问
int norVar = __cself->norVar; // bound by copy
printf("%d %d %d\n",globalVar, (*staticVar), norVar);
}
可以发现block截获了使用了的
变量,方式就是创建自己的成员变量,并用截获的值进行赋值,并且规律是:
- 不截获全局变量
- 截获静态变量的地址
- 截获局部变量的值。
一般我们可能回在block 中改变我们截获的值,比如改变局部变量。
int globalVar = 10;
int main(int argc, const char * argv[]) {
static int staticVar = 20;
int norVar = 30;
void (^block)(void) = ^{
++norVar;
printf("%d %d %d\n",globalVar, staticVar, norVar);
};
block();
return 0;
}
于是回出现下面的错误。
Variable is not assignable (missing __block type specifier)
但是改变其他变量的时候,又不会出现这样的问题。那么现在看到C++代码之后就明朗了,因为局部变量是指传递的,不能在内部进行修改。这就解决了面试题(2)的第一问为什么在默认情况下无法修改被block捕获的变量?
但是如果非要修改,可以用关键字__block
。
但是在这之前,先来看看面试题(3)。
void test1() {
int a = 10;
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;
block();
}
因此可以轻易的得出该变量的值是 10.
3.4 __block关键字
把上面的局部变量,改成下面这样,就不会产生错误。现在又来转换成C++代码来解析。
__block int norVar = 30;
直接分割数据结构解析。
#pragma clang assume_nonnull end
int globalVar = 10;//全局变量
// __block关键字描述的变量最终会被描述和组织成一个类
struct __Block_byref_norVar_0 {
void *__isa;
__Block_byref_norVar_0 *__forwarding; // 这里有一个自身类型的指针,类型链表的next指针一样
int __flags;
int __size;
int norVar; // 存储的值
};
//block变量
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *staticVar;
__Block_byref_norVar_0 *norVar; // __block对象的地址
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticVar, __Block_byref_norVar_0 *_norVar, int flags=0) : staticVar(_staticVar), norVar(_norVar->__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_norVar_0 *norVar = __cself->norVar; // bound by ref
int *staticVar = __cself->staticVar; // bound by copy
// 重点:对象->自己->值,这里会不会多此一举呢?后面解释
++(norVar->__forwarding->norVar);
printf("%d %d %d\n",globalVar, (*staticVar), (norVar->__forwarding->norVar));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->norVar, (void*)src->norVar, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->norVar, 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};
//main函数
int main(int argc, const char * argv[]) {
static int staticVar = 20;
__attribute__((__blocks__(byref))) __Block_byref_norVar_0 norVar = {(void*)0,(__Block_byref_norVar_0 *)&norVar, 0, sizeof(__Block_byref_norVar_0), 30};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticVar, (__Block_byref_norVar_0 *)&norVar, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
main函数中有对__block类的一个对象的初始化。
__attribute__((__blocks__(byref))) __Block_byref_norVar_0 norVar = {(void*)0,(__Block_byref_norVar_0 *)&norVar, 0, sizeof(__Block_byref_norVar_0), 30};
简化一下:
__Block_byref_norVar_0 norVar = {
0,
&norVar, // 传递的是自己的地址
0,
sizeof(__Block_byref_norVar_0),
30
}
总结一下:在它的成员变量里面有一个__forwarding指针指向了自己。于是访问的时候,norVar对象找到自己的__forwarding成员,从而找到norVar的值。这样是不是感觉多此一举,为什么不直接访问这个norVar的值?这个就要说一下block的存储域了。
到这里就解决了面试题(2)的第二个问题__block都做了什么?
++(norVar->__forwarding->norVar);
3.5 存储域
在一开始我们知道一共有三种block,就像变量一样。因此对于block的这三个存储域可以这样理解:当我门创建一个全局变量的时候,那么这个变量的就在数据段上,创建局部变量、返回值的时候是在栈上,block是一样的:
void (^globalBlock)(void) = ^{
printf("_NSConcreteGlobalBlock\n");
};
int main(int argc, const char * argv[]) {
void (^stackBlock)(void) = ^{
printf("_NSConcreteStackBlock\n");
};
globalBlock();
stackBlock();
return 0;
}
但是对于_NSConcreteMallocBlock
来说,我们是创建不出来的。就像我们不能直接创建出一个存在堆上的int型变量,但是可以通过复制来存在堆上。
例如,我们malloc一个int大小的空间,然后将一个int的值复制进去。
Blocks提供了将Block和__block变量从栈上复制到堆上的方法。之所以复制,是为了解决栈上的block的生命周期短的问题。复制到堆上所以Block将_NSConcreteMallocBlock
赋值给impl.isa
。
下面举一个例子。
typedef void (^blk_t)(int);
blk_t testFunc(int rate) {
return ^(int parm){
printf("return a block with parm %d\n", parm * rate);
} ;
}
int main(int argc, const char * argv[]) {
blk_t res = testFunc(10);
res(10);
return 0;
}
在ARC环境下,这个是没有问题的,因为ARC会自动加入copy操作。但是非ARC环境下:
error:
returning block that lives on the local stack
return ^(int parm){
^~~~~~~~~~~~
1 error generated.
修改如下:
typedef void (^blk_t)(int);
blk_t testFunc(int rate) {
return [^(int parm){
printf("return a block with parm %d\n", parm * rate);
} copy];
}
int main(int argc, const char * argv[]) {
blk_t res = testFunc(10);
res(10);
return 0;
}
在其他变量的情况下,我们是不用在意复制这个问题的,因为会返回一个临时变量进行赋值,但是block是一个特殊的变量,是一段代码。满足下面的条件编译器会自动帮助我们进行添加copy操作:
1)Block作为返回值的时候
2)将Block赋值给附有__strong修饰符id类型或Block成员变量的时候
3)不进行手动添加的情况如下:向方法或函数的参数中传递Block时(除在方法名含有usingBlock的Cocoa框架方法或Grand Central Dispatch 的API、ARC环境外)
举个例子:
@interface BlockClass : NSObject
-(id)getBlockArray;
@end
@implementation BlockClass
-(id)getBlockArray {
int tmpVal = 0;
return [[NSArray alloc]initWithObjects: ^{
printf("%d\n", tmpVal);
},
nil];
}
@end
int main(int argc, const char * argv[]) {
BlockClass *blockclass = [BlockClass new];
void (^block)(void) = [[blockclass getBlockArray] objectAtIndex:0];
block();
return 0;
}
上述代码会发生崩溃。
在ARC环境下还是会自动添加copy。
这样修改后就不会了。
-(id)getBlockArray {
int tmpVal = 0;
return [[NSArray alloc]initWithObjects: [^{
printf("%d\n", tmpVal);
} copy],
nil];
}
3.6 __forwarding
下面改说一下__block了。还记得__block的结构体里面有一个__forwarding的指针吗?这个指针就是在__block在被复制到堆上的时候发生作用。由于Block中使用了__block变量,当Block被复制到了堆上的时候,__block也要被复制到堆上。
如果多个BLock中使用了同一个__block变量,__block会被第一次被使用的时候复制到堆上,然后增加引用计数。
当block复制到堆上后,__block变量的数据结构会发生下面的变化。
于是无论是在block中,block外使用__block变量,又或者__block在堆上或者在栈上,都可以访问同一个__block变量。
4. Block的循环引用
现在来到Block的最后一个内容,循环引用。在说到Objective-C的内存管理的时候,也说到了循环引用的问题。Block的循环引用的问题用一个例子来说明:
/**
循环引用
*/
typedef void (^blk_t)(void);
@interface MyClass : NSObject
@property(nonatomic, strong) blk_t block;
@end
@implementation MyClass
-(instancetype) init {
self = [super init];
_block = ^{
NSLog(@"%@",[self class]);
};
return self;
}
-(void)dealloc {
NSLog(@"dealloc");
}
@end
int main(int argc, const char * argv[]){
MyClass *obj = [MyClass new];
NSLog(@"%@", [obj class]);
return 0;
}
没有调用dealloc。
这是因为MyClass对象堆Block时强引用,而Block使用了__strong修饰符id类型self。并且BLock由于满足Block赋值给了作为成员变量的block,因此复制到了堆上。如此:block持有self,self持有block。
其实编译器都提醒了:
更改方法为:
-(instancetype) init {
self = [super init];
id __weak tmpSelf = self;
_block = ^{
NSLog(@"%@",[tmpSelf class]);
};
return self;
}
5. 结语
到此block语法结束。本篇博文内容较多,有些地=地方可能存在错误和不足,也有一些地方介绍的不好,但这都是学习block的心得与经验,望前辈与大佬不吝指正与批评。后续还会对内容进行精练与添加……