在一般的需求中,我们会使用异步请求来进行数据交换,等待数据返回之后再进行回调操作,执行所需要的操作.这种方式的好处是,不需要阻塞线程来等待请求结果。但是在一些特殊的场景中我们需要使用同步等待数据的方式来获取数据,例如阿里云的oss中获取token就是这种需求.
在iOS9.0之前的版本中,我们可以NSURLConnection发送同步请求来获取到数据,
+ (nullable NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse * _Nullable * _Nullable)response error:(NSError **)error API_DEPRECATED("Use [NSURLSession dataTaskWithRequest:completionHandler:] (see NSURLSession.h", macos(10.3,10.11), ios(2.0,9.0), tvos(9.0,9.0)) __WATCHOS_PROHIBITED;
但是在iOS9.0之后的版本中,NSURLConnection逐渐被NSURLSession替代,原来的同步接口在渐渐淡出视野.那么使用NSURLSession如果实现同步请求呢?下面以AFN为例,提供两种实现思路.
使用信号量
信号量可以理解为是一个资源计数器,对信号量有两个操作来达到互斥,分别是P和V操作。 一般情况是这样进行临界访问或互斥访问的: 设信号量值为1, 当一个进程A运行时,使用资源,进行P操作,即对信号量值减1,也就是资源数少了1个。这时信号量值为0。系统中规定当信号量值为0是,必须等待,直到信号量值不为零才能继续操作。 这时如果进程B想要运行,那么也必须进行P操作,但是此时信号量为0,所以无法减1,即不能P操作,也就阻塞。这样就达到了进程A的排他访问。 当进程A运行结束后,释放资源,进行V操作。资源数重新加1,这时信号量的值变为1,这时进程B发现资源数不为0,信号量能进行P操作了,立即执行P操作。信号量值又变为0,进程B有资源,其余线程必须等到,达到线程B的排他访问。 这就是信号量来控制线程互斥的原理。
在iOS中,信号量的实现依赖以下函数:
dispatch_semaphore_create(long value); // 创建一个semaphore dispatch_semaphore_signal(dispatch_semaphore_t dsema); // 发送一个信号 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等到信号
第一个函数有一个长整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,便可以实现一个"同步的请求":
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; __block id _responseObject = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { _responseObject = responseObject; dispatch_semaphore_signal(semaphore); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { _responseObject = nil; dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"获取到结果:%@", _responseObject);
这里有一个小问题需要注意一下:dispatch_semaphore_wait这个方法是会阻塞线程的,而AFN的回调代码默认是在主队列中进行的,所以这个操作如果放在主线程中会造成死锁:dispatch_semaphore_wait阻塞了主线程,需要等待信号来解除线程阻塞, 在线程阻塞接触之前,该线程不能继续执行任务.但是如果回调也在主队列中,由于主线程已经被阻塞,所以回调中的操作永远不会执行,造成主线程被永久阻塞.
所以解决方案也有两种:
- 将该操作放在非主线程中进行:例如将请求放在自定义队列中
dispatch_queue_t queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_async(queue, ^{ AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; __block id _responseObject = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { _responseObject = responseObject; dispatch_semaphore_signal(semaphore); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { _responseObject = nil; dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"获取到结果:%@", _responseObject); });
- 设置回调操作队列为非主队列:
manager.completionQueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT);
使用锁
在多线程开发中,锁是专门用来控制资源竞争访问权限的工具,以确保在同一时间最多只能有一个线程对资源进行访问,以确保资源的完整性.因此,我们也可以使用锁来实现"同步"请求操作.
这里我们选择NSCondition锁来做实现.NSCondition锁有两个主要的功能:锁定和监测.在添加锁时可以监测是否满足指定条件,如果不满足可以使用wait方法阻塞线程,等待signal或者broadcast信号,收到信号后会再次尝试加锁,如果加锁成功,线程阻塞解除,否则继续阻塞.
@interface NSCondition : NSObject <NSLocking> { @private void *_priv; } - (void)wait; - (BOOL)waitUntilDate:(NSDate *)limit; - (void)signal; - (void)broadcast; @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
使用NSCondition来实现同步的代码大概这个样子:
NSCondition *lock = [[NSCondition alloc] init]; __block NSDictionary *_responseObject = nil; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [lock lock]; _responseObject = responseObject; [lock signal]; [lock unlock]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [lock lock]; _responseObject = @{}; [lock signal]; [lock unlock]; }]; [lock lock]; if (!_responseObject) { [lock wait]; } [lock unlock]; NSLog(@"responseObject == %@", _responseObject);
同样地,尽量不要将该请求操作放在主线程中,这样回调线程(主线程)会通过发送信号激活阻塞当前线程;如果非要放在主线程中,那就需要设置请求的回调(其实是发送信号的操作)在其他线程中执行.
也可以使用NSConditionLock来进行实现:
dispatch_queue_t queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_async(queue, ^{ NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0]; __block NSDictionary *_responseObject = nil; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [lock lock]; _responseObject = responseObject; [lock unlockWithCondition:1]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [lock lock]; _responseObject = @{}; [lock unlockWithCondition:1]; }]; [lock lockWhenCondition:1]; NSLog(@"responseObject == %@", _responseObject); [lock unlock]; });