概述
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;