寫在前面:最近在學習上有一些煩躁,莫名的,學海無涯苦作舟吧!希望再接再厲的堅持下去,學習,創業!!!
閉包捕獲的是變數的引用而不是當前變數的拷貝
注意,這裡的變數包含了值型別和引用型別。如果是引用型別,則是捕獲了物件的引用,即在閉包中複製了一份物件的引用,物件的引用計數加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("大哥")
}
複製程式碼
輸出
大哥
複製程式碼
顯然,果然方法中傳入的是小弟
, 但是輸出的是閉包
,哎呀,這個太簡單了,不就是方法中傳入的是指標嗎?大家應該都知道吧~
結語
希望可以給大家一些參考吧,我覺得在學習的過程中,還是應該稍微多想一些,不要淺嘗輒止。共同進步吧!