写在前面:最近在学习上有一些烦躁,莫名的,学海无涯苦作舟吧!希望再接再厉的坚持下去,学习,创业!!!
闭包捕获的是变量的引用而不是当前变量的拷贝
注意,这里的变量包含了值类型和引用类型。如果是引用类型,则是捕获了对象的引用,即在闭包中复制了一份对象的引用,对象的引用计数加1;如果是值类型呢,捕获的是值类型的指针,如果在闭包中修改值类型的话,同样会改变外界变量的值。
func delay(_ duration: Int, closure: @escaping () -> Void) {
let times = DispatchTime.now() + .seconds(duration)
DispatchQueue.main.asyncAfter(deadline: times) {
print("开始执行闭包")
closure()
}
}
func captureValues() {
var number = 1
delay(1) {
print(number)
}
number = 2
}
captureValues()
复制代码
如果按照以前的思路,很可能会的得出结论:输出1,为什么?因为闭包直接捕获的值本身的拷贝,但是在Swift不是这样的,Swift捕获的是变量的引用,而非变量的值的拷贝,所以这里闭包捕捉了number
变量的引用,当闭包执行时,指针指向的值类型number
的值已经为2了,所以这里的输出为
开始执行闭包
2
复制代码
在闭包中改变变量的值
在外面改变变量的值之后,闭包执行是捕获到的变量的值会随之发生改变,当然了,如果在闭包内部改变变量的值的话,外界的变量值会发生改变吗?答案当然是yes。在闭包中修改变量的值也是通过指针改变变量实际的值,所以肯定会发生改变啦~
func changeValues() {
var number = 1
delay(1) {
print(number)
number += 1
}
delay(2) {
print(number)
}
}
复制代码
输出的值为:
开始执行闭包
1
开始执行闭包
2
复制代码
闭包如何捕获变量的值,而不是引用呢?
那么我们有时候肯定会有个需求那就是只想捕捉当前变量的值,不希望在闭包执行前,其他地方对变量值的修改会影响到闭包所捕获的值。为了实现这个,Swift提供了捕获列表
,可以实现捕获变量的拷贝,而不是变量的指针!
func captureStatics() {
var number = 1
// 这里在编译的时候,count直接copy了变量的值从而达到了目的
delay(1) { [count = number] in
print("count = \(count)")
print("number = \(number)")
}
number += 10
}
复制代码
输出如下:
开始执行闭包
count = 1
number = 11
复制代码
闭包的两个关键字
聊到闭包,就不得不提到闭包的两个关键字@escaping
和@autoclosure
它们分别代表了逃逸闭包和自动闭包
@escaping
- 什么是逃逸闭包呢?当一个闭包作为参数传到一个函数中,而这个闭包在函数返回之后才被执行,这个闭包就被称为逃逸闭包
- 如果闭包在函数体内部做异步操作,一般函数会很快执行完毕并且返回,但是闭包却必须逃逸,这样才可以处理异步回调
- 在网络请求中,逃逸闭包被大量使用,用来处理网络的回调
func delay(_ duration: Int, closure: @escaping () -> Void) {
let times = DispatchTime.now() + .seconds(duration)
DispatchQueue.main.asyncAfter(deadline: times) {
print("开始执行闭包")
closure()
}
print("方法执行完毕")
}
复制代码
这个方法就是一个典型的例子,作为参数传递进来的闭包是会延时执行的,所以函数先有返回值,再有闭包执行,所以闭包参数需要添加上@escaping
关键字
方法执行完毕
开始执行闭包
复制代码
@autoclosure
其实自动闭包,大多是听得多,用得少,它的作用是简化参数传递,并且延迟执行时间。 我们来写一个简单的方法
func autoTest(_ closure: () -> Bool) {
if closure() {
} else {
}
}
复制代码
这是一个以闭包做为参数,而且闭包并不会在函数返回之后才执行,而是在方法体中作为了一个条件而执行,那么我们如何调用这个方法呢?
autoTest { () -> Bool in
return "n" == "b"
}
复制代码
当然,由于闭包会默认将最后一个表达式作为返回值,所以可以简化为:
autoTest { "n" == "b" }
复制代码
那么还可以更简洁吗?答案是可以的,在闭包中使用@autoclosure
关键字
func autoTest(_ closure: @autoclosure () -> Bool) {
if closure() {
} else {
}
}
复制代码
autoTest("n" == "b")
复制代码
没错,连大括号都省略了,直接添加一个表达式即可,这个时候肯定有人有疑问,那我直接使用表达式不行吗,为什么还要使用@autoclosure
闭包呢?
理论上其实是可行的,但是如果直接使用表达式的话,在调方法的时候,这个表达式就会进行计算,然后将值作为参数传入方法中;如果是@autoclosure
闭包,只会在需要执行它的时候才会去执行,而并不会在一开始去就计算出结果,和懒加载有些类似~
- @autoclosure 和普通表达式最大的区别就是,普通表达式在传入参数的时候,会马上被执行,然后将执行的结果作为参数传递给函数
- 使用@autoclosure 标记的参数,虽然我们传入的也是一个表达式,但这个表达式不会马上被执行,而是要由调用的函数内来决定它具体执行的时间
闭包的循环引用
闭包的循环引用的原理:object -> 闭包 -> object 形成环形引用,从而无法释放彼此,形成了循环引用!那么问题来了:
UIView.animate(withDuration: TimeInterval) {
}
DispatchQueue.main.async {
}
复制代码
在以上两个闭包中使用self
调用方法,会造成循环引用吗?
还用想吗?当然不会啦,首先self
要持有闭包,才有可能循环引用,但是self
不持有闭包,闭包虽然会强引用 self
, 却没有形成引用的闭环,所以并不会造成循环引用!关于这里在后面会详细描述到,现在来看看闭包中的两个关键字,Weak
和 Owned
Apple建议如果可以确定self在访问时不会被释放的话,使用unowned
,如果self存在被释放的可能性就使用weak
[weak self]
我们来看一个简单的例子
class Person {
var name: String
lazy var printName: () -> () = {
print("\(self.name)")
}
init(name: String) {
self.name = name
}
deinit {
print("\(name) 被销毁")
}
}
func test() {
let person = Person.init(name: "小明")
person.printName()
}
text()
复制代码
输出结果为:
小明
复制代码
为什么? 只要是稍微了解一点循环引用的人都知道,发生这种情况的主要原因是self
持有了closure
,而closure
有持有了self
,所以就造成了循环引用,从而小明对象没有被释放。 所以在这个时候可以选择使用weak,这样Person
对象是可以被正常释放的,只不过,如果是异步操作的话,当Person对象被释放之后,再执行闭包中语句的时候,是不会执行的,因为self已经是nil了
class Person {
var name: String
lazy var printName: () -> Void = { [weak self] in
print("\(self?.name)")
}
init(name: String) {
self.name = name
}
deinit {
print("\(name) 被销毁")
}
func delay(_ duration: Int, closure: @escaping () -> Void) {
let times = DispatchTime.now() + .seconds(duration)
DispatchQueue.main.asyncAfter(deadline: times) {
print("开始执行闭包")
closure()
}
}
}
let person = Person.init(name: "小明")
person.delay(2, closure: person.printName)
复制代码
结果如下:
小明 被销毁
开始执行闭包
nil
复制代码
这即是使用weak的好处,也是坏处,确实可以避免循坏引用的发生,但是却无法保证闭包中的语句全部执行,所以就可以考虑到OC中的strongSelf的方式,使用strongSelf就是让闭包中的语句要么全部执行,要么全部不执行:
lazy var printName: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
print(strongSelf.name)
}
复制代码
这也是我们在实际的应用中使用最多的一种方式,要么都执行,要么都不执行; 那么有没有一种方法是,既可以避免循环引用,又要保证代码的完整执行呢?答案是有的,在唐巧的一篇博客中提到过,要使得一个block避免循环引用有两种方式:
- 事前预防,即使用weak,unowne
- 事后补救,即在传入block后,自己手动的去断开block的连接
lazy var printName: () -> Void = {
print(self.name)
self.printName = {}
}
复制代码
输出结果如下:
-------开始执行闭包--------
小明
-------结束执行闭包---------
小明对象被销毁
复制代码
其实相当于我在执行完毕之后,主动断开闭包对self的持有!!通过这种方式的好处就是,我不会造成循环引用,也可以保证闭包中的代码段执行完全,不过这种做法是有风险的,那就是如果忘记了主动断开的话,依旧是会造成循环引用的。
[unowned self]
这种其实非常好理解,就是如果self的生命周期和闭包的生命周期一致,或者比闭包的生命周期还长的话,那就使用unowned
关键字。在实际的使用中,还是遵循Apple的推荐:
如果可以确定self在访问时不会被释放的话,使用
unowned
,如果self存在被释放的可能性就使用weak
真正的循环引用
为什么要提到正在的循环引用,当然我主要是针对闭包去谈这个问题,因为很多时候在使用的过程中很多人疯狂的使用weak
,但是却不知道到底在什么情况下会造成循环引用! 其实很简单,就是在self持有闭包的时候,即闭包是self的属性时才会发生循环引用!
class Person {
var name: String
lazy var printName: () -> Void = {
print(self.name)
self.printName = {}
}
init(name: String) {
self.name = name
}
deinit {
print("\(name)对象被销毁")
}
func delay2(_ duration: Int) {
let times = DispatchTime.now() + .seconds(duration)
DispatchQueue.main.asyncAfter(deadline: times) {
print("-------开始执行闭包--------")
print(self.name)
print("-------结束执行闭包---------")
}
}
}
func test2() {
let person = Person.init(name: "小明")
person.delay2(2)
}
test2()
复制代码
可以猜测一下,对象会销毁吗?
-------开始执行闭包--------
小明
-------结束执行闭包---------
小明对象被销毁
复制代码
有人问了?不对啊,我在闭包中使用了self啊,为什么不会造成循环引用呢?因为循环引用最起码有两个持有才是循环,一个是self -> 闭包
还有一个是闭包 -> self
,显然这里是后者,所以包括我们大多少时候使用的网络请求,只要self不持有回调闭包,其实是不会造成循环引用的!
问题来了,为什么很多人都在网络请求中使用weak self呢? 其实我个人感觉还是有必要的,因为很多时候你都不确定网络请求的类是否持有你传入的闭包,所以还是应该使用weak或者unowned的
好,看到这里是不是又有了一个疑问,那就是明明self不持有闭包,为什么闭包还没有释放呢? 这就又涉及另一个知识点了,就是在Swift中闭包和类都是引用类型,你将闭包作为参数传入网络请求中,其实最后是被系统所持有的,比如使用Alamofilre请求数据,调用某个请求方法最后会走到如下区域
(queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
复制代码
而我们使用的UIView的动画,DispatchQueue等其实都是闭包被系统所持有才不会被释放的,这个要明白,当然这只是我的推断,如果哪位大牛知道更详细,或者我理解错误了,希望可以告诉我,很谢谢~
然后提一嘴我的小结论,就是如果使用DispatchQueue的方式捕获的并不是闭包的引用,而是闭包的拷贝
var test = {
print("first")
}
UIView.animate(withDuration: 0.2, delay: 0.5, options: UIViewAnimationOptions.curveLinear, animations: {
test()
}, completion: nil)
test = {
print("second")
}
复制代码
输出:
first
复制代码
所以可以很显然得得知,其实系统捕获的是闭包的拷贝,而不是闭包的引用!!!
而方法中是不是捕获的闭包的引用呢?我们来测试一下:
class Person {
var name: String
init(name: String) {
self.name = name
}
func test(cloure: () -> Void) {
cloure()
}
}
var cloure = {
print("小弟")
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
person.test(cloure: cloure)
}
cloure = {
print("大哥")
}
复制代码
输出
大哥
复制代码
显然,果然方法中传入的是小弟
, 但是输出的是闭包
,哎呀,这个太简单了,不就是方法中传入的是指针吗?大家应该都知道吧~
结语
希望可以给大家一些参考吧,我觉得在学习的过程中,还是应该稍微多想一些,不要浅尝辄止。共同进步吧!