这篇文章主要围绕几个问题展开:git
__block
是什么,以及它的 forwarding
指针的用处block 的定义:带有自动变量(局部变量) 的 匿名函数。github
主要是弄清楚「带有」、「自动变量」和「匿名函数」是什么,咱们就能知道 block 大概是个什么东西了。框架
自动变量 指的是局部做用域变量,具体来讲便是在控制流进入变量做用域时系统自动为其分配存储空间,并在离开做用域时释放空间的一类变量。在许多程序语言中,自动变量与术语局部变量所指的变量其实是同一种变量,因此一般状况下 “自动变量" 和 "局部变量" 是同义的。ide
主要意思是自动变量的生命周期由系统控制,当自动变量超过其做用域,会被系统自动释放。在 iOS 说自动变量,能够当作局部变量来理解。函数
而匿名函数,就是 没有名称的函数。C 语言的标准是不容许这样的函数存在的,由于调用函数必须知道函数名。固然也可使用函数指针来调用,不过在赋值给函数指针的时候,也是须要知道函数名,否则也没法得到该函数的地址。post
来看一个简单的 block:ui
^() {
printf("a simple block");
}
复制代码
这个函数就是没有名字的。spa
那什么是「带有」呢?指针
带有其实就是咱们常说的 捕获,那为何一个 block 要去捕获自动变量呢?其实 block 在 OC 中本质上也是一个 OC 对象,它有它的结构,在它结构内部也有 isa 指针,它是一个 封装了函数调用以及函数调用环境的 OC 对象。也就是说在你调用这个 block 的时候,它须要保证它的调用环境是可用的,而自动变量的生命周期是由系统控制的,当你调用 block 的时候,极可能其中使用到的自动变量已经被释放了,因此要把自动变量捕获进 block 结构体的内部,才能保证函数的调用环境。捕获的意思指 block 所使用的自动变量值被自动保存到 block 结构体实例中。rest
那么还会不会捕获其余变量?好比静态变量、全局变量、静态全局变量?其实不会,虽然这些变量的做用域不一样,可是在整个程序中,一个变量总保持在一个内存区域,所以,虽然屡次使用,可是无论在任什么时候候以任何状态调用,使用的都是相同的内存区域,同一个变量,因此并不须要捕获这些变量。
那是否是全部的 block 都会捕获变量呢?也不是,其实只要保证函数调用环境就能够,block 在 OC 中有三种类型:
_NSConcreteGlobalBlock
),存在数据区(.data
区)_NSConcreteStackBlock
),存在堆区_NSConcreteMallocBlock
),存在栈区在写全局变量的位置定义 block 的时候,生成的 block 类型是全局 block,由于在使用全局区域的地方不能使用自动变量,因此不存在对自动变量进行捕获。其实还有一种状况,就算 block 在日常定义全局变量的地方定义,使用的类型也是 _NSConcreteGlobalBlock
类型,那就是在没有捕获自动变量的时候。因此全局 block 有两种状况:
除此以外的 block 语法生成的 block 就全是 _NSConcreteStackBlock
类型的了,也就是栈 block。还有一个堆 block 是怎么来的呢?其实不是咱们建立出来的,是系统根据状况帮咱们从栈上复制到堆上的。之因此要复制也是由于做用域的问题,设置在栈上的 block,若是其所属的变量做用域结束,该 block 也会被废弃,因此得拷贝到堆上,除了系统自动生成,咱们也能够手动调用 block 的 copy 方法将栈上的 block 拷贝到堆上。
简单列一下栈上的 block 复制到堆上的状况(ARC):
自动复制:
__strong
修饰符 id 类型的类或 block 类型的成员变量时usingBlock
的方法参数时手动复制:
在调用 block 的 copy 实例方法时,若是 block 配置在栈上,那么该 block 会从栈复制到堆。block 做为函数返回值返回时,将 block 赋值给附有 __strong
修饰符 id 类型的类或 block 类型的成员变量时,编译器自动的将对象的 block 做为参数并调用 _Block_copy
函数,这与调用 block 的 copy 实例方法的效果相同。在方法名中含有 usingBlock
的 Cocoa 框架方法或 Grand Central Dispatch 的 API 中传递 block 时,在该方法或函数内部对传递过来的 block 调用 block 的 copy 实例方法或者 _Block_copy
函数。
栈 block 也是否是去持有外部对象的,只有堆 block 才会去持有外部对象,栈 block 不捕获是由于它的生命周期大于等于它所使用的自动变量的生命周期。堆 block 对去持有外部对象,也就是捕获自动变量,在堆 block 将被释放的时候,会对它所持有的对象进行一次 release
操做。
编译器大多数状况下都能判断出是否须要复制,可是有一种状况是判断不出来的,那就是:
可是若是在方法或函数中适当的复制了传递过来的参数,那么就没必要在调用该方法或函数前手动复制了。
要注意一个问题就是,将 block 从栈上复制到堆上是很消耗 CPU 的,因此当 block 设置在栈上就可以知足需求的话,将其复制到堆上是一种资源的浪费。
栈上的 block 调用 copy 会将 block 复制到堆中,那么堆 block 和全局 block 调用 copy 方法又会发生什么呢?列了一个表,以下:
Block 的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域(全局区) | 什么也不作 |
_NSConcreteMallocBlock | 堆 | 引用计数增长 |
前面提到堆 block 将被释放会对所持有的对象进行一次 release
操做,来看一下堆 block 对一个自动变量的捕获过程:
copy
函数copy
函数内部会调用 _Block_object_assign
函数_Block_object_assign
函数会根据自动变量的修饰符(__strong
、__weak
、__unsafe_unretained
)作出相应操做,相似于 retain
,造成强引用、弱应用当 block 从堆上移除:
dispose
函数dispose
函数内部会调用 _Block_object_dispose
函数_Block_object_dispose
函数会自动释放引用的自动变量,相似于 release其中主要涉及两个函数,copy
函数和 dispose
函数,当栈上的 block 复制到堆时调用 copy
函数,当堆上的 block 被废弃时调用 dispose
函数。
来个小结,block 分三种类型,全局 block、堆 block、栈 block,只有堆才会捕获变量,而且只捕获自动变量,也就是局部变量。
block 有一个现象,那就是没法修改外部变量,如:
int a = 1;
void (^blk)(void) = ^{
a = 2;
};
复制代码
上面代码会如下错误:
Variable is not assignable (missing __block type specifier)
复制代码
提示咱们须要使用 __block
对变量进行修饰,也就是在 int a
前使用 __block
来修饰。为何 block 不能修改外部对象?__block
后又能够修改外部对象了?
先来看看它为何不能修改外部的对象,前面提到 block 能够捕获自动变量,可是 block 只捕获自动变量的值,而并不捕获它的地址,至关于在 block 内部新建了一个属性,存储了所使用的对象的自动变量的 值,因此在 block 内部使用的自动变量已经不是以前的那个自动变量,即便你修改也影响不了以前的自动变量。基于这个缘由,苹果在编译器编译的过程检测到给截获的自动变量赋值操做时,就会产生一个编译错误。
那为何加上了 __block
做为修饰就能够了呢?实际上是由于系统帮咱们从新生成了一个新的对象,来看一段代码:
__block int val = 10;
void (^blk)(void) = ^{ val = 1; };
复制代码
该代码编译成 C++ 代码后以下:
int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__main_block_impl_0 (
__main_block_func_0, &__main_block_desc_0_DAT, &val, 0x22000000);
return 0;
}
复制代码
也就是以前 int
类型的 val 被转变成了 __Block_byref_val_0
类型的一种结构体,该结构体的声明以下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
}
复制代码
该结构体中最后的成员变量 val 就是以前的 int val
。
^{ val = 1; }
被转成了什么呢?以下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
}
复制代码
看一下它的查找过程 (val->__forwarding->val)
,block 的 __main_block_impl_0
结构体实例 __cself
指向 __block
变量的 __Block_byref_val_0
结构体的指针,__Block_byref_val_0
结构体实例的成员变量 __forwarding
持有指向该实例自身的指针,经过成员变量 __forwarding
访问成员变量 val。(成员变量 val 是该实例自身持有的变量,它至关于原自动变量。)查找过程以下图:
__block
变量的 __Block_byref_val_0
结构体并不在 block 内部的结构体中,这样作是为了在多个 block 中使用 __block
变量。
那还有一个问题,就是为何须要有一个 __forwarding
指针去指向本身?其实这是为了保证无论 __block
变量配置在栈上仍是堆上时都可以正确访问 __block
变量。怎么说呢?当 block 从栈上被复制到堆时,在栈区的 __block
变量也会复制一份到堆中,此时会将 __block
的成员变量 forwarding
的值替换为复制目标堆上的 __block
变量用结构体实例的地址。以下图:
那么这样,不管是 block 语法中、block 语法外使用 __block
变量,仍是 __block
变量配置在栈上仍是堆上,均可以顺利的访问同一个 __block
变量。
若是在 block 中使用附有 __strong
修饰符的对象类型自动变量,那么当 block 从栈复制到堆时,该对象为 block 持有,这样容易引发循环引用。
好比:
self
持有 block,block 持有 self
,这就造成了循环引用。解决的方式有三种:
__weak
__unsafe_unretained
__block
(1)__weak
的方式:
- (id)init
{
self = [super init];
id __weak tmp = self;
blk_ = ^{ NSLog(@"self = %@", tmp); };
return self;
}
复制代码
(2)__unsafe_unretained
的方式:
- (id)init
{
self = [super init];
id __unsafe_unretained tmp = self;
blk = ^{ NSLog(@"self = %@", tmp); };
return self;
}
复制代码
__unsafe_unretained
与 __weak
的区别在于 __unsafe_unretained
所指向的对象被回收以后,__unsafe_unretained
指针并不会自动置为 nil,此时 __unsafe_unretained
指针就是悬垂指针,对悬垂指针进行操做可能会引起崩溃。
(3)那么还有一种 __block
来破解循环引用的方式:
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t blk_;
}
@implementation MyObject
- (instancetype)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk_();
}
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
复制代码
这种方式有一个问题,就是必需要执行 block 才能解除引用链条,由于 tmp = nil
是写在 block 内部的。反正就目前来讲,99% 状况下使用的都是 __weak
。还有一种状况是先 __weak
,而后在 block 内部再使用一个 __strong
指针去强引用它,以下:
__weak typeof(self) weakSelf = self;
blk_ = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@",strongSelf);
};
复制代码
这样是为了保证 block 内部的代码可以执行完,由于在执行 NSLog
以前,self 有可能会被释放,因此对其进行一个强引用。若是出现双层循环或多层循环,要再对 strongSelf
进行 __weak
而后再 __strong
。。。这样一层层嵌套。
放一下我项目中用到的 @weakify
和 @strongify
宏定义:
// 弱引用
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif
// 强引用
#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
复制代码
最后说一个小例子,就是为按钮或者 UIView
类型的控件添加 block,你们能够按照这篇 文章 来实现这个小轮子。我从新再封装了一下,放到了 github 之中,使用大概是这样子的:
#import "UIButton+FRButtonEventBlock.h"
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.aButton];
self.aButton.fr_touchUpInside = ^{
NSLog(@"touchUpInside");
};
self.aButton.fr_touchUpOutside = ^{
NSLog(@"touchUpOutside");
};
self.aButton.fr_touchDown = ^{
NSLog(@"touchDown");
};
self.aButton.fr_touchCancel = ^{
NSLog(@"touchCancel");
};
}
复制代码