什么是block
block是什么?虽然所有的iOS开发都会使用到这个对象,但实际上要完全搞清楚block也是不容易的。
首先下个定义,实际上block是包含函数指针及上下文调用的对象。后面,我们再慢慢看下block内部究竟是怎样进行函数调用和上下文的处理。
block实质
举个?
我们先拿一个简单的block进行block内部实现的分析。
- (void)test {
int temp = 1;
void (^Mblock)(void) = ^() {
NSLog(@"%d", temp);
};
Mblock();
}
以上是一个非常简单的block,无参数,无返回值。我们就拿这个进行分析。老规矩。clang -rewrite-objc xx.m
,编译后变成了如下内容:
struct __MBlock__test_block_impl_0 {
struct __block_impl impl;
struct __MBlock__test_block_desc_0* Desc;
int temp;
__MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int flags=0) : temp(_temp) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {
int temp = __cself->temp; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_tlqj59rx34dfrxpflntjghz80000gn_T_MBlock_a175b7_mi_0, temp);
}
static struct __MBlock__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __MBlock__test_block_desc_0_DATA = { 0, sizeof(struct __MBlock__test_block_impl_0)};
static void _I_MBlock_test(MBlock * self, SEL _cmd) {
int temp = 1;
void (*Mblock)(void) = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp));
((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}
如上的代码,我们看出来。block的调用其实是使用函数指针进行调用的。block对象实际上在编译器处理后是__MBlock__test_block_impl_0
对象。
__MBlock__test_block_impl_0
的实现:
struct __MBlock__test_block_impl_0 {
struct __block_impl impl;
struct __MBlock__test_block_desc_0* Desc;
int temp;
//初始化
__MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int flags=0) : temp(_temp) {
impl.isa = &_NSConcreteStackBlock; //栈block
impl.Flags = flags; //标记
impl.FuncPtr = fp; //实现
Desc = desc; //描述
}
};
而在初始化的时候传入的参数:
void (*Mblock)(void) = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp));
__MBlock__test_block_func_0
__MBlock__test_block_desc_0_DATA
temp
我可以通过上面的注释看出,__MBlock__test_block_func_0
就是我们在block内部的实现,是一个函数的指针,并通过初始化的时候赋值给impl。
而__MBlock__test_block_desc_0_DATA
是__MBlock__test_block_desc_0
的初始化方法,内部存储的是关于block的一些描述,size和预存字段reserved。
最后的参数则是block内部需要的参数temp。
调用
((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
这个函数精简一下:
(Mblock->FuncPtr)(Mblock);
实际上就是调用函数指针,并把block传入。最后调用实现函数:
static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {
int temp = __cself->temp; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_tlqj59rx34dfrxpflntjghz80000gn_T_MBlock_a175b7_mi_0, temp);
}
阶段小结
实质上block就是一个函数指针的包装,它同时还有截获变量的功能,并自己维护一个内存管理(关于内存管理方面,后面会介绍)。
Block截获变量
关于block截获变量的问题在笔试中经常考到,这里探讨一下block在截获变量的时候做了什么。
首先,我们搞清楚变量的类型有哪些。变量包括:
- 全局变量
- 静态全局变量
- 局部变量(基本数据类型、引用数据类型)
- 静态局部变量
关于这个问题,我们同样举个?:
下面这个代码中,我们定义了静态全局变量temp3、全局变量temp4、局部变量temp、局部变量temp2、强类型的strObj。
#import "MBlock.h"
@implementation MBlock
static int temp3 = 3;
int temp4 = 4;
- (void)test {
int temp = 1;
static int temp2 = 2;
__strong NSObject *strObj;
void (^Mblock)() = ^() {
NSLog(@"%d", temp);
NSLog(@"%d", temp2);
NSLog(@"%d", temp3);
NSLog(@"%d", temp4);
NSLog(@"%@", strObj);
};
Mblock();
}
@end
我们通过编译的源码来看下最后怎么截获变量的吧。(保留关键代码)
static int temp3 = 3;
int temp4 = 4;
struct __MBlock__test_block_impl_0 {
struct __block_impl impl;
struct __MBlock__test_block_desc_0* Desc;
//内部参数只有局部变量
int temp;
int *temp2;
NSObject *__strong strObj;
__MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int *_temp2, NSObject *__strong _strObj, int flags=0) : temp(_temp), temp2(_temp2), strObj(_strObj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void _I_MBlock_test(MBlock * self, SEL _cmd) {
int temp = 1;
static int temp2 = 2;
__attribute__((objc_ownership(strong))) NSObject *strObj;
//
void (*Mblock)() = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp, &temp2, strObj, 570425344));
((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}
从上面的代码中,我们可以看出实际上在block实质上的对象Impl中只有局部变量的参数存在,全局变量不存在变量的截获。而且,静态局部变量保存的是一个指针,引用类型变量则保留了所有权修饰符。
__block
我们常常会在block内部修改变量的值,这个时候就需要添加__block修饰符。
举个?
@implementation MBlock
static int temp3 = 3;
int temp4 = 4;
- (void)test {
__block int temp = 1;
static int temp2 = 2;
void (^Mblock)() = ^() {
temp = 3;
temp2 = 3;
temp3 = 3;
temp4 = 3;
};
Mblock();
}
@end
可以看出只有局部变量在block内部修改的时候需要添加__block修饰。编译后结果:
struct __Block_byref_temp_0 {
void *__isa; //isa指针
__Block_byref_temp_0 *__forwarding; //内存相关指针
int __flags;
int __size; //占据空间大小
int temp; //实际上的值
};
static void _I_MBlock_test(MBlock * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_temp_0 temp = {(void*)0,(__Block_byref_temp_0 *)&temp, 0, sizeof(__Block_byref_temp_0), 1};
static int temp2 = 2;
void (*Mblock)() = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, &temp2, (__Block_byref_temp_0 *)&temp, 570425344));
((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}
可以看出编译后,局部变量temp变成了结构体对象__Block_byref_temp_0
。初始化的时候,把temp的地址传入。
static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {
__Block_byref_temp_0 *temp = __cself->temp; // bound by ref
int *temp2 = __cself->temp2; // bound by copy
(temp->__forwarding->temp) = 3;
(*temp2) = 3;
temp3 = 3;
temp4 = 3;
}
而最后调用的时候,是通过temp对象的forwarding指针获取对象,再去获取temp的值。至于为啥要通过forwarding指针去绕一圈的原因,下面说block内存相关内容的时候会进行介绍。
内存管理
block内存相关的问题,在前面的解说中遇到了一部分。现在我们详细解释一下。
前面我们介绍block的时候,Impl内部有一个参数isa,它指向的是_NSConcreteStackBlock
。很明显,这是一个栈block的意思,之前的?也是在栈中创建的block。那么还存在其他的类型么?
block的分类
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
NSGlobalBlock
全局block位于text段,并且在block内部没有引用任何外部变量。
void (^globalBlock) () = ^ () {
NSLog(@"global block");
};
NSLog(@"%@", globalBlock);
//<__NSGlobalBlock__: 0x1096e203a>
NSStackBlock
NSStackBlock位于栈内存,函数返回后block将无效,在block内部可以引用外部变量。在MRC下,NSStackBlock在函数的空间被收回后再调用会crash,所以使用的时候需要手动copy。而ARC下,当其赋值给strong对象时,系统会自动copy,所以在ARC下还是很难看到NSStackBlock的。
int temp=0;
NSLog(@"%@", ^{
NSLog(@"stack block here, temp =%d", temp);
});
//<__NSStackBlock__: 0x7fff592ebad3>
void (^block)()=^{
NSLog(@"stack block here, temp =%d", temp);
};
NSLog(@"%@",block);
//<__NSMallocBlock__: 0x7fae49e03262>
NSMallocBlock
堆内存的block,可以对其引用计数+1或者-1,copy的话不会产生新的对象。
copy
对上面的三个block进行copy操作的话,都会发生什么呢?经过尝试发现了如下的规则:
原 | 操作 | 现 |
---|---|---|
NSGlobalBlock | copy | NSGlobalBlock |
NSStackBlock | copy | NSMallocBlock |
NSMallocBlock | copy | 计数+1 |
并且实际上之前__block对象的forwarding指针指向随着copy操作而改变,copy前指向栈区的指针,在copy后会指向堆区。