Reacting to Memory Warnings With Swinject

I’ll be honest, this blog post will be a bit obscure. Dependency injection container AND memory warnings? That’s pretty peculiar combination in my opinion.

Not so long ago, every time you created new view controller in Xcode, the default template contained didReceiveMemoryWarning method override. Nowadays, in Xcode 11.4, it’s not longer there. Either Apple is not so concerned about memory usage anymore or they just gave up, because most developers (at least in projects I’ve seen) do not implement this method. What about you? Have you ever implemented it? I personally remember one time during my career when I did that.

This method is not the only way we can react to memory warnings - our apps also receive UIApplication.didReceiveMemoryWarningNotification notification. You can observe it in e.g. your model layer and act accordingly.

As you may now, Swinject is one of the most popular Dependency Injection frameworks for Swift. It has pretty standard API where you register your dependencies in container and then you can resolve them. While registering a dependency, you can specify a scope which describes how instances of this dependency will be managed. Out of the box, Swinject supports 4 scopes:

  • Transient - each time you request a dependency, new instance of it will be created
  • Container - you’ll always get the same instance
  • Graph - one instance is shared when resolving dependency graph, but when dependency is explicitly requested then new instance is created each time
  • Weak - as long as there is some strong reference to dependency, the same instance is returned. Otherwise it is removed and new instance is returned when needed again

On top of that, Swinject allows us to define custom scopes. When I saw that in documentation for the first time, I started thinking about possible use case for it. Then it came to me - what if we could create a scope which clears instances when app receives memory warning? This could be useful if we had some dependency which uses a lot of memory and is expensive to create (E.g. during init it has to read large file to memory). With custom scope, such dependency can be kept in memory to avoid recreating it but removed when available memory is low.

Implementing custom scope

Let’s try to implement it then! Swinject’s documentation shows us how to implement custom scope. To achieve what we want, we need to implement custom InstanceStorage. InstanceStorage, as its name suggests, holds instance of some type. When we ask container to resolve a dependency, it checks if its storage has instance and returns it. If storage has no instance, then container creates new one and saves it in storage. All default scopes are implemented by using some sort of InstanceStorage implementation, e.g. transient uses a storage which always returns nil for instance and ignores setting it. (You can see it here).

Our storage, besides holding an instance, needs to react to memory warnings by removing it from memory. We can do that by observing aforementioned UIApplication.didReceiveMemoryWarningNotification notification. Below you can see full implementation of the storage which I named SufficientMemoryStorage.

extension ObjectScope {
    static let sufficientMemory = ObjectScope(storageFactory: SufficientMemoryStorage.init)
}

private final class SufficientMemoryStorage: InstanceStorage {
    var instance: Any?
    
    override init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(releaseInstanceIfNeeded),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
    
    @objc private func releaseInstanceIfNeeded() {
        weak var weakInstance = instance as AnyObject
        instance = nil
        instance = weakInstance
    }
}

You might ask, what’s up with assigning the instance to weak var weakInstance and then reassigning it to instance. By doing that, we remove instance from storage only when it’s not used anywhere. If instance is not used, then instance is the only strong reference to it and weakInstance will be cleared when we assign nil to instance. If instance has other strong references, then assigning nil to instance does not clear weakInstance so we reassign it instance to keep it in the storage.

Custom scope usage

Let’s now use our custom scope in the wild. I created sample application which has navigation controller with one view controller in it. When button is tapped, second view controller is pushed. This second view controller uses a service (called ExpensiveService 😉) which is both expensive to create and takes significant memory space. By using custom scope we created above, one instance ExpensiveService is used as long as memory warning does not occur. If memory warning occurs and second view controller isn’t on screen at the time, instance of ExpensiveService is removed from memory. Moreover, whole app is unaware of this behavior, as this is part of dependency container setup. This is how the setup looks like:

let appContainer = Container { container in
    container
        .register(UINavigationController.self) { resolver in
            UINavigationController(
                rootViewController: resolver.resolve(FirstViewController.self)!
            )
        }
    
    container
        .autoregister(FirstViewController.self, initializer: FirstViewController.init)
    
    container
        .register(SecondViewControllerFactory.self) { resolver in
            { resolver.resolve(SecondViewController.self)! }
        }
    
    container
        .autoregister(SecondViewController.self, initializer: SecondViewController.init)
        .inObjectScope(.transient)
    
    container
        .autoregister(ExpensiveServiceType.self, initializer: ExpensiveService.init)
        .inObjectScope(.sufficientMemory)
}

We have all our view controllers registered in the container, along with factory which FirstViewController uses to create instance of SecondViewController (We could use appContainer in FirstViewController to resolve SecondViewController instance, but that would turn our container into service locator. And that is an anti-pattern). On the bottom we have registration for ExpensiveService. You can see that usage of our custom scope is no different than using built-in scopes.

You can run the example yourself. After starting the app in simulator, you can trigger memory warnings by pressing Shift + Cmd + M or from Debug menu. ExpensiveService prints to the console each time it is initialized and deinitialized, so you can clearly see how our custom scope manages its lifetime.

I have yet to use this in production, but maybe you’ll be first 😃. If so, don’t hesitate and share it in the comments or by mentioning me on Twitter. See you next time!

comments powered by Disqus