最近在看Swift闭包截获变量时遇到了各类问题,总结以后发现主要是还用停留在OC时代的思惟来思考Swift问题致使的。借此机会首先复习一下OC中关于block的细节,同时整理Swift中闭包的相关的问题。不论是目前使用OC仍是Swift,又或者是从OC转向Swift,均可以阅读这篇文章并与我交流。html
#OC的blockios
OC的block已经有不少相关的文章介绍了,主要难点在于__block
修饰符的做用和原理,以及循环引用问题。咱们首先由浅入深举几个例子看一看__block
修饰符,最后分析循环引用问题。这里的讨论都是基于ARC的。面试
int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();
// 打印结果是:"value = 10"
复制代码
OC的block会截获外部变量,对于int
等基本数据类型,block的内部会拷贝一份,简单来讲,它的实现大概是这样的:swift
struct block_impl {
//其它内容
int value;
};
复制代码
由于block内部拷贝了截获的变量的副本,因此生成block后再修改变量,不会影响被block截获的变量。同时block内部也不能修改这个变量。闭包
若是要想在block中修改被截获的基本类型变量,咱们须要把它标记为__block
:app
__block int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();
// 打印结果是:"value = 20"
复制代码
这是由于,对于被标记了__block
的变量,block在截获它时,会保存一个指针。简单来讲,它的实现大概是这样的:ide
struct block_impl {
//其它内容
block_ref_value *value;
};
struct block_ref_value {
int value; // 这里保存的才是被截获的value的值。
};
复制代码
因为block中一直有一个指针指向value,因此block内部对它的修改,能够影响到block外部的变量。由于block修改的就是那个外部变量而不是外部变量的副本。优化
上面关于block具体实现的例子只是一个简化模型,事实上并不是如此,但本质相似。总的来讲,只有由__block
修饰符修饰的变量,在被block截获时才是可变的。关于这方面的详细解释,能够参考这三篇文章:ui
__block
的实现原理__block
原理的文章,内容会详细一些。block截获指针和截获基本类型是类似的,不过稍稍复杂一些。先看一个最简单的例子。spa
Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};
p.name = @"new name";
block();
// 打印结果是:"person name = new name"
复制代码
在截获基本类型时,block内部可能会有int capturedValue = value;
这样的代码,类比到指针也是同样的,block内部也会有这样的代码:Person *capturedP = p;
。在ARC下,这实际上是强引用(retain)了block外部的p
。
因为block内部的p
和外部的p
指向的是同一块内存地址。因此在block外部修改p
的属性,依然会影响到block内部截获的p
。
须要强调一点,这里的p
依然不是可变的。修改p
的name
不是改变p
,只是改变p
内部的属性:
Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
p.name = @"new name"; //OK,没有改变p
p = [[Person alloc] initWithName:@"new name"]; //编译错误
NSLog(@"person name = %@", p.name);
};
block();
复制代码
类比__block
修饰符对基本类型的做用原理,由它修饰的指针,在被block截获时,截获的实际上是这个指针的指针。好比咱们把刚刚的例子修改一下:
__block Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};
p = nil;
block();
// 打印结果是:"person name = (null)"
复制代码
此时,block内部有一个指向外部的p
的指针,一旦p
被设为nil
,这个内部的指针就指向了nil
。因此打印结果就是null
了。
还记得之前有一次面试时被问到,__block
会不会retain
变量?答案是:会的。从原理上分析,__block
修饰的变量被封装在结构体中,block内部持有对这个结构体的强引用。这一点不论是对于基本类型仍是指针都是通用的。从实际例子上来讲:
Block block;
if (true) {
__block Person *p = [[Person alloc] initWithName:@"zxy"];
block = ^{
NSLog(@"person name = %@", p.name);
};
}
block();
// 打印结果是:"person name = zxy"
复制代码
若是没有retain
被标记为__block
的指针p
,那么超出做用于后应该会获得nil
。
无论对象是否标记为__block
,一旦block截获了它,就会强引用它。因此,判断是否发生循环引用,只要判断block截获的对象,是否也持有block便可。若是这个对象确实须要直接或间接持有block,那么咱们须要避免block强引用这个对象。解决办法是使用__weak
修饰符。
// block是self的一个属性
id __weak weakSelf = self;
block = ^{
//使用weakSelf代替self
};
复制代码
block不会强引用被标记为__weak
的对象,只会对其产生弱引用。为了防止在block内的操做会释放wself
,能够先强引用它。这种作法有一个很漂亮的名字叫weak-strong dacne
,具体实现方法能够参考RAC的@strongify
和@weakify
。
简单来讲,除非标记为__weak
,block老是会强引用任何捕获的对象。而__block
表示捕获的就是指针自己,而非另外一个指向这个对象的指针。也就是说,被__block
修饰的对象在block内、外的改动会互相影响。
若是想避免循环引用问题,首先要肯定block引用了哪些对象,而后判断这些对象是否直接或间接持有block,若是有的话把这些对象标记为__weak
避免block强引用它。
OC中的__block
是一个很讨厌的修饰符。它不只不容易理解,并且在ARC和非ARC的表现大相径庭。__block
修饰符本质上是经过截获变量的指针来达到在闭包内修改被截获的变量的目的。
在Swift中,这叫作截获变量的引用。闭包默认会截取变量的引用,也就是说全部变量默认状况下都是加了__block
修饰符的。
var x = 42
let f = {
// [x] in //若是取消注释,结果是42
print(x)
}
x = 43
f() // 结果是43
复制代码
若是若是被截获的变量是引用,和OC同样,那么在闭包内部有一个引用的引用:
var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
print(a?.name)
}
a = A(name: "new name")
}
block2?() //结果是:"Optional("new name")"
复制代码
若是把变量写在截获列表中,那么block内部会有一个指向对象的强引用,这和在OC中什么都不写的效果是同样的:
var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
[a] in
print(a?.name)
}
a = A(name: "new name")
}
block2?() //结果是:"Optional("old name")"
复制代码
Swift会自动持有被截获的变量的引用,这样就能够在block内部直接修改变量。不过在一些特殊状况下,Swift会作一些优化。经过以前OC中对__block
的分析能够看到,持有变量的引用确定比直接持有变量开销更大。因此Swift会自动判断你是否在闭包中或闭包外改变了变量。若是没有改变,闭包会直接持有变量,即便你没有显式的把它卸载捕获列表中。下面这句话截取自Swift官方文档:
As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by or outside a closure.
不论是否显示的把变量写进捕获列表,闭包都会对对象有强引用。若是闭包是某个对象的属性,并且闭包中截获了对象自己,或对象的某个属性,就会致使循环引用。这和OC中是彻底同样的。解决方法是在捕获列表中把被截获的变量标记为weak
或unowned
。
关于Swift的循环引用,有一个须要注意的例子:
class A {
var name: String = "A"
var block: (() -> ())?
//其余方法
}
var a: A? = A()
var block = {
print(a?.name)
}
a?.block = block
a = nil
block()
复制代码
咱们先建立了可选类型的变量a
,而后建立一个闭包变量,并把它赋值给a
的block
属性。这个闭包内部又会截获a
,那这样是否会致使循环引用呢?
答案是否认的。虽然从表面上看,对象的闭包属性截获了对象自己。可是若是你运行上面这段代码,你会发现对象的deinit
方法确实被调用了,打印结果不是“A”而是“nil”。
这是由于咱们忽略了可选类型这个因素。这里的a
不是A类型的对象,而是一个可选类型变量,其内部封装了A的实例对象。闭包截获的是可选类型变量a
,当你执行a = nil
时,并非释放了变量a
,而是释放了a
中包含的A类型实例对象。因此A的deinit
方法会执行,当你调用block时,因为使用了可选链,就会获得nil
,若是使用强制解封,程序就会崩溃。
若是想要人为形成循环引用,代码要这样写:
var block: (() -> ())?
if true {
var a = A()
block = {
print(a.name)
}
a.name = "New Name"
}
block!()
复制代码
为了不weak
变量在闭包中提早被释放,咱们须要在block一开始强引用它。这在OC部分已经讲过如何使用了。Swift中实现Weak-Strong Dance通常有三种方法。分别是最简单的if let
可选绑定、标准库的withExtendedLifetime
方法和自定义的withExtendedLifetime
方法。
__block
修饰符,可是多了截获列表。经过把截获的变量标记为weak
避免引用循环