Swift之你真的知道爲何使用weak嗎?

寫在前面:最近在學習上有一些煩躁,莫名的,學海無涯苦做舟吧!但願再接再礪的堅持下去,學習,創業!!!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 它們分別表明了逃逸閉包和自動閉包函數

@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, 卻沒有造成引用的閉環,因此並不會形成循環引用!關於這裏在後面會詳細描述到,如今來看看閉包中的兩個關鍵字,WeakOwned 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避免循環引用有兩種方式:

  1. 事前預防,即便用weak,unowne
  2. 過後補救,即在傳入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("大哥")
}
複製代碼

輸出

大哥
複製代碼

顯然,果真方法中傳入的是小弟, 可是輸出的是大哥,哎呀,這個太簡單了,不就是方法中傳入的是指針嗎?你們應該都知道吧~ 方法中獲取的是閉包的引用!

結語

但願能夠給你們一些參考吧,我以爲在學習的過程當中,仍是應該稍微多想一些,不要淺嘗輒止。共同進步吧!

相關文章
相關標籤/搜索