KVO bug / counter-intuitive behavior

A question mainly to Foundation developers. The example below demonstrates how leaking a KVO observer causes crash on a seemingly unrelated operation. On one hand this is of course programmer's bug, on the other hand one of our devs has just spent a lot of time figuring out what is wrong with "unrelatedGoodProperty" in her code. Do you think it is something that can be improved? Or documented?


@interface Spectacular : NSObject
@property (copy) NSString* propertyLeakingObservers;
@property (copy) NSString* unrelatedGoodProperty;
@end
@implementation Spectacular
@end

@interface LeakedObserver : NSObject
@end
@implementation LeakedObserver
@end

@interface Observer : NSObject
@end
@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
{
    NSLog(@"Observing %@ of %@", keyPath, object);
}
@end

- (void) applicationDidFinishLaunching:(NSNotification *)aNotification;
{
    Spectacular* spectacular = [Spectacular new];
    @autoreleasepool {
        LeakedObserver* observerLeak = [LeakedObserver new];
        [spectacular addObserver:observerLeak forKeyPath:@"propertyLeakingObservers" options:0 context:0];
    }
    Observer* observer2 = [Observer new];
    [spectacular addObserver:observer2 forKeyPath:@"unrelatedGoodProperty" options:0 context:0];

    spectacular.unrelatedGoodProperty = @"boop"; // *** -[LeakedObserver retain]: message sent to deallocated instance 0x6040000013d0

    [spectacular removeObserver:observer2 forKeyPath:@"unrelatedGoodProperty" context:0];
}

It's not a KVO bug, and to be precise it's not a leak. (A leak would occur if you failed to release an object that had no references.) But it is a memory management bug. Specifically, "addObserver:…" is documented (https://developer.apple.com/documentation/objectivec/nsobject/1412787-addobserver) to not retain the observer object.


I'd say in general the how-do-I-track-this-down situation cannot be improved, because memory management bugs tend to manifest themselves long after the point of the error. The kind of crash or misbehavior you see is often unrelated to the behavior of the mis-managed object. You have to very, very lucky to be able to draw line from the outcome to the source of the bug.


In this case, if the error seen originally was "message sent to deallocated object", or even if the error message was uninformative, it's always a good start to assume a memory management error. Once you get to identifying the class of the receiver, and knowing that it was a property observer for this object, then looking beyond the specific property should be a standard debugging move.


Assuming you end up in the debugger when the "retain" fails, you can sometimes gain a bit of traction by examining the value of the "observationInfo" property of any NSObject. This should give you a list of observers, and a bit of poking around should lead you to the fact that the overreleased observer was for a different property.


In confusing cases, I also find that simply reading the source code is often the quickest way to find the bug. If you read with a critical eye to the object lifetime assumptions the code rests on, you sometimes find that a potential bug jumps right out at you.

KVO bug / counter-intuitive behavior
 
 
Q