Swift裏你可能不知道的事兒(3)——處理closure和類對象之間的reference cycle

泊學高清視頻
泊閱文檔
咱們來了解一下這類reference cycle是如何發生的,以及對應的解決方法。swift

什麼是Closure和類對象間的reference cycle

首先,咱們定義一個類,用來表示HTML DOM元素:spa

class HTMLElment {
    let name: String
    let text: String?
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(self.name) is being deinitialized")
    }
}

其中name表示HTML標籤的名字,text表示標籤之間的文本內容,因爲不是全部HTML標籤之間都有文本,所以,它是一個String?。接下來,咱們能夠像這樣定一個HTMLElement對象。code

var h1: HTMLElment? = HTMLElment(name: "h1", text: "Title")

若是,咱們但願能夠把HTMLElement表明的對象渲染出來,例如:<h1>Title</h1>。爲了能夠在將來定製這個渲染操做,咱們決定給HTMLElement添加一個Closure member,它不接受任何參數,返回咱們但願渲染的字符串:orm

class HTMLElment {
    let name: String
    let text: String?
    var asHTML: Void -> String = { // WRONG SYNTAX!!!
        if let text = self.text {
            return "<\(self.name)>\(self.text)</\(self.name)>"
        }
        else {
            return "<\(self.name)>"
        }
    }

    // Omit for simplicity...
}

當咱們這樣編寫asHTML的時候,Swift會告訴咱們發生了一些語法錯誤:
bo-reading-unresolved-self@2x.jpg視頻

這是因爲,Swift沒法確認當咱們在Closure中使用self時,它已經被完整的初始化過了。若是咱們須要這種初始化約束,咱們能夠把asHTML定義爲lazy。對象

class HTMLElment {
    // Omit for simplicity
    lazy var asHTML: Void -> String = {
        // Omit for simplicity...
    }

    // Omit for simplicity...
}

**「lazy能夠確保一個成員只在類對象被完整初始化過以後,才能使用。」
——特別提示**ci

定義了asHTML以後,咱們就能夠觀察h1的構建和釋放過程了。首先,咱們看使用asHTML以前:文檔

var h1: HTMLElment? = HTMLElment(name: "h1", text: "Title")
h1 = nil

在Playground結果裏,咱們能夠看到h1先被建立,然後被銷燬的過程(由於HTMLElement的deinit方法被調用了)。
bo-reading-normal-deinit-h1@2x.jpg字符串

而當咱們在讓h1等於nil前,使用asHTML的話,狀況就不一樣了:get

var h1: HTMLElment? = HTMLElment(name: "h1", text: "Title")
h1.asHTML
h1 = nil

這時咱們就發現,HTMLElement的deinit再也不被調用了。
bo-reading-abnormal-deinit-h1@2x.jpg

根據咱們以前的經驗,必定是在某處發生了reference cycle。爲了可以搞清楚這個問題,咱們先來看一下在h1等於nil以前,相關對象之間的關係:
bo-reading-closure-ref-cycle@2x.jpg

h1是咱們定義的strong reference。Closure做爲一個引用類型,它有本身的對象,所以asHTML也是一個strong reference。因爲asHTML「捕獲」了HTMLElement的self,所以HTMLElement的引用計數是2。當h1爲nil時,asHTML對closure的引用和closure對self的「捕獲」就造成了一個reference cycle。

**「儘管在closure內部,使用了屢次selfclosure對self的捕獲僅發生1次(引用計數只加1)。」
——特別提示**

用capture list解決reference cycle

本質上來講,closure做爲一個引用類型,解決reference cycle的方式和解決類對象之間的reference cycle是同樣的,若是引發reference cycle的"捕獲"不能爲nil,就把它定義爲unowned,不然,定義爲weak。而指定「捕獲」方式的地方,叫作closure的capture list。咱們把asHTML修改爲下面這樣:

class HTMLElment {
    let name: String
    let text: String?
    lazy var asHTML: Void -> String = {
        // text
        // Capture list
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(self.text)</\(self.name)>"
        }
        else {
            return "<\(self.name)>"
        }
    }

    // Omit for simplicity...
}

咱們使用一對 [] 表示closure的capture list,因爲「捕獲」到的self不能爲nil(不然closure也不存在了),所以咱們把它定義爲unowned self。在咱們這樣作以後,當h1爲nil時,對象之間的關係就變成了這樣:
bo-reading-closure-unowned-capture@2x.jpg

因爲HTMLElement沒有了strong reference,所以它會被ARC釋放掉,進而asHTML引用的closure也會變成「孤魂野鬼」,ARC固然也不會放過它。所以,closure和類對象間的循環引用問題就解決了。

在這裏,關於closure capture list,咱們要多說兩點:

  • 若是closure帶有完整的類型描述,capture list必須寫在參數列表前面;

  • 若是咱們要在capture list裏添加多個成員,用逗號把它們分隔開;

    class HTMLElment {

    let name: String
       let text: String?
       lazy var asHTML: Void -> String = {
           // text
           // Capture list
           [unowned self /*, other capture member*/] () -> String in
           if let text = self.text {
               return "<\(self.name)>\(self.text)</\(self.name)>"
           }
           else {
               return "<\(self.name)>"
           }
       }
    
       // Omit for simplicity...

**「當一個類中存在訪問數據成員的closure member時,務必要謹慎處理它有可能帶來的reference cycle問題。」——特別提示**

相關文章
相關標籤/搜索