我正在测试一些使用 Grand Central Dispatch 进行异步处理的代码。测试代码如下所示:
[object runSomeLongOperationAndDo:^{
STAssert…
}];
测试必须等待操作完成。我目前的解决方案如下所示:
__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
STAssert…
finished = YES;
}];
while (!finished);
哪个看起来有点粗糙,你知道更好的方法吗?我可以公开队列,然后通过调用 dispatch_sync
来阻止:
[object runSomeLongOperationAndDo:^{
STAssert…
}];
dispatch_sync(object.queue, ^{});
…但这可能在 object
上暴露太多。
尝试使用 dispatch_semaphore
。它应该看起来像这样:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object runSomeLongOperationAndDo:^{
STAssert…
dispatch_semaphore_signal(sema);
}];
if (![NSThread isMainThread]) {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}
}
即使 runSomeLongOperationAndDo:
确定操作实际上没有足够长的时间值得线程化并改为同步运行,这也应该正确运行。
除了在其他答案中详尽介绍的信号量技术之外,我们现在可以使用 Xcode 6 中的 XCTest 通过 XCTestExpectation
执行异步测试。这消除了在测试异步代码时对信号量的需求。例如:
- (void)testDataTask
{
XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];
NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error, @"dataTaskWithURL error %@", error);
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
}
XCTAssert(data, @"data nil");
// do additional tests on the contents of the `data` object here, if you want
// when all done, Fulfill the expectation
[expectation fulfill];
}];
[task resume];
[self waitForExpectationsWithTimeout:10.0 handler:nil];
}
为了未来的读者,虽然调度信号量技术在绝对需要时是一种极好的技术,但我必须承认,我看到太多不熟悉良好异步编程模式的新开发人员很快就将信号量作为实现异步的通用机制例程同步运行。更糟糕的是,我看到他们中的许多人在主队列中使用这种信号量技术(我们永远不应该阻塞生产应用程序中的主队列)。
我知道这里不是这种情况(发布此问题时,没有像 XCTestExpectation
这样的好工具;此外,在这些测试套件中,我们必须确保测试在异步调用完成之前不会完成) .这是可能需要阻塞主线程的信号量技术的罕见情况之一。
因此,我向这个原始问题的作者表示歉意,信号量技术对谁来说是合理的,我向所有看到这种信号量技术并考虑在他们的代码中应用它作为处理异步的一般方法的新开发人员写这个警告方法:请注意,十分之九,信号量技术在遇到异步操作时并不是最好的方法。相反,请熟悉完成块/闭包模式,以及委托协议模式和通知。这些通常是处理异步任务的更好方法,而不是使用信号量使它们同步运行。通常有充分的理由将异步任务设计为异步行为,因此使用正确的异步模式而不是试图使它们同步行为。
NSOperationQueue
上安排下载。除非我使用信号量之类的东西,否则文档下载 NSOperation
将立即全部完成,并且不会有任何真正的下载排队——它们几乎会同时进行,这是我不希望的。信号量在这里合理吗?或者有没有更好的方法让 NSOperations 等待其他人的异步结束?或者是其他东西?
AFHTTPRequestOperation
对象的操作队列,那么您应该只创建一个完成操作(您将依赖于其他操作)。或者使用调度组。顺便说一句,您说您不希望它们同时运行,如果这是您需要的,那很好,但是您要按顺序而不是同时执行此操作会付出严重的性能损失。我通常使用 4 或 5 的 maxConcurrentOperationCount
。
我最近再次来到这个问题,并在 NSObject
上写了以下类别:
@implementation NSObject (Testing)
- (void) performSelector: (SEL) selector
withBlockingCallback: (dispatch_block_t) block
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self performSelector:selector withObject:^{
if (block) block();
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
}
@end
这样,我可以轻松地将带有回调的异步调用转换为测试中的同步调用:
[testedObject performSelector:@selector(longAsyncOpWithCallback:)
withBlockingCallback:^{
STAssert…
}];
通常不使用这些答案中的任何一个,它们通常不会扩展(当然,这里和那里都有例外)
这些方法与 GCD 的预期工作方式不兼容,最终会导致死锁和/或通过不间断轮询杀死电池。
换句话说,重新排列您的代码,以便没有同步等待结果,而是处理通知状态更改的结果(例如回调/委托协议、可用、离开、错误等)。 (如果你不喜欢回调地狱,这些可以重构为块。)因为这是如何将真实行为暴露给应用程序的其余部分,而不是将其隐藏在虚假外观后面。
相反,请使用 NSNotificationCenter,为您的类定义带有回调的自定义委托协议。如果您不喜欢到处乱搞委托回调,请将它们包装到一个具体的代理类中,该类实现自定义协议并将各种块保存在属性中。可能还提供便利的构造函数。
最初的工作稍微多一些,但从长远来看,它将减少可怕的竞争条件和电池谋杀轮询的数量。
(不要问一个例子,因为它是微不足道的,我们也必须花时间学习 Objective-c 基础知识。)
这是一个不使用信号量的绝妙技巧:
dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
[object doSomething];
});
dispatch_sync(serialQ, ^{ });
您所做的是使用带有空块的 dispatch_sync
等待在串行调度队列上同步等待,直到 A-Synchronous 块完成。
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
NSParameterAssert(perform);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
perform(semaphore);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
}
示例用法:
[self performAndWait:^(dispatch_semaphore_t semaphore) {
[self someLongOperationWithSuccess:^{
dispatch_semaphore_signal(semaphore);
}];
}];
还有 SenTestingKitAsync 可让您编写如下代码:
- (void)testAdditionAsync {
[Calculator add:2 to:2 block^(int result) {
STAssertEquals(result, 4, nil);
STSuccess();
}];
STFailAfter(2.0, @"Timeout");
}
(有关详细信息,请参阅 objc.io article。)从 Xcode 6 开始,XCTest
上有一个 AsynchronousTesting
类别,可让您编写如下代码:
XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
[somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];
这是我的一个测试的替代方法:
__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];
STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
success = value != nil;
[completed lock];
[completed signal];
[completed unlock];
}], nil);
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);
-waitUntilDate:
的 NSCondition
documentation“您必须在调用此方法之前锁定接收器。”所以 -unlock
应该在 -waitUntilDate:
之后。
斯威夫特 4:
创建远程对象时使用 synchronousRemoteObjectProxyWithErrorHandler
而不是 remoteObjectProxy
。不再需要信号量。
下面的示例将返回从代理接收到的版本。如果没有 synchronousRemoteObjectProxyWithErrorHandler
,它将崩溃(尝试访问不可访问的内存):
func getVersion(xpc: NSXPCConnection) -> String
{
var version = ""
if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
{
helper.getVersion(reply: {
installedVersion in
print("Helper: Installed Version => \(installedVersion)")
version = installedVersion
})
}
return version
}
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
// ... your code to execute
dispatch_semaphore_signal(sema);
}];
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}
这为我做到了。
有时,超时循环也很有帮助。你可以等到你从异步回调方法中得到一些(可能是 BOOL)信号,但是如果没有响应,你想跳出那个循环怎么办?下面是解决方案,上面大部分都回答了,但增加了超时。
#define CONNECTION_TIMEOUT_SECONDS 10.0
#define CONNECTION_CHECK_INTERVAL 1
NSTimer * timer;
BOOL timeout;
CCSensorRead * sensorRead ;
- (void)testSensorReadConnection
{
[self startTimeoutTimer];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {
/* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
if (sensorRead.isConnected || timeout)
dispatch_semaphore_signal(sema);
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];
};
[self stopTimeoutTimer];
if (timeout)
NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);
}
-(void) startTimeoutTimer {
timeout = NO;
[timer invalidate];
timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
-(void) stopTimeoutTimer {
[timer invalidate];
timer = nil;
}
-(void) connectionTimeout {
timeout = YES;
[self stopTimeoutTimer];
}
问题的非常原始的解决方案:
void (^nextOperationAfterLongOperationBlock)(void) = ^{
};
[object runSomeLongOperationAndDo:^{
STAssert…
nextOperationAfterLongOperationBlock();
}];
在运行我的方法之前,我必须等到加载 UIWebView,我能够通过使用 GCD 结合该线程中提到的信号量方法对主线程执行 UIWebView 就绪检查来完成此工作。最终代码如下所示:
-(void)myMethod {
if (![self isWebViewLoaded]) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block BOOL isWebViewLoaded = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (!isWebViewLoaded) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
isWebViewLoaded = [self isWebViewLoaded];
});
[NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s
}
dispatch_sync(dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(semaphore);
});
});
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}
}
}
//Run rest of method here after web view is loaded
}
不定期副业成功案例分享
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
替换dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);