前言
Block 是 C 语言的扩充功能,可以用一句话来表示Blocks的扩充功能:带有局部变量(自动变量)的匿名函数。
Block 是在开发过程中使用非常频繁。它可以在需要使用的时候直接调用,非常好的保持了上下文的关系。但是我对于它的具体实现一直不是很清楚。
简单调用代码:
#define block ^{NSLog(@"===== block1 age: %d", age);}
#import <Foundation/Foundation.h>
static int weight = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block1)(void)= ^{
NSLog(@"===== block1");
};
int age = 10;
void(^block2)(void)= block;
void(^block3)(void)= ^{
NSLog(@"===== block2, weight: %d", weight);
};
NSLog(@"\n直接创建: \t%@\n直接创建+copy: \t%@\n创建并无调用变量: \t%@\n创建并调用局部变量: \t%@\n创建并调用全局变量: \t%@",[block class], [[block copy] class], [block1 class], [block2 class], [block3 class]);
}
return 0;
}
输出内容:
直接创建: __NSStackBlock__
直接创建+copy: __NSMallocBlock__
创建并无调用变量: __NSGlobalBlock__
创建并调用局部变量: __NSMallocBlock__
创建并调用全局变量: __NSGlobalBlock__
字段说明
匿名函数
Block 表达式
^
返回值类型
参数列表
表达式
- ^ 是插入记号,表示将要插入一个 Block
- 返回值类型 可以省略,但是如果表达式中有返回值,那么返回值类型一定要和返回值类型一致
- 参数列表 可以省略,说明调用时没有参数传递
所以Block 的常见写法有如下几种:
^int (int count) {return count + 1;} // 有返回值类型,有参数列表
^(int count) {return count + 1;} // 没有有返回值类型,有参数列表
^int {return 1;} // 有返回值类型,没有参数列表
^ {NSLog(@"Block");} // 没有有返回值类型,没有参数列表
Block 类型
类型 | 名称 | 存放位置 | 环境 |
---|---|---|---|
NSGlobalBlock | 全局Block | data区 - 全局访问 | 没有访问auto变量 |
NSStackBlock | 栈Block | 栈区 - 自动释放 | 访问了auto变量 |
NSMallocBlock | 堆Block | 堆区 - 手动释放 | 栈Block 调用了 copy |
一般来说,在 ARC 模式下,把 Block 赋值给变量的时候,系统会自动调用 copy 操作把 Block 复制到堆上,所以基本上不会访问到栈Block的。这里把 Block 放在堆上的主要原因就是 栈区的变量在超出生命周期之后自动释放,很容易导致 Block 还没有调用就已经释放了。
自动变量
在 Block 中,表达式会截获所有使用的自动变量的值(保存该自动变量的瞬时值)。这个值的捕获时机是在 Block 定义的时候,所以在 Block 定义后再修改变量的值是不会对 Block 内部使用的值造成影响的。
但是 Block 自动捕获的变量是不可以修改的。如果需要修改这个变量并且保持 Block 内外值一致。就需要在外部定义变量的时候使用 __block
来修饰。这里需要注意一下,操作可变对象是可以的。给可变对象重新赋值是不可以的。
NSMutableArray *array = [NSMutableArray new];
void (^blk)(void) = ^ {
[array addObject:[NSObject new]];
};
blk();
NSLog(@"%lu", (unsigned long)array.count);
这段代码是没有问题的,因为虽然是在修改变量 array 的内部值,但是 array 本身并没有变。
NSMutableArray *array = [NSMutableArray new];
void (^blk)(void) = ^ {
array = [NSMutableArray new];
};
blk();
NSLog(@"%lu", (unsigned long)array.count);
这段代码是会报错,xcode 的错误信息是 Variable is not assignable (missing __block type specifier)
。因为这里是给变量 array 重新赋值了。
另外,在使用 Block 的时候要注意 C 语言的数组,由于自动捕获机制没有针对 C 语言数据的实现。所以如果有这方面的需求的话,需要使用指针来解决这个问题。
Block 实质
通过查看源码可以知道,Block 的大致结构如下:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_obj_1 *obj; // by ref
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_1 *_obj, __Block_byref_age_0 *_age, int flags=0) : obj(_obj->__forwarding), age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block
在开发过程中,如果想让 block 内外的变量值保持一致,就需要使用 __block 来修饰定义的变量。
__block 修饰的变量最大的特点是在生成的结构体中存在 __forwarding 变量,这个变量始终指向变量的实际地址。例如 Block 从栈copy到堆上的时候,栈区定义的变量值也会复制到堆上,所以对应的__block 结构体中的 __forwarding 指向堆中的地址。这样每次访问这个变量的时候,都会访问 __forwarding 指针指向的内存地址的值,从而保持值一致。
__weak
block使用过程中,最常见的一个问题就是循环引用,循环引用的定义和引发的后果这里就不多做阐述。而避免循环引用的方式也很简单,就是在外部使用 __weak 修饰符申明一下,block 内部引用的是这个 __weak 修饰的变量。但是这样也会有一个问题,就是如果在 block内部调用的时候,这个对象有可能已经释放了。这样会导致原本的逻辑遭到破坏,这时候有人提出来,完美的解决办法就是 block 外部使用 __weak 修饰一下,block 内部 使用 __strong 重新赋值一下,这样就可以在使用的时候保证这个值的存在了。
最后
欢迎斧正~
网友评论