概述

Block技术是iOS开发中很常用的功能,也是很多开发者喜欢偏好使用的技术之一,事实上Block也确实带给很多程序员极大的便利,但是Block的使用中也有很多隐藏的坑,一不小心就会造成循环引用或者内存泄漏,所以,熟练的使用Block和规范的使用Block也是每个程序员必须掌握的技巧,在这里我们就来好好的分析和讲解Block。

Block的简单使用

我问过我的同事,他们可能使用Block最频繁的地方是在回调的时候,也就是传值的时候,非常的方便,代码看起来也很直观,Block的创建我们如果用typedef别名的话一般是这样的:

typedef <#returnType#>(^<#name#>)(<#arguments#>)

这就引出了Block的结构:

returnType 这是指Block的返回类型,如果没有返回值我们这里就可以填上Void;

name 顾名思义这个就是Block的名称;

arguments 从表面意思上来看这个单词是参数的意思,在这里也就是Block参数和参数类型;

看完结构,我们先来完整的使用一下简单的Block

void (^testBlock)(void) = ^(){

NSLog(@"调用了Block");

};

testBlock();

打印

function:-[TransValueNext viewDidLoad]_block_invoke line:24 content:调用了Block

这里我们写了一个简单的Block,返回值类型为Void,名称为testBlock,参数为Void,Block里直接打印了'调用了block',这样我们就很清晰直观的看到了一个完整的简单的Block功能。

底层实现

1. 什么是Block

要说起一个Block的底层代码,我们就不得不用Clang将oc代码转成c++代码来分析,这里我们将一个Block代码

int multiplier = 6;

int(^Block)(int) = ^int(int num){

return num *multiplier;

};

Block(2);

使用下面的命令

clang -rewrite-objc file.m

将代码转成C++代码之后,我们可以发现

Block底层

这个Block转化成了一个方法这个方法中含有一个返回值为int的Block对象,包括了__MCBlock__method_block_impl_0这个结构体,结构体里包括了__MCBlock__method_block_func_0这个函数指针,__MCBlock__method_block_desc_0_DATA这个关于Block的相关描述和multiplier这个外部参数三个组成部分。深挖__MCBlock__method_block_impl_0进去我们不难发现

__MCBlock__method_block_impl_0

Block实际上就是一个对象,那么这个对象中封装了函数以及函数的执行上下文。

2.什么是Block调用

Block调用实际上就是函数的调用,我们再次来看上述的CPP代码中的调用

Block调用

其实就是跟普通的函数调用方法一样是去通过函数指针调用Block的实现

3. Block截获变量

int multiplier = 6;

int(^Block)(int) = ^int(int num ){

return num * multiplier ;

};

multiplier = 4;

NSLog("result is%d" ,Block(2) ) ;

来看这道经典的block面试题,它的结果是12还是8?(答案为12)

static int multiplier = 6;

int(^Block)(int) = ^int(int num ){

return num * multiplier ;

};

multiplier = 4;

NSLog("result is%d" ,Block(2) ) ;

而这个答案为8。我们总结一下是因为:

对于基本数据类型的局部变量截获其值

对于对象类型的局部变量连同所有权修饰符一起截获

以指针形式截获静态变量

不截获全局变量和静态全局变量

4. __block修饰符

一般情况下,对被截获变量进行赋值操作需要添加__block修饰符,但是需要记住的是赋值并不等于使用。

有一个笔试面试题:

NSMutableArray *array = [NSMutableArray array];

void (^Block)(void) = ^{

[array addObject:@"123"];

};

Block();

问:此时array需不需要加上__block修饰符呢?答案是不需要。

那么有人就会问了,为什么不要呢,上述问题我们可以看成是对array的使用,而不是重新赋值,所以我们并不需要为array添加上__block修饰符。相反,这种情况我们就需要添加__block了:

__block NSMutableArray *array = nil;

void (^Block)(void) = ^{

array = [NSMutableArray array];

};

这里就是给array重新初始化赋值操作,所以必然我们就需要加上修饰符来修饰array。

两幅图:

什么时候需要修饰

什么时候不需要修饰

5.Block内存管理

通常Block分为三种:

全局区的Block:NSGlobalBlock;

栈区的Block:NSStackBlock;

堆区的Block:NSMallocBlock;

三种Block存放区域

1、NSGlobalBlock:

当我们声明一个block时,如果这个block没有捕获外部的变量,那么这个block就位于全局区,此时对NSGlobalBlock的retain、copy、release操作都无效。ARC和MRC环境下都是如此。

如图所示,声明并且定义一个全局区的block

2、NSStackBlock: 这里可能有人会问,平时编程的时候很少遇到位于栈区的block,为什么呢?因为在ARC环境下,当我们声明并且定义了一个block,并且没有为Block添加额外的修饰符(默认是__strong修饰符),如果该Block捕获了外部的变量,实质上是有一个从NSStackBlock转变到NSMallocBlock的过程,只不过是系统帮我们完成了copy操作,将栈区的block迁移到堆区,延长了Block的生命周期。对于栈区block而言,栈block在当函数退出的时候,该空间就会被回收。 那什么时候在ARC的环境下出现NSStackBlock呢?如果我们在声明一个block的时候,使用了__weak或者__unsafe__unretained的修饰符,那么系统就不会为我们做copy的操作,不会将其迁移到堆区。下面我们实验一下:

如上图所示,被__weak修饰的myBlock1捕获了外部变量n,成为一个栈区的block

默认修饰符环境下,捕获了外部变量的block位于堆区

我们可以手动地去执行copy方法,验证系统为我们做的隐式转换:

如图所示,手动执行copy方法之后,block被迁移到了堆区

3、NSMallocBlock:在MRC环境下,我们需要手动调用copy方法才可以将block迁移到堆区,而在ARC环境下,__strong修饰的(默认)block只要捕获了外部变量就会位于堆区,NSMallocBlock支持retain、release,会对其引用计数+1或 -1。声明以及定义位于堆区的block如上图所示。

Block copy操作之后

Block copy操作

5.Block的循环引用

其实在实际开发中Block的循环引用是最致命的,因为会引起内存泄漏,这个也是程序员不可忽视的一个细节之一,关于循环引用,我们可以理解为对象强引用Block,而Block又持有这个对象,这样就会产生循环引用

Block循环引用

那如何破除循环引用呢,我们必须给Block中的持有对象的属性进行一个弱引用,则给对象的属性添加一个__weak修饰符。

举个例子

typedef void (^block)(id obj);

@property (nonatomic, copy) block blk;

- (void)viewDidLoad {

[super viewDidLoad];

self.array = [NSMutableArray array];

self.blk = ^(id obj){

[self.array addObject:obj];

NSLog(@"array count = %ld",[self.array count]);

};

}

- (void)viewDidLoad {

[super viewDidLoad];

self.array = [NSMutableArray array];

ViewController * __weak temp = self;

self.blk = ^(id obj){

[temp.array addObject:obj];

NSLog(@"array count = %ld",[temp.array count]);

};

}

总结

总结就不说什么了,来几个题目吧

1、UIView 的 animation动画块使用了Block,内部使用self不会循环引用,为什么呢

答:UIView 动画块是类方法,不被self持有,所以不会循环引用。

2、Monsary也使用了Block来设置控件的布局,Block内部使用self,为什么不会循环引用呢

答:看源码可以看出,Monsary使用的Block是当做参数传递的,即便block内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用。

3、reactiveCocoa如果不使用@weakify @strongify,会循环引用,两个宏就等于下边代码:

__weak typeof(self) weakSelf = self;

__strong typeof(weakSelf) strongSelf = weakSelf;