- (void) testStartingOnBackgroundThread { XCDYouTubeVideoOperation *operation = [[XCDYouTubeVideoOperation alloc] initWithVideoIdentifier:nil languageIdentifier:nil]; [self keyValueObservingExpectationForObject:operation keyPath:@"isFinished" handler:^BOOL(id observedObject,NSDictionary *change) { XCTAssertNil([observedObject video]); XCTAssertNotNil([observedObject error]); return YES; }]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{ XCTAssertFalse([NSThread isMainThread]); [operation start]; }); [self waitForExpectationsWithTimeout:5 handler:nil]; }
当我在Mac上本地运行它时,此测试总是通过,但有时它会因此错误而发生在fails on Travis:
Failed: caught “NSRangeException”,“Cannot remove an observer <_XCKVOExpectation 0x1001846c0> for the key path “isFinished” from <XCDYouTubeVideoOperation 0x1001b9510> because it is not registered as an observer.”
难道我做错了什么?
解决方法
当您调用keyValueObservingExpectationForObject:keyPath:handler:时,会在引擎盖下创建_XCKVOExpectation对象.它负责观察您传递的对象/ keyPath.触发KVO通知后,将调用_safelyUnregister方法,这是删除观察者的位置.这是_safelyUnregister方法的(逆向工程)实现.
@implementation _XCKVOExpectation - (void) _safelyUnregister { if (!self.hasUnregistered) { [self.observedObject removeObserver:self forKeyPath:self.keyPath]; self.hasUnregistered = YES; } } @end
在waitForExpectationsWithTimeout:handler:的末尾再次调用此方法,并且在释放_XCKVOExpectation对象时.请注意,操作在后台线程上终止,但测试在主线程上运行.所以你有一个竞争条件:如果在后台线程上将hasUnregistered属性设置为YES之前在主线程上调用了_safelyUnregister,则会删除观察者两次,导致无法删除观察者异常.
因此,为了解决此问题,您必须使用锁保护_safelyUnregister方法.这是一个代码片段供您在测试目标中进行编译,该代码片段将负责修复此错误.
#import <objc/runtime.h> __attribute__((constructor)) void WorkaroundXCKVOExpectationUnregistrationRaceCondition(void); __attribute__((constructor)) void WorkaroundXCKVOExpectationUnregistrationRaceCondition(void) { SEL _safelyUnregisterSEL = sel_getUid("_safelyUnregister"); Method safelyUnregister = class_getInstanceMethod(objc_lookUpClass("_XCKVOExpectation"),_safelyUnregisterSEL); void (*_safelyUnregisterIMP)(id,SEL) = (__typeof__(_safelyUnregisterIMP))method_getImplementation(safelyUnregister); method_setImplementation(safelyUnregister,imp_implementationWithBlock(^(id self) { @synchronized(self) { _safelyUnregisterIMP(self,_safelyUnregisterSEL); } })); }
编辑
这个bug已经是fixed in Xcode 7 beta 4了.