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 对应的实例,其存续时间会小于视图的存续时间吗?

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 办法手动取消订阅。

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

除了能够从订阅者一方自动取消订阅联系外,如果 Publisher 不复存在了,订阅联系也将自动解除。

ObservedObject 和 StateObject 两者都保存了视图与可调查目标的订阅联系,在视图存续期内,它们都不会自动取消这个订阅,但 ObservedObject 无法确保可调查目标是否会因为被毁掉而提前取消订阅。

描绘、实例与视图

SwiftUI 是一个声明式的框架,开发者用代码来声明( 描绘 )想要的 UI 呈现。例如下面就是一个有关视图的声明( 描绘 ):

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 视图的生命周期研究](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 视图的存续机制有所帮助。

希望本文能够对你有所帮助。一起也欢迎你经过 Twitter、 Discord 频道 或博客的留言板与我进行交流。

我正以聊天室、Twitter、博客留言等讨论为创意,从中选取有代表性的问题和技巧制作成 Tips ,发布在 Twitter 上。每周也会对当周博客上的新文章以及在 Twitter 上发布的 Tips 进行汇总,并经过邮件列表的形式发送给订阅者。

订阅下方的 邮件列表,能够及时取得每周的 Tips 汇总。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】