Search code examples
cocoacocoa-bindingskey-value-observingdealloc

Cocoa bindings and KVO, unregister the observer, when the observing object gets `dealloced`


How can i unregister the observer, when the observing object gets dealloced?

How can cocoa bindings handle a situation when the observed objects gets deallocated?

By using manual KVO, i have to remove the observing (removeObserver) before dealloc the object... how does Cocoa bindings handle this (stop observing on dealloc of the observed object)?


Solution

  • Update 2017

    As @GregBrown has pointed out in the comments the original 2013 answer does not work in 2017. I assume the original answer did work in 2013, as my practice is not to answer without testing, but I no longer have any code I used.

    So how do you do solve this in 2017? The simplest answer is swizzling, which some will find a contradiction but it need not be when using blocks. Below is a quick proof-of-concept with the following caveats:

    • This is not thread safe. Consider what might happen if two or more threads execute the code at the same time. Standard techniques will address that.

    • Efficiency was not a consideration! For example, you might wish to swizzle dealloc once per class and keep a list of observer/keypath in a per-instance associated object.

    • This code only supports auto-removal, you cannot manually choose to remove an observer. You might wish to change that.

    The code:

    @implementation AutoRemovedKVO
    
    typedef void (*DeallocImp)(id, SEL);
    
    + (void)forTarget:(NSObject *)target
          addObserver:(NSObject *)observer
           forKeyPath:(NSString *)keyPath
              options:(NSKeyValueObservingOptions)options
              context:(nullable void *)context
    {
       // register the observer
       [target addObserver:observer forKeyPath:keyPath options:options context:context];
    
       // swizzle dealloc to remove it
    
       Class targetClass = target.class;
       SEL deallocSelector = NSSelectorFromString(@"dealloc");
       DeallocImp currentDealloc = (DeallocImp)method_getImplementation(  class_getInstanceMethod(targetClass, deallocSelector) );
    
       // don't capture target strongly in block or dealloc will never get called!
       __unsafe_unretained NSObject *targetPointer = target;
    
       void (^replacementBlock)(id self) = ^(__unsafe_unretained id self)
       {
          if (self == targetPointer)
             [targetPointer removeObserver:observer forKeyPath:keyPath];
    
          currentDealloc(self, deallocSelector);
       };
    
       class_replaceMethod(targetClass, deallocSelector, imp_implementationWithBlock(replacementBlock), "v@:");
    }
    
    @end
    

    Both uses of __unsafe_unretained are to work around consequences of ARC. In particular methods usually retain their self argument, dealloc methods do not, and blocks follow the same retain-as-needed model. To use a block as the implementation of dealloc this behaviour needs to be overridden, which is what __unsafe_unretained is being used for.

    To use the above code you simply replace:

    [b addObserver:a forKeyPath:keyPath options:options context:NULL];
    

    with:

    [AutoRemovedKVO forTarget:b addObserver:a forKeyPath:keyPath options:options context:NULL]; 
    

    Allowing for the above caveats the above code will do the job in 2017 (no guarantee for future years!)

    Original 2013 Answer

    Here in outline is how you can handle this, and similar situations.

    First look up associated objects. In brief you can attach associated objects to any other object (using objc_setAssociatedObject) and specify that the associated object should be retained as long as the object it is attached to is around (using OBJC_ASSOCIATION_RETAIN).

    Using associated objects you can arrange for an observer to be automatically removed when the observed object is deallocated. Let X be the observer and Y be the observed objects.

    Create an "unregister" class, say Z, which takes (via init) an X & Y and in its dealloc method does removeObserver.

    To setup the observation, X:

    1. Creates an instance of Z, passing itself and Y.
    2. Registers itself as an observer of Y.
    3. Associates Z with Y.

    Now when Y is deallocated Z will be deallocated, which will result in Z's dealloc being called and unregistering the observation of X.

    If you need to remove the observation of X while Y is still active you do this by removing the associated object - and doing so will trigger its dealloc...

    You can use this pattern whenever you want to trigger something when another object is deallocated.

    HTH