Swift 記憶體管理之 weak 與 unowned

Wenslow發表於2019-04-20

在用 Swift 做開發時,我們可以使用 weak 或是 unowned 打破類例項和閉包的強引用迴圈。今天我們來聊一聊 weakunowned 的相同和不同之處。

weak

日常開發中,我們經常會用 weak 來標記代理或者在閉包中使用它來避免引用迴圈。

weak var delegate: SomeDelegate?

lazy var someClosure: () -> Void = { [weak self] in
    guard let self = self else { retrun }
    self.balabala
}
複製程式碼

當我們賦值給一個被標記 weak 的變數時,它的引用計數不會被改變。而且當這個弱引用變數所引用的物件被釋放時,這個變數將被自動設為 nil這也是弱引用必須被宣告為 Optional 的原因。

unowned

weak 相同,unowned 也可以在不增加引用計數的前提下,引用某個類例項。

unowned let someInstance: SomeClass

lazy var someClosure: () -> Void = { [unowned self] in
    self.balabala
}
複製程式碼

在使用 unowned 時,我們不需要將變數宣告為 Optional

需要注意的是。對於被 unowned 標記的變數,即使它的原來引用已經被釋放,它仍然會保持對被已經釋放了的物件的一個 "無效的" 引用,它不是 Optional ,也不會被指向 nil。所以,當我們試圖訪問這樣的 unowned 引用時,程式就會發生錯誤。

我們看下邊這段示例程式碼:

class SomeSingleton {
    
    static let share = SomeSingleton()
    
    func closure(closure: (() -> Void)?) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            closure?()
        }
    }
}

class Person {
    
    let someSingleton = SomeSingleton.share
    let portrait = UIImage()
    
    func testClosure() {
        someSingleton.closure { [unowned self] in
            print(self.portrait)
        }
    }
    
    deinit {
        print("Person is deinited")
    }
}

class ViewController: UIViewController {
    
    var person: Person?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        person = Person()
        
        person?.testClosure()
        
        person = nil
    }
}
複製程式碼

在這裡我們定義了一個單例,這個單例提供一個兩秒之後觸發的閉包。接著我們又在 Person 這個類中引用這個單例。最後,我們在 ViewController 中例項化一個 Person 物件,並在呼叫 testClosure() 方法後,將其設為 nil

在程式執行之後,我們觀察控制檯的 log。persondenint 後,控制檯列印出了 Person is deinited。在兩秒後,單例的閉包被觸發,程式嘗試訪問 personportrait 屬性。由於 person 在此時已經是 nil,我們正在嘗試讀取一個已經被釋放,但 unowned reference 還存在但物件。所以程式丟擲了異常。

Person is deinited
Fatal error: Attempted to read an unowned reference but object 0x6000027b5bf0 was already deallocated2019-04-20
複製程式碼

如果我們將 [unowned self] 替換為 [weak self],再重新執行一遍程式。

someSingleton.closure { [weak self] in
    print(self?.portrait)
}
複製程式碼

觀察控制檯的log。在 person 被設定為 nil 兩秒之後,單例閉包被觸發。由於我們在閉包中使用了 weak,所以程式不會出錯,self?.portrait 的值為 nil

Person is deinited
nil
複製程式碼

weak vs. unowned

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.

根據蘋果的官方文件的建議。當我們知道兩個物件的生命週期並不相關,那麼我們必須使用 weak。相反,非強引用物件擁有和強引用物件同樣或者更長的生命週期的話,則應該使用 unowned

例如,ViewControler 對它的 SubView 的引用可以使用 unowned。因為 ViewControler 的生命週期一定比對它的 SubView 長。

而在使用服務時,則需要看情況使用 weak。因為服務的初始化方法可能是被工廠模式或 Service Locator 所封裝。這些服務可能在某些時候被重構為單例,此時它們的生命週期發生了改變。

捕獲列表

除了常用的 weak selfunowned self 之外,我們還可以使用捕獲列表來打破閉包引用迴圈。將需要被捕獲的變數,用 weak selfunowned self 標記。

someSingleton.closure { [weak portrait] in
    print(portrait)
}

/* 或者 */

someSingleton.closure { [unowned portrait] in
    print(portrait)
}
複製程式碼

捕獲列表也可以用來初始化新的變數

/* 由於 UIImageView(image: portrait) 返回 Optional 的值,
而 unowned 不可以用來標示 Optional 的變數,
所以在這裡我們需要強制解包。 */

someSingleton.closure { [unowned imageView = UIImageView(image: portrait)!] in
    print(imageView)
}

/* 或者 */

someSingleton.closure { [weak imageView = UIImageView(image: portrait)] in
    print(imageView)
}
複製程式碼

此時編譯器會給出警告。因為這些變數的作用域只在閉包內部。

Instance will be immediately deallocated because variable 'imageView' is 'unowned'

/* 或者 */

Instance will be immediately deallocated because variable 'imageView' is 'weak'
複製程式碼

參考:Automatic Reference Counting

相關文章