如何在Objective-C中等待无回调的异步方法执行完成?
问题:无回调异步方法的测试等待策略
现有Objective-C方法:
- (void)performFoo:(int)bar;
该方法内部通过dispatch调用后台线程,随后在主线程入队动画更新bar属性,但未提供完成回调。编写测试代码时:
- (void)testFoo { [Klass performFoo:2]; XCTAssertTrue([Klass barValue], 2); }
由于performFoo:会立即返回,此时barValue尚未更新,断言常失败或不稳定。在无法修改performFoo:代码的前提下,有哪些最优等待策略?
此前尝试过[NSRunloop runUntilDate:]定时等待,但时间设置不合理会导致问题,是否有KVO等更优方案?
推荐解决方案
1. KVO结合XCTestExpectation(推荐)
利用XCTest自带的期望机制,结合KVO监听barValue的变化,当值更新为目标值时触发期望,避免无意义的等待:
- (void)testFoo { // 创建测试期望 XCTestExpectation *expectation = [self expectationWithDescription:@"barValue更新为2"]; // 监听barValue的KVO [Klass addObserver:self forKeyPath:@"barValue" options:NSKeyValueObservingOptionNew context:(__bridge void * _Nullable)(expectation)]; // 调用目标方法 [Klass performFoo:2]; // 等待期望触发,设置合理超时时间(比如2秒,足够动画完成) [self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) { [Klass removeObserver:self forKeyPath:@"barValue"]; if (error) { NSLog(@"等待超时: %@", error); } }]; // 断言最终值 XCTAssertEqual([Klass barValue], 2); } // 实现KVO回调方法 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"barValue"]) { NSNumber *newValue = change[NSKeyValueChangeNewKey]; if ([newValue intValue] == 2) { XCTestExpectation *expectation = (__bridge XCTestExpectation *)context; [expectation fulfill]; } } }
注意:确保barValue是KVO兼容的属性(比如用@property声明,且内部通过setBarValue:修改),否则KVO无法触发。
2. 循环检查+RunLoop运行(轻量方案)
如果KVO不可用,可以主动循环检查barValue的状态,同时每次循环让RunLoop运行一小段时间,避免阻塞测试线程:
- (void)testFoo { [Klass performFoo:2]; NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:2]; BOOL valueUpdated = NO; while (!valueUpdated && [[NSDate date] compare:timeoutDate] == NSOrderedAscending) { if ([Klass barValue] == 2) { valueUpdated = YES; break; } // 让RunLoop运行0.01秒,处理主线程的动画和任务 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; } XCTAssertTrue(valueUpdated, @"barValue未在超时时间内更新为2"); XCTAssertEqual([Klass barValue], 2); }
这种方式比固定时长等待更高效,一旦值更新就立即结束等待,同时设置超时避免无限循环。
3. 监听主线程任务队列(进阶方案)
如果动画是通过UIView动画或dispatch_async(dispatch_get_main_queue(), ...)执行的,可以通过监听主线程的任务完成情况来等待。利用iOS 10+可用的dispatch_main_queue_has_pending_work()判断主线程队列是否有未执行的任务:
- (void)testFoo { [Klass performFoo:2]; NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:2]; BOOL done = NO; while (!done && [[NSDate date] compare:timeoutDate] == NSOrderedAscending) { // 检查是否还有未处理的主线程任务,同时判断目标值是否更新 BOOL hasPendingTasks = dispatch_main_queue_has_pending_work(); if (!hasPendingTasks && [Klass barValue] == 2) { done = YES; break; } [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; } XCTAssertEqual([Klass barValue], 2); }
内容的提问来源于stack exchange,提问作者gran_profaci
相关产品推荐
相关产品推荐

