Skip to content

Instantly share code, notes, and snippets.

@mhuusko5
Created December 5, 2016 19:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhuusko5/32c9bec29ece774501bcc6707371baab to your computer and use it in GitHub Desktop.
Save mhuusko5/32c9bec29ece774501bcc6707371baab to your computer and use it in GitHub Desktop.
KVO with multi-key path where observer is added/removed while non-last-key property is mutated, crashes
/* 2016-12-05 19:29:16.988026 Compass[71668:644974] [General] An uncaught exception was raised
2016-12-05 19:29:16.988060 Compass[71668:644974] [General] Cannot update for observer <TestObserver 0x600000001060> for the key path "nestedObject.nestedField" from <TestObject 0x600000026ee0>, most likely because the value for the key "nestedObject" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the TestObject class.
2016-12-05 19:29:16.988395 Compass[71668:644974] [General] (
0 CoreFoundation 0x00007fffaa71ee7b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fffbf303cad objc_exception_throw + 48
2 CoreFoundation 0x00007fffaa79d99d +[NSException raise:format:] + 205
3 Foundation 0x00007fffac0f5a62 -[NSKeyValueNestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:] + 830
4 Foundation 0x00007fffac0c8e88 NSKeyValueDidChange + 186
5 Foundation 0x00007fffac207c37 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 944
6 Foundation 0x00007fffac08cd1d -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 60
7 Foundation 0x00007fffac0f559b _NSSetObjectValueAndNotify + 261
8 Compass 0x000000010000277f -[TestObserver set] + 255
9 Compass 0x0000000100002866 __19-[TestObserver set]_block_invoke + 38
10 libdispatch.dylib 0x00000001000be6e5 _dispatch_call_block_and_release + 12
11 libdispatch.dylib 0x00000001000b4f5c _dispatch_client_callout + 8
12 libdispatch.dylib 0x00000001000c59c7 _dispatch_queue_override_invoke + 1360
13 libdispatch.dylib 0x00000001000b71d7 _dispatch_root_queue_drain + 671
14 libdispatch.dylib 0x00000001000b6ee8 _dispatch_worker_thread3 + 114
15 libsystem_pthread.dylib 0x000000010012c89a _pthread_wqthread + 1299
16 libsystem_pthread.dylib 0x000000010012c375 start_wqthread + 13
)
2016-12-05 19:29:16.997343 Compass[71668:644974] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot update for observer <TestObserver 0x600000001060> for the key path "nestedObject.nestedField" from <TestObject 0x600000026ee0>, most likely because the value for the key "nestedObject" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the TestObject class.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fffaa71ee7b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fffbf303cad objc_exception_throw + 48
2 CoreFoundation 0x00007fffaa79d99d +[NSException raise:format:] + 205
3 Foundation 0x00007fffac0f5a62 -[NSKeyValueNestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:] + 830
4 Foundation 0x00007fffac0c8e88 NSKeyValueDidChange + 186
5 Foundation 0x00007fffac207c37 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 944
6 Foundation 0x00007fffac08cd1d -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 60
7 Foundation 0x00007fffac0f559b _NSSetObjectValueAndNotify + 261
8 Compass 0x000000010000277f -[TestObserver set] + 255
9 Compass 0x0000000100002866 __19-[TestObserver set]_block_invoke + 38
10 libdispatch.dylib 0x00000001000be6e5 _dispatch_call_block_and_release + 12
11 libdispatch.dylib 0x00000001000b4f5c _dispatch_client_callout + 8
12 libdispatch.dylib 0x00000001000c59c7 _dispatch_queue_override_invoke + 1360
13 libdispatch.dylib 0x00000001000b71d7 _dispatch_root_queue_drain + 671
14 libdispatch.dylib 0x00000001000b6ee8 _dispatch_worker_thread3 + 114
15 libsystem_pthread.dylib 0x000000010012c89a _pthread_wqthread + 1299
16 libsystem_pthread.dylib 0x000000010012c375 start_wqthread + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) */
@interface NestedTestObject : NSObject
@property NSString *nestedField;
@end
@implementation NestedTestObject @end
@interface TestObject : NSObject
@property NSString *field;
@property NestedTestObject *nestedObject;
@end
@implementation TestObject @end
@interface TestObserver : NSObject
@property TestObject *object;
@end
@implementation TestObserver
- (instancetype)init {
self = [super init];
_object = [TestObject new];
// Dummy observer for safety
[_object addObserver:self
forKeyPath:@"nestedObject.nestedField"
options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:observerContext];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self observe];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self set];
});
return self;
}
- (void)set {
if (arc4random_uniform(10) > 5) {
self.object.nestedObject.nestedField = @"no prob";
} else {
self.object.nestedObject = [NestedTestObject new];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self set];
});
}
-(void)observe {
[self.object addObserver:self
forKeyPath:@"nestedObject.nestedField"
options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:observerContext];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self.object removeObserver:self forKeyPath:@"nestedObject.nestedField" context:observerContext];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self observe];
});
});
}
static void *observerContext = &observerContext;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if (context != observerContext) {
return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
// NSLog(@"Changed");
}
@end
@proxi
Copy link

proxi commented May 24, 2019

Hello Mathew; I’ve run into this problem, too. I wonder if you have ever figured out a workaround?

@mhuusko5
Copy link
Author

Hahaha this was a while ago. But yes I came up with a horrendous workaround.

First of all...

static void synchronizeKVOSetter(Class clazz) {
    SEL selector = NSSelectorFromString(@"_changeValueForKeys:count:maybeOldValuesDict:usingBlock:");
    Method method = class_getInstanceMethod(clazz, selector);
    IMP superIMP = method_getImplementation(method);

    IMP newIMP = imp_implementationWithBlock(^(id self, id *keys, unsigned int count, id oldValues, id block) {
        @synchronized(self) {
            ((void (*)(id, SEL, id*, unsigned int, id, id))superIMP)(self, selector, keys, count, oldValues, block);
        }
    });

    class_addMethod(clazz, selector, newIMP, method_getTypeEncoding(method));
}

// e.g. synchronizeKVOSetter([NSObject class]);

If that's not enough, then you also need to make your class's add/remove observer look like this (if you're working with a shared base class, just paste this in, otherwise if you're working with classes outside of your control, you'll need to do some more swizzling)...

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    @synchronized(self) {
        @synchronized ([self valueForKeyPath:[keyPath componentsSeparatedByString:@"."][0]]) {
            [super removeObserver:observer forKeyPath:keyPath];
        }
    }
}

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context {

    @synchronized (self) {
        @synchronized ([self valueForKeyPath:[keyPath componentsSeparatedByString:@"."][0]]) {
            [super addObserver:observer forKeyPath:keyPath options:options context:context];
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment