iOS概念攻坚之路(七):block

前言

这篇文章主要围绕几个问题展开:git

  1. block 是什么
  2. block 的类型
  3. __block 是什么,以及它的 forwarding 指针的用处
  4. block 为何会形成循环引用
  5. block 的拷贝机制
  6. block 的运用

block 是什么

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 都会捕获变量呢?也不是,其实只要保证函数调用环境就能够,block 在 OC 中有三种类型:

  • 全局 block(_NSConcreteGlobalBlock),存在数据区(.data 区)
  • 堆 block(_NSConcreteStackBlock),存在堆区
  • 栈 block(_NSConcreteMallocBlock),存在栈区

在写全局变量的位置定义 block 的时候,生成的 block 类型是全局 block,由于在使用全局区域的地方不能使用自动变量,因此不存在对自动变量进行捕获。其实还有一种状况,就算 block 在日常定义全局变量的地方定义,使用的类型也是 _NSConcreteGlobalBlock 类型,那就是在没有捕获自动变量的时候。因此全局 block 有两种状况:

  • 在记述全局变量的地方用 block 时
  • block 没有截获自动变量时

除此以外的 block 语法生成的 block 就全是 _NSConcreteStackBlock 类型的了,也就是栈 block。还有一个堆 block 是怎么来的呢?其实不是咱们建立出来的,是系统根据状况帮咱们从栈上复制到堆上的。之因此要复制也是由于做用域的问题,设置在栈上的 block,若是其所属的变量做用域结束,该 block 也会被废弃,因此得拷贝到堆上,除了系统自动生成,咱们也能够手动调用 block 的 copy 方法将栈上的 block 拷贝到堆上。

简单列一下栈上的 block 复制到堆上的状况(ARC):

自动复制:

  • block 做为函数返回值时(自动生成复制到堆上的代码)
  • 将 block 赋值给附有 __strong 修饰符 id 类型的类或 block 类型的成员变量时
  • block 做为 Cocoa API 中方法名含有 usingBlock 的方法参数时
  • block 做为 GCD API 的方法参数时

手动复制:

  • 调用 copy 方法

在调用 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 时

可是若是在方法或函数中适当的复制了传递过来的参数,那么就没必要在调用该方法或函数前手动复制了。

要注意一个问题就是,将 block 从栈上复制到堆上是很消耗 CPU 的,因此当 block 设置在栈上就可以知足需求的话,将其复制到堆上是一种资源的浪费。

栈上的 block 调用 copy 会将 block 复制到堆中,那么堆 block 和全局 block 调用 copy 方法又会发生什么呢?列了一个表,以下:

Block 的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域(全局区) 什么也不作
_NSConcreteMallocBlock 引用计数增长

前面提到堆 block 将被释放会对所持有的对象进行一次 release 操做,来看一下堆 block 对一个自动变量的捕获过程:

  1. 调用 block 内部的 copy 函数
  2. copy 函数内部会调用 _Block_object_assign 函数
  3. _Block_object_assign 函数会根据自动变量的修饰符(__strong__weak__unsafe_unretained)作出相应操做,相似于 retain,造成强引用、弱应用

当 block 从堆上移除:

  1. 会调用 block 内部的 dispose 函数
  2. dispose 函数内部会调用 _Block_object_dispose 函数
  3. _Block_object_dispose 函数会自动释放引用的自动变量,相似于 release

其中主要涉及两个函数,copy 函数和 dispose 函数,当栈上的 block 复制到堆时调用 copy 函数,当堆上的 block 被废弃时调用 dispose 函数。

来个小结,block 分三种类型,全局 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 的循环引用

若是在 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
复制代码

为 UIButton 添加 block

最后说一个小例子,就是为按钮或者 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");
    };
}
复制代码

参考文章

iOS中Block的用法,示例,应用场景,与底层原理解析(这多是最详细的Block解析)

iOS开发 | 让你的UIButton自带block

相关文章
相关标签/搜索