Created
December 5, 2016 19:33
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 |
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
Hello Mathew; I’ve run into this problem, too. I wonder if you have ever figured out a workaround?