Categories
iOS

The pitfall in using weak referenced delegations for observer pattern

As Android and Java expert, recently I have came across a very nice iOS implementation for the observer pattern that exploits swift’s ARC to automatically “remove” the observer, this of course cannot be implemented in a JVM GC as unlike ARC, GC does not guarantee clearing weak references right on time. It was so good and simple (If it could be used in Android – there was no need for google to invent LiveData) that I had to deep dive into it, is it really that perfect?

For a working example, I’m gonna simplify things a bit and use Producer Consumer example for our discussion but the idea is the same. so it goes like this :

Inject the Consumer into the Producer and weakly reference the consumer inside the Producer. This way one may think he can exploit swift’s ARC to automatically “remove” the consumer for him by zeroing the references to it when he wants to stop. but is it really that safe and sound?

Consider the following simple scenario :

				
					enum ResourceError: Error {
        case UsingFreedResource
    }
    class Resource {
        var freed = false
        func use() throws {
            print("resource used")
            if (freed) {
                throw ResourceError.UsingFreedResource
            }
        }
        func free() {
            print("resource freed")
            freed = true
        }
    }
    class Consumer {
        var resource : Resource
        init (r: Resource) {
            self.resource = r
        }
        func consume(i: Int) {
            print("Consumer consumed (i)")
            // we are expecting resource to be useable while this consumer lives so we feel comfortable to use try!
            try! resource.use()
        }
        deinit {
            print("Consumer deinit")
        }
    }
    class Producer {
        weak var consumer: Consumer?
        init(c: Consumer) {
            self.consumer = c
        }
        func produce() {
            let dispatchQueue = DispatchQueue(label: "Test", qos: .background)
            dispatchQueue.async{
                guard let c = self.consumer else {
                    return
                }
                for _ in 1...4 {
                    // lets assume it takes us a lot of time to produce
                    sleep(1)
                    c.consume(i: Int.random(in: 1...10))
                }
            }
        }
        deinit {
            print("Producer deinit")
        }
    }
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let r = Resource()
        var c : Consumer? = Consumer(r: r)
        let p = Producer(c: c!)
        p.produce()
        print("we do some stuff for 3 seconds, consuming what producer produce all along")
        sleep(3)
        print("we set c = nill because we do not want to consume anymore")
        c = nil
        print("now we expect that consumer callbacks will never be called so we can free resources")
        r.free()
        return true
    }
				
			

And the output :

we do some stuff for 3 seconds, consuming what producer produce all along

Consumer consumed 2

resource used

Consumer consumed 1

resource used

we set c = nill because we do not want to consume anymore

now we expect that consumer callbacks will never be called so we can free resources

resource freed

Consumer consumed 8

resource used

Fatal error: ‘try!’ expression unexpectedly raised an error: a.AppDelegate.ResourceError.UsingFreedResource: file a/AppDelegate.swift, line 36

2021-07-14 22:53:22.755141+0300 a[29664:556943] Fatal error: ‘try!’ expression unexpectedly raised an error: a.AppDelegate.ResourceError.UsingFreedResource: file a/AppDelegate.swift, line 36

The thing that can be easily missed when using this approach is that when you actually following the weak reference and its not yet nil, you are putting it in a variable and the variable holds a strong reference to it. so even if the references outside are all cleared, the object is still alive as long as that variable still references to it. so it is not safe to assume, that if you clear all reference to the consumer/observer in your code, the object is not held inside the producer/observable callback executing code. 

To summarize – using this approach is great as long as you are aware that when you stop referencing the observer its still can be held inside the observable in a previous execution of emitting a change to it.

Leave a Reply

Your email address will not be published. Required fields are marked *