You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 07:20:36