一 前言:
Swift 中的 Closure 和 Objective-C 中的Block 都是非常常用 的语法。本文从定义 和 使用两方面,对外部变量的捕获等 各方面对比二者的异同和优劣。
二 对比:
代码下载地址: https://github.com/LoveHouLinLi/CompareBlockClosure 这里面有两个工程 一个是Objective-C 写的 另一个是 Swift 写的 。 用于区分二者的不同。 我们分别打开两个工程。
1.0 定义 开始
OC 中: 下面这是一个 Block的 简单定义 返回值 (^ Block名称)(参数) ,参数可以是多个
但是 Block 类型的并不能作为 返回值 也不可以 作为函数的返回值 。 在swift 中Closure 是可以的作为另一个闭包 也可以作为函数的返回值 。
void (^removeBlock)(void) = ^{
[array removeObjectAtIndex:3];
};
但是实际上 我们使用block 都是 使用宏定义的方式。 这样很方便我们使用 不用到处写上复杂的表达式了。
typedef void(^Block)(int x);
很多 朋友 好奇为啥 OC 中Block 要用copy 修饰。 其实 Block 用Copy修饰 源自于 MRC 时期;
在 MRC 时代请看下面这段代码: 会引起 crash 因为 MRC 中Block 默认是在栈上面的。
void (^block)(void);
- (void)testStackBlock
{
int i=arc4random()%2;
if (i==0) {
block=^{
NSLog(@" Block A i is %d",i);
};
}else{
block=^{
NSLog(@"Block B i is %d",i);
};
}
block();
}
如果想让 上面那段代码不 Crash 就要这样写,每次赋予新值的时候这样写。 这样Block 从栈 copy至堆上面了。
block=[^{
NSLog(@" Block A i is %d",i);
} copy];
但是 在ARC 时代 默认Block 就是在堆上面 ,所以不需要copy 但是大家习惯上这样修饰了。
Swift 中:
Closure 是这样定义的:(参数) -> 返回值 这种语法 运算的表达式
下面的每段都是可以的
let calAdd:(Int,Int) ->(Int) ={ (a:Int,b:Int) -> Int in return a + b }
let calAdd4:(Int,Int) ->(Int) = {
(a,b) in return a+b;
}
let calAdd3:(Int,Int) ->(Int) = {
(a,b) in a+b;
}
let calAdd2:(Int,Int) ->(Int) = {
a,b in return a+b
}
let calAdd6:(Int,Int) ->(Int) = {
a,b in a+b
}
如果闭包没有参数,可以直接省略“in”
let calAdd5:() ->Int = {
return 100+150;
}
同样 Closure 也可以使用 宏定义 也能方便我们使用
typealias AddClosure = (Int,Int) ->(Int)
下面是 Closure 作为函数的返回值 的情形:
func captureValue2(sums amount:Int) -> ()->Int {
var total = 0
let AddBlock:() ->Int = {
total += amount
return total
}
return AddBlock
}
若将闭包作为函数最后一个参数,可以省略参数标签,然后将闭包表达式写在函数调用括号后面
func trailingClosure(testBlock:()->Void) {
testBlock() // 调用block
}
trailingClosure(testBlock: {
print("正常写法 没省略()") // 和 OC 中的Block 写法类似
})
2.0 从捕获外部的变量角度分析
不论是 Block 和 Swift 都可以捕获外部的变量。
OC 中的 Block 会从 局部非指针变量 ,局部指针变量 ,全局变量 ,static 变量 四个方面来对比。
- (void)viewDidLoad {
[super viewDidLoad];
// 这种在 Swift 里面叫做自动闭包
array = @[@"I",@"have",@"a",@"apple"].mutableCopy;
[self testBlockCaptureStaticValue];
[self testBlockCaptureGlobalNormalBasicValue];
[self testBlockCapturePartNormalBasicValue];
[self testBlockChangeCapturedNormalTypeWithPointer];
}
DEMO 中分别对比了这几种类型 。 注意指针类型局部变量 为啥使用指针能改变 不适用指针没改变 。在OC block 中有循环应用的情况。
在 swift 中 捕获的变量 和 OC 中有很大不同。
首先是 逃逸闭包
当一个闭包作为参数传到一个函数中,需要这个闭包在函数返回之后才被执行,我们就称该闭包从函数种逃逸。一般如果闭包在函数体内涉及到异步操作,但函数却是很快就会执行完毕并返回的,闭包必须要逃逸掉,以便异步操作的回调。
逃逸闭包一般用于异步函数的回调,比如网络请求成功的回调和失败的回调。语法:在函数的闭包行参前加关键字“@escaping”。
Block 中默认都是逃逸的
func doSomethingDelayWithNoneEscaping(some:()->Void) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {
some()
}
// IDE 编译优化 这种显式的我们能避免 我们在 TestViewController
some()
print("do Something Function Body")
}
这种会有类似的提醒 编译器 提醒加 escaping IDE 提醒如下:
Closure use of non-escaping parameter 'some' may allow it to escape
将一个闭包标记为@escaping意味着你必须在闭包中显式的引用self。 其实@escaping和self都是在提醒你,这是一个逃逸闭包, 别误操作导致了循环引用!而非逃逸包可以隐式引用self。 在Block 中为了避免循环引用我们在使用完Block后 置block 为nil 这样 避免。 非逃逸闭包默认帮我们做了这一步。在返回时 把Closure 设置为空。 在 TestViewController 中做了对比。
注意 局部非指针变量时 和 Closure的 区别!!! Block 中没改变 Closure 中改变了
- (void)testBlockCapturePartNormalBasicValue
{
int num = 100;
int (^TestBlock)(int) = ^(int x){
return num+x;
};
NSLog(@"使用局部变量的结果:%d",TestBlock(100));
num=50;//change the value of number.
NSLog(@"修改局部变量的值再次调用block的结果:%d",TestBlock(100));
// 200 200
}
但是在 Closure 中 局部变量发生了改变。
var num:Int = 100
// static var b:Int = 100
func testClosureCaptureStaticValue() {
var number:Int = 100
let closure:(Int) ->(Int) = {
a in a+number
}
print("局部变量改变number值\(closure(100))")
number = 50
print("局部变量改变number值\(closure(100))")
let closure2:(Int) ->(Int) = {
a in a+self.num
}
print("全局变量改变number值\(closure2(100))")
num = 50
print("全局变量改变number值\(closure2(100))")
}
在Swift中,这叫做截获变量的引用。闭包默认会截取变量的引用,也就是说所有变量默认情况下都是加了__block修饰符的 这点和 Block 不同。
3.0 循环引用
OC 中的循环引用 请参考 我http://blog.csdn.net/delongyangforcsdn/article/details/74529926 和 http://www.jianshu.com/writer#/notebooks/20125367/notes/20921257 的内容 这里就 不多说了。
但是在 swift 中 原理 是类似的 但是 因为 Swift 中可选类型的存在 导致 情形多出了些。请看代码 Swift 工程中的TestViewController 。
先来看第一种形式的 循环引用。
这个 block 是一个全局的 block 没有说是escaping 还是 非escaping 这点 和block 中的类似 。
var block:(()->())?
func testRetainCycleInClosureThree() {
let a = Person()
// 全局 的 变量
block = {
print(self.view.frame)
}
a.name = "New Name"
block!()
}
现在 看第二种形式的 循环引用 这种形式 。关键是 person = Person() 是一个全局变量 。 controller 虽然不直接持有 closure 但是 person的 block 持有了Closure 而Controller 持有了person 。 而 Closure 又持有Controller。
func testRetainCycleInClosureFour() {
person = Person()
let block = {
self.x = 20;
print(self.x)
}
person?.block = block
// person = nil // 如果person 不设置成 nil 会有循环引用
block()
}
在 OC 中如果这样写 也会造成 循环引用
- (void)testCycleFour
{
self.person = [[Person alloc] initWithName:@"name"];
void (^block)(void) = ^(){
NSLog(@"rect is %@",NSStringFromCGRect(self.view.frame));
};
self.person.block2 = block;
}
我们先创建了可选类型的变量a,然后创建一个闭包变量,并把它赋值给a的block属性。这个闭包内部又会截获a,那这样是否会导致循环引用呢? 答案是否定的。虽然从表面上看,对象的闭包属性截获了对象本身。但是如果你运行上面这段代码,你会发现对象的deinit方法确实被调用了,打印结果不是“A”而是“nil”。
这是因为我们忽略了可选类型这个因素。这里的a不是A类型的对象,而是一个可选类型变量,其内部封装了A的实例对象。闭包截获的是可选类型变量a,当你执行a = nil时,并不是释放了变量a,而是释放了a中包含的A类型实例对象。所以A的deinit方法会执行,当你调用block时,由于使用了可选链,就会得到nil,如果使用强制解封,程序就会崩溃。
注意 !!这里循环的原因 是 person 是全局的 和 是否调用没有关系
参考文档