Does @ObservedObject live in the RAM?

I know that an environment object lives in memory, but does an observed object? Or does it live in CPU cache?

I'm asking this question because I'd like to explore the performance differences between having an environment object versus having an observed object which you pass down between all the views (like in TCA).

I think you may have some misconceptions about what CPU caches are. They aren't a separate pool of memory that you put certain things into. They're basically an accelerator for ram: they apply equally to everything, and you can't really control what goes into them.

So to directly answer the question: both kinds of objects live in both RAM and the CPU cache.

2 Likes

I see, thanks for the info. Interestingly, when testing passing down an observable object (instantiated as a @StateObject in the App struct) down a single view, the environment object actually performed better than the observed object – (about 10 μs better). However, when instantiating the observable object as a simple property of the App struct, the opposite is true.

1 Like

And I guess that is because EnvironmentObjects are shared and their context is linked to the place where the first instance was created.

ObservedObjects are always being allocated when the view in redrawn and cause some complexity operations may brick the code.

So, my point is that EnvironmentObjects are cleaner, safety and optimized.

2 Likes

True, well said – I'm building a library where you can actually ignore unnecessary changes to your app state in views which listen to an EnvironmentObject. This is one of the biggest problems which comes with EnvironmentObject: it redraws the whole view hierarchy whenever objectWillChange is sent. But with this, I'll be able to reap the benefits of EnvironmentObject while also sidestepping its disadvantages.

1 Like

Hmm, I didn't remember that. I guess we all learning from SwiftUI :grinning_face_with_smiling_eyes:

I think there are way too many misunderstandings in this thread. The EnvironmentObject property wrapper just grabs the object by the given type from the environment (aka. EnvironmentalValues) or traps if it was never injected before.

If you pay close attention there will be a EnvironmentPropertyKey<StoreKey<Model>> in the environment. You can also see how certain subscriptions are been shared between independent ObservedObject and EnvironmentObject.

class Model: ObservableObject {
  @Published
  var count = 0
}

struct ContentView: View {
  // This PW is irrelevant for this example, we just need it to initialize
  // our model somewhere.
  //
  // I'm intentionally not using `StateObject` to prevent `ContentView` from
  // reloading when model gets updated, as just like `ObservedObject` it will
  // subscribe to `objectWillChange`. This detail is also irrelevant for this
  // example but it's worth explicitly pointing out.
  @State
  var _model = Model()

  var body: some View {
    print(Self.self, #function)

    return VStack {
      // dependency injection through the environment rather than through
      // intermediate `init`s from each view. `ContentView -> A -> B`
      A()
        .environmentObject(_model)
        .border(Color.green)

      // Explicit dependency injection
      Explicit_A(model: _model)
        .border(Color.red)

      Button("increment") {
        _model.count += 1
      }
    }
  }
}

struct A: View {
  var body: some View {
    B()
  }
}

struct Explicit_A: View {
  let model: Model
  var body: some View {
    Explicit_B(model: model)
  }
}

struct B: View {
  @Environment(\.self)
  var _environment: EnvironmentValues

  @EnvironmentObject
  var _model: Model

  init() {
    // Manually initialize those PWs just for the purpose of the example.
    self.__environment = Environment(\.self)
    self.__model = EnvironmentObject()

    print(Self.self, #function)
  }

  var body: some View {
    print(Self.self, #function)
    dump(_environment)
    dump(__model)

    return Text(String("\(_model.count)"))
  }
}

struct Explicit_B: View {
  @ObservedObject
  var _model: Model

  init(model: Model) {
    // Manually initialize those PWs just for the purpose of the example.
    self.__model = ObservedObject(wrappedValue: model)

    print(Self.self, #function)
  }

  var body: some View {
    print(Self.self, #function)
    dump(self.__model)

    return Text(String("\(_model.count)"))
  }
}

The TLDR is that EnvironmentObject is essentially just another ObservedObject which is supplied with the observable object automatically from the environment to avoid passing it manually though the entire view tree.

P.S.: If you know that mutation on your model should not invalidate the entire view tree then I think it's not correct to initialize your model within a StateObject like that.

One "possible" workaround for this could look like this:

struct ContentView: View {
  final class _ImmutableBox: ObservableObject {
    // This publisher will never emit any value, never fail and never complete.
    let objectWillChange = Empty<Never, Never>(completeImmediately: false)
    let model = Model()
  }
  
  // `Model` is still initialized lazely
  @StateObject
  var _box = _ImmutableBox()
  var _model: Model {
    _box.model
  }
  ...
}

If you're curious and fully understand the semantics from ObservedObject, State and DynamicProperty, here is a backwards compatible custom reimplementation of StateObject that will work with iOS 13.

However, to workaround one little issue this implementation requires one additional cycle at the beginning of the container view's lifecycle.

Terms of Service

Privacy Policy

Cookie Policy