當 Swift 中的 lazy、weak 碰上 NSObject

語言: CN / TW / HK

前言

Hi Coder,我是 CoderStar!

今天給大家介紹一個我遇到的小坑。過程大概是這樣的,一個複用頁面通過不同的入口進入,等返回時,有的正常,有的卻出現了 Crash,log 資訊如下。

Cannot form weak reference to instance XXXXXX. It is possible that this object was over-released, or is in the process of deallocation.

然後看了一下 Crash 時候的呼叫棧,發現崩潰在deinitKVO釋放Observer的過程中。一段排查之後,新的坑點出爐了。

具體業務程式碼就不貼了,貼一個能觸發 Bug 的 Demo 吧(不包含使用合理性,僅用來測試 Crash)。

問題

```swift protocol MyServiceDelegate: AnyObject {}

class MyService { weak var delegate: MyServiceDelegate? func stop() {} }

class MyClass: NSObject, MyServiceDelegate { private lazy var service: MyService = { let service = MyService() service.delegate = self return service }()

deinit {
    service.stop()
}

}

// 測試 func test() { let myClass = MyClass() } ```

大家覺得這段程式碼會發生什麼?可能大家看了上面的介紹心中已經有了預想的答案。是的,跟上面 Crash 報錯資訊一致。

那我們分析一下,問題出在哪裡?其實 Crash 資訊相對已經比較明顯了,結合到程式碼就是 self 當前已經在釋放中了(deinit),不可以被弱引用了(service.delegate = self)。

其實出現這個 Crash 有三個條件:

  • lazy
  • weak
  • NSObject

示例程式碼去除這三個條件中任何一個,Crash 都不會發生。

具體原因:附上 automatic-reference-counting 中的一段描述。

ARC's implementation of zeroing weak references requires close coordination between the Objective-C reference counting system and the zeroing weak reference system. This means that any class which overrides retain and release can't be the target of a zeroing weak reference. While this is uncommon, some Cocoa classes, like NSWindow, suffer from this limitation. Fortunately, if you hit one of these cases, you will know it immediately, as your program will crash with a message like this:

objc[2478]: cannot form weak reference to instance (0x10360f000) of class NSWindow

如果大家有興趣看原始碼,可以檢視 objc4-680 中的weak_register_no_lock函式。其丟擲了 Crash。

解決

解決方式其實可以很簡單,先介紹簡單的一種:

解決方式一

定義一個service是否初始化的變數,然後在deinit時根據變數控制是否繼續呼叫service.stop()。就是下面這樣:

```swift class MyClass: NSObject, MyServiceDelegate { private var isServiceInit = false private lazy var service: MyService = { isServiceInit = true

    let service = MyService()
    service.delegate = self
    return service
}()

deinit {
    /// 實際業務中,service未初始化使用過,也不會需要在`deinit`時再進行一些處理
    if isServiceInit {
        service.stop()
    }
}

} ```

解決方式二

如果這樣的屬性比較多,可能上面的方式就需要定義同等數量的控制變數,相對比較繁瑣,那我們就簡單封裝一下。

```swift final public class Lazy { public init(initializer: @escaping () -> T) { self.initializer = initializer }

public private(set) var valueIfInitialized: T?

public var wrapped: T {
    if let value = valueIfInitialized {
        return value
    } else {
        let value = initializer()
        valueIfInitialized = value
        return value
    }
}

private let initializer: () -> T

} ```

使用變成了這樣。

```swift class MyClass: NSObject, MyServiceDelegate { private lazy var service = Lazy { let service = MyService() service.delegate = self return service }

/// 過程中使用service,可使用 service.wrapped

deinit {
    service.valueIfInitialized?.stop()
}

} ```

雖然也還是有點醜陋,但是聊勝於無吧。暫時沒找到更好的寫法了。

最後

本次文章比較簡短,現在就 Enjoy 吧!

要更加努力呀!

Let's be CoderStar!


有一個技術的圈子與一群同道中人非常重要,來我的技術公眾號,這裡只聊技術乾貨。

微信公眾號:CoderStar