寫在前面:最近在學習上有一些煩躁,莫名的,學海無涯苦做舟吧!但願再接再礪的堅持下去,學習,創業!!!swift
在Swift中:變量分爲值類型和引用類型。若是是引用類型,則是捕獲了對象的引用,即在閉包中複製了一份對象的引用,對象的引用計數加1;若是是值類型呢,捕獲的是值類型的指針,若是在閉包中修改值類型的話,一樣會改變外界變量的值。api
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了,因此這裏的輸出爲bash
開始執行閉包
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
}
複製代碼
輸出以下:async
開始執行閉包
count = 1
number = 11
複製代碼
聊到閉包,就不得不提到閉包的兩個關鍵字@escaping
和@autoclosure
它們分別表明了逃逸閉包和自動閉包函數
func delay(_ duration: Int, closure: @escaping () -> Void) {
let times = DispatchTime.now() + .seconds(duration)
DispatchQueue.main.asyncAfter(deadline: times) {
print("開始執行閉包")
closure()
}
print("方法執行完畢")
}
複製代碼
這個方法就是一個典型的例子,做爲參數傳遞進來的閉包是會延時執行的,因此函數先有返回值,再有閉包執行,因此閉包參數須要添加上@escaping
關鍵字學習
方法執行完畢
開始執行閉包
複製代碼
其實自動閉包,大可能是聽得多,用得少,它的做用是簡化參數傳遞,而且延遲執行時間。 咱們來寫一個簡單的方法測試
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
閉包,只會在須要執行它的時候纔會去執行,而並不會在一開始去就計算出結果,和懶加載有些相似~
閉包的循環引用的原理:object -> 閉包 -> object 造成環形引用,從而沒法釋放彼此,造成了循環引用!那麼問題來了:
UIView.animate(withDuration: TimeInterval) {
}
DispatchQueue.main.async {
}
複製代碼
在以上兩個閉包中使用self
調用方法,會形成循環引用嗎?
還用想嗎?固然不會啦,首先self
要持有閉包,纔有可能循環引用,可是self
不持有閉包,閉包雖然會強引用 self
, 卻沒有造成引用的閉環,因此並不會形成循環引用!關於這裏在後面會詳細描述到,如今來看看閉包中的兩個關鍵字,Weak
和 Owned
Apple建議若是能夠肯定self在訪問時不會被釋放的話,使用unowned
,若是self存在被釋放的可能性就使用weak
咱們來看一個簡單的例子
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避免循環引用有兩種方式:
lazy var printName: () -> Void = {
print(self.name)
self.printName = {}
}
複製代碼
輸出結果以下:
-------開始執行閉包--------
小明
-------結束執行閉包---------
小明對象被銷燬
複製代碼
其實至關於我在執行完畢以後,主動斷開閉包對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("大哥")
}
複製代碼
輸出
大哥
複製代碼
顯然,果真方法中傳入的是小弟
, 可是輸出的是大哥
,哎呀,這個太簡單了,不就是方法中傳入的是指針嗎?你們應該都知道吧~ 方法中獲取的是閉包的引用!
但願能夠給你們一些參考吧,我以爲在學習的過程當中,仍是應該稍微多想一些,不要淺嘗輒止。共同進步吧!