众所周知,block可以封装一个匿名函数为对象,并捕获上下文所需的数据,并传给目标对象在适当的时候回调。
本文将从源码出发,分析block的本质。
一、Block定义
1. Block 定义及使用
返回值类型 (^block变量名)(形参列表) = ^(形参列表) {
};
// 调用Block保存的代码
block变量名(实参);
2. 项目中使用格式
在项目中,通常会重新定义block的类型的别名,然后用别名来定义block的类型
// 定义block类型
typedef void (^Block)(int);
// 定义block
Block block = ^(int a){};
// 调用block
block(3);
二、Block底层实现
block的底层实现是结构体,和类的底层实现类似,都有isa指针,可以把block当成是一个对象。下面通过创建一个控制台程序,来窥探block的底层实现
block 的内存结构图:
Block_layout结构体成员含义如下:
- isa: 指向所属类的指针,也就是block的类型
- flags: 标志变量,在实现block的内部操作时会用到
- Reserved: 保留变量
- invoke: block执行时调用的函数指针,block内部的执行代码都在这个函数中
- descriptor: block的详细描述,包含 copy/dispose 函数,处理block引用外部变量时使用
- variables: block范围外的变量,如果block没有调用任何外部变量,该变量就不存在
Block_descriptor结构体成员含义如下:
- reserved: 保留变量
- size: block的内存大小
- copy: 拷贝block中被 __block 修饰的外部变量
- dispose: 和 copy 方法配置应用,用来释放资源
具体实现代码如下(代码来自Block_private.h):
enum {
BLOCK_REFCOUNT_MASK = (0xffff),
BLOCK_NEEDS_FREE = (1 << 24),
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), /* Helpers have C++ code. */
BLOCK_IS_GC = (1 << 27),
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_DESCRIPTOR = (1 << 29)
};
/* Revised new layout. */
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
二、Block转成c++代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 最简block
^{ };
}
return 0;
}
利用 clang 把 .m 的文件转换为 .cpp 文件,就可以看到 block 的底层实现了
$ clang -rewrite-objc main.m
转换后的代码:
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) {
}
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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
}
return 0;
}
从代码中可以看出,__main_block_impl_0就是block的C++实现(最后面的_0代表是main中的第几个block),__main_block_func_0是block的代码块,__main_block_desc_0是block的描述,__block_impl是block的定义。
__block_impl结构体为
struct __block_impl {
void *isa;//指向所属类的指针,也就是block的类型
int Flags;//标志变量,在实现block的内部操作时会用到
int Reserved;//保留变量
void *FuncPtr;//block执行时调用的函数指针
};
__main_block_impl_0解释如下:
- impl: block对象
- Desc: block对象的描述
其中,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) 这是显式构造函数,flags的默认值为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_impl_0的isa指针指向了_NSConcreteStackBlock,所有的局部block都是在栈上门创建
- 从main函数中看, __main_block_impl_0的FuncPtr指向了函数__main_block_func_0
- __main_block_impl_0的Desc也指向了定义__main_block_desc_0时就创建的__main_block_desc_0_DATA,其中Block_size记录了block结构体大小等信息
__main_block_desc_0成员含义如下:
- reserved: 保留变量
- Block_size: block内存大小,sizeof(struct __main_block_impl_0)
三、Block变量截获
局部变量截获 是值截获。 比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。
同样,在block里如果修改变量num,也是无效的,甚至编译器会报错。
对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。
NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
void(^block)(void) = ^{
NSLog(@"%@",arr);//局部变量
[arr addObject:@"4"];
};
[arr addObject:@"3"];
arr = nil;
block();
打印为1,2,3
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置为nil,对block没有影响,而该对象调用方法会影响
局部静态变量截获 是指针截获。
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。
__block 修饰的外部变量
对于用 block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改block 修饰的外部变量的值。
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
输出为:
age = 18
为什么使用__block 修饰的外部变量的值就可以被block修改呢?
我们使用 clang 将 OC 代码转换为 C++ 文件:
__block int val = 10;
转换成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
会发现一个局部变量加上block修饰符后竟然跟block一样变成了一个Block_byref_val_0结构体类型的自动变量实例!!!!
四、Block的几种形式
block有三种类型:
- 全局块(_NSConcreteGlobalBlock)
- 栈块(_NSConcreteStackBlock)
- 堆块(_NSConcreteMallocBlock)
这三种block各自的存储域如下图:
- 全局块存在于全局内存中, 相当于单例.
- 栈块存在于栈内存中, 超出其作用域则马上被销毁
- 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
a、遇到一个Block,我们怎么这个Block的存储位置呢?
1、Block不访问外界变量(包括栈中和堆中的变量)
Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。
2、Block访问外界变量
MRC 环境下:访问外界变量的 Block 默认存储栈中。
ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。
b、ARC下,访问外界变量的 Block为什么要自动从栈区拷贝到堆区呢?
栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃。如下图:
为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。
Block的类 | 位置 | 复制效果 |
---|---|---|
NSGlobalBlock | 程序数据区 | 什么也不做 |
NSStackBlock | 栈 | 从栈复制到堆上 |
NSMallocBlock | 堆 | 引用计数增加 |
代码示例:
1、不使用外部变量的block是全局block
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);
输出:NSGlobalBlock
2、使用外部变量并且未进行copy操作的block是栈block
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);
输出:NSStackBlock
日常开发时是将block当做参数传递给方法:
- (void)testWithBlock:(void(^)(NSString *string))callback{
NSString *string = @"string";
callback(string);
NSLog(@"%@",[callback class]);
}
NSInteger num = 10;
[self testWithBlock:^(NSString *string) {
NSLog(@"stackBlock:%zd",num);
}];
输出:NSGlobalBlock
3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
- 比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
NSLog(@"%@",[globalBlock class]);
输出:NSGlobalBlock 仍是全局block
- 而对2中的栈block进行赋值操作:
NSInteger num = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);
输出:NSMallocBlock
对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
比如:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block
{
block();
dispatch_block_t tempBlock = block;
NSLog(@"%@,%@",[block class],[tempBlock class]);
}
输出:NSStackBlock,NSMallocBlock
即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。
c、block变量与forwarding
在copy操作之后,既然block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?forwarding 终于要闪亮登场了,如下图:
通过forwarding, 无论是在block中还是 block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。
- 面试基础
iOS面试基础知识 (一)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (二)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (三)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (四)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (五)
https://github.com/iOS-Mayday/heji
网友评论