StateObject 與 ObservedObject

語言: CN / TW / HK

StateObject 是在 SwiftUI 2.0 中才新增的屬性包裝器,它的出現解決了在某些情況下使用 ObservedObject 檢視會出現超預期的問題。本文將介紹兩者間的異同,原理以及注意事項。

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】

先說結論

StateObject 和 ObservedObject 兩者都是用來訂閱可觀察物件( 符合 ObservableObject 協議的引用型別 )的屬性包裝器。當被訂閱的可觀察物件通過內建的 Publisher 傳送資料時( 通過 @Published 或直接呼叫其 objectWillChange.send 方法 ),StateObject 和 ObservedObject 會驅動其所屬的檢視進行更新。

ObservedObject 在檢視的存續期間只儲存了訂閱關係,而 StateObject 除了儲存了訂閱關係外還保持了對可觀察物件的強引用。

基於 Swift 的 ARC( 自動引用計數 )機制,StateObject 保證了可觀察物件的生存期必定不小於檢視的存續期,從而確保了在檢視的存續期內資料的穩定。

而由於 ObservedObject 只儲存了訂閱關係,一旦被訂閱的可觀察物件的生存期小於檢視的存續期,檢視會出現各種不可控的表現。

相信有人會提出這樣的疑問,難道下面程式碼中的 testObject 對應的例項,其存續時間會小於檢視的存續時間嗎?

swift struct DemoView: View { @ObservedObject var testObject = TestObject() var body: some View { Text(testObject.name) } }

在某些情況下,確實會是這樣。下文中將詳細探討其中的原因。

原理

ARC

Swift 使用自動引用計數( ARC )來跟蹤和管理引用型別例項的記憶體使用情況。只要還有一個對類例項的強引用存在,ARC 便不會釋放該例項佔用的記憶體。換而言之,一旦對例項的強引用為 0 ,該例項將被 Swift 銷燬,其所佔用的記憶體也將被收回。

StateObject 通過保持一個對可觀察物件的強引用,確保了該物件例項的存續期不小於檢視的存續期。

訂閱 與 Cancellable

在 Combine 中,當使用 sink 或 assign 來訂閱某個 Publisher 時,必須要持有該訂閱關係,才能讓這個訂閱正常工作,訂閱關係被包裝成 AnyCancellable 型別,開發者可以通過呼叫 AnyCancellable 的 cancel 方法手動取消訂閱。

```swift var cancellable: AnyCancellable? init() { cancellable = NotificationCenter.default.publisher(for: .AVAssetContainsFragmentsDidChange) .sink { print($0) } }

var cancellable = Set() init() { NotificationCenter.default.publisher(for: .AVAssetContainsFragmentsDidChange) .sink { print($0) } .store(in: &cancellable) } ```

除了可以從訂閱者一方主動取消訂閱關係外,如果 Publisher 不復存在了,訂閱關係也將自動解除。

ObservedObject 和 StateObject 兩者都儲存了檢視與可觀察物件的訂閱關係,在檢視存續期內,它們都不會主動取消這個訂閱,但 ObservedObject 無法確保可觀察物件是否會由於被銷燬而提前取消訂閱。

描述、例項與檢視

SwiftUI 是一個宣告式的框架,開發者用程式碼來宣告( 描述 )想要的 UI 呈現。例如下面便是一個有關檢視的宣告( 描述 ):

swift struct DemoView:View{ @StateObject var store = Store() var body: some View{ Text("Hello \(store.username)") } }

當 SwiftUI 開始建立以該描述生成的檢視時,大致會進行如下的步驟:

  • 建立一個 DemoView 的例項
  • 進行與該檢視有關的一些準備工作( 例如依賴注入 )
  • 對該例項的 body 屬性求值
  • 渲染檢視

從 SwiftUI 的角度來說,檢視是對應著螢幕上某個區域的一段資料,它是通過呼叫某個根據描述該區域的宣告所建立的例項的 body 屬性計算而來。

檢視的生存期從其被載入到檢視樹時開始,至其被從檢視樹上移走結束。

在檢視的存續期中,檢視值將根據 source of truth ( 各種依賴源 )的變化而不斷變化。SwiftUI 也會在檢視存續期內因多種原因,不斷地依據描述該區域的宣告建立新的例項,從而保證始終能夠獲得準確的計算值。

由於例項是會反覆建立的,因此,開發者必須用特定的標識( @State、@StateObject 等 )告訴 SwiftUI ,某些狀態是與檢視存續期繫結的,在存續期期間是唯一的。

當將檢視載入到檢視樹時,SwiftUI 會根據當時採用的例項將需要繫結的狀態( @State、@StateObject、onReceive 等 )託管到 SwiftUI 的託管資料池中,之後無論例項再被建立多少次,SwiftUI 始終只使用首次建立的狀態。也就是說,為檢視繫結狀態的工作只會進行一次。

請閱讀 SwiftUI 檢視的生命週期研究 一文,瞭解更多有關檢視與例項之間的關係

屬性包裝器

Swift 的屬性包裝器( Property Wrappers )在管理屬性儲存方式的程式碼和定義屬性的程式碼之間添加了一層分離。一方面它方便開發者將一些通用的邏輯統一封裝起來,作用於給定的資料之上,另一方面如果開發者對某個屬性包裝器的用途不甚瞭解,那麼就可能會出現看到的和實際上的不一致的情況( 理解偏差 )。

很多情況下,我們需要從檢視的角度來理解 SwiftUI 的屬性包裝器名稱,例如:

  • ObservedObject ( 檢視訂閱某個可觀察物件 )
  • StateObject( 訂閱某個可觀察物件,並持有其強引用 )
  • State( 持有某個值 )

ObservedObject 和 StateObject 兩者通過滿足 DynamicProperty 協議從而實現上面的功能。在 SwiftUI 將檢視新增到檢視樹上時,呼叫 _makeProperty 方法將需要持有的訂閱關係、強引用等資訊儲存到 SwiftUI 內部的資料池中。

請閱讀 避免 SwiftUI 檢視的重複計算 一文,瞭解更多有關 DynamicProperty 的實現細節

ObservedObject 偶爾出現靈異現象的原因

如果使用類似 @ObservedObject var testObject = TestObject() 這樣的程式碼,有時會出現靈異現象。

@StateObject 研究 一文中,展示了因錯誤使用 ObservedObject 而引發靈異現象的程式碼片段

出現這種情況是因為一旦,在檢視的存續期中,SwiftUI 建立了新的例項並使用了該例項( 有些情況下,建立新例項並不一定會使用 ),那麼,最初建立的 TestObject 類例項將被釋放( 因為沒有強引用 ),ObservedObject 中持有的訂閱關係也將無效。

某些檢視,或許是由於其所處的檢視樹的層級很高( 例如根檢視 ),或者由於其本身的生存期較短,抑或者它受其他狀態的干擾較少。上述條件促使了在該檢視的存續期內 SwiftUI 只會建立一個例項。這也是 @ObservedObject var testObject = TestObject() 並非總會失效的原因。

注意事項

  • 避免建立 @ObservedObject var testObject = TestObject() 這樣的程式碼

原因上文中已經介紹了。ObservedObject 的正確用法為:@ObservedObject var testObject:TestObject 。通過從父檢視傳遞一個可以保證存續期長於當前檢視存續期的可觀察物件,從而避免不可控的情況發生

  • 避免建立 @StateObject var testObject:TestObject 這樣的程式碼

@ObservedObject var testObject = TestObject() 類似, @StateObject var testObject:TestObject 偶爾也會出現與預期不符的狀況。例如,在某些情況下,開發者需要父檢視不斷地生成全新的可觀察物件例項傳遞給子檢視。但由於子檢視中使用了 StateObject ,它只會保留首次傳入的例項的強引用,後面傳入的例項都將被忽略。儘量使用 @StateObject var testObject = TestObject() 這樣不容易出現歧義表達的程式碼

  • 輕量化檢視中使用的引用型別的構造方法

無論使用 ObservedObject 還是 StateObject 抑或不新增屬性包裝器,在檢視中宣告的類例項,都會隨著檢視描述例項的建立而一遍遍地被多次建立。不在它的構造方法中引入無關的操作可以極大地減輕系統的負擔。對於資料的準備工作,可以使用 onAppear 或 task ,在檢視載入時進行。

總結

StateObject 和 ObservedObject 是我們經常會使用的屬性包裝器,它們都有各自擅長的領域。瞭解它們內涵不僅有助於選擇合適的應用場景,同時也對掌握 SwiftUI 檢視的存續機制有所幫助。

希望本文能夠對你有所幫助。同時也歡迎你通過 TwitterDiscord 頻道 或部落格的留言板與我進行交流。

我正以聊天室、Twitter、部落格留言等討論為靈感,從中選取有代表性的問題和技巧製作成 Tips ,釋出在 Twitter 上。每週也會對當周部落格上的新文章以及在 Twitter 上釋出的 Tips 進行彙總,並通過郵件列表的形式傳送給訂閱者。

訂閱下方的 郵件列表,可以及時獲得每週的 Tips 彙總。

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】