文章首发地址(Mr黄黄黄黄黄先森的博客 (thatisawesome.club))
业务背景
想想这样一个业务场景,客户端通过 /api/commit
接口向 Server 发起一个提交任务请求,Server 收到请求之后返回一个提交成功的 Response , 客户端为了获取任务的执行进度,需要每隔一段时间调用 /api/query
接口查询当前任务的执行状态知道任务执行完成。基于此,我们怎样写这样一个轮询请求呢?
基于以上的业务,笔者封装了一个 PHQueryServer
单例对象,该对象内部维护着一个 Timer
和一个浮点型变量 progress
,Timer
每隔 2 秒会随机在 progress
的基础上加 0% - 10% 来模拟 Server 的处理进度, 外部提供了一个
- (void)getCurrentProgressWithCompletion:(void (^)(float currentProgress))completion;
复制代码
接口获取当前进度。
// PHQueryServer.h
#import <Foundation/Foundation.h>
@interface PHQueryServer : NSObject
- (void)getCurrentProgressWithCompletion:(void (^)(float currentProgress))completion;
+ (instancetype)defaultServer;
@end
// PHQueryServer.m
#import "PHQueryServer.h"
@interface PHQueryServer ()
@property (nonatomic, assign, readwrite) float currentProgress;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation PHQueryServer
+ (instancetype)defaultServer {
static PHQueryServer *server = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
server = [[PHQueryServer alloc] init];
[server startProcess];
});
return server;
}
- (void)startProcess {
[self.timer fire];
}
- (NSTimer *)timer {
if (!_timer) {
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf process];
}];
}
return _timer;
}
- (void)process {
// 模拟 Server 处理异步任务
float c = self.currentProgress;
self.currentProgress = c + (arc4random() % 10);
if (self.currentProgress >= 100) {
self.currentProgress = 100;
[self.timer invalidate];
self.timer = nil;
}
}
- (float)currentProgress {
return [@(_currentProgress) floatValue];
}
- (void)getCurrentProgressWithCompletion:(void (^)(float))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟网络发送过程耗时
sleep(arc4random() % 3);
float currentProgress = [self currentProgress];
// 模拟网络接受过程耗时
sleep(arc4random() % 2);
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(currentProgress);
});
}
});
}
@end
复制代码
基于 NSTimer
考虑到需要每隔一段时间去轮询一下,NSTimer
再合适不过了。定时器每隔一段时间,发送一个网络请求,获取到 Response
之后更新 Model
, 如果任务的状态是 Finished
即当前的 progress >= 100
,则 invalidate timer
结束轮询。 Talk is cheap,show me the code.
// PHTimerQueryHelper.h
#import <Foundation/Foundation.h>
typedef void (^PHQueryTimerCallback)(void);
@interface PHTimerQueryHelper : NSObject
- (void)startQueryWithModel:(PHQueryModel *)queryModel
callback:(PHQueryTimerCallback)callback;
@end
// PHTimerQueryHelper.m
#import "PHTimerQueryHelper.h"
#import "PHQueryServer.h"
@interface PHTimerQueryHelper ()
@property (nonatomic, strong) NSTimer *queryTimer;
@property (nonatomic, copy ) PHQueryTimerCallback callback;
@property (nonatomic, strong) PHQueryModel *queryModel;
@end
@implementation PHTimerQueryHelper
- (void)startQueryWithModel:(PHQueryModel *)queryModel
callback:(PHQueryTimerCallback)callback {
_callback = callback;
_queryModel = queryModel;
[self.queryTimer fire];
}
- (NSTimer *)queryTimer {
if (!_queryTimer) {
__weak typeof(self) weakSelf = self;
_queryTimer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
[[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
if (currentProgress > weakSelf.queryModel.progress) {
weakSelf.queryModel.progress = currentProgress;
if (weakSelf.callback) {
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.callback();
});
}
}
// 结束轮询
if (currentProgress >= 100) {
[weakSelf.queryTimer invalidate];
weakSelf.queryTimer = nil;
}
}];
}];
}
return _queryTimer;
}
@end
复制代码
PHQueryServer
会在子线程执行耗时的 sleep()
函数来模拟网络请求耗时,之后在主线程将当前的进度通过 completion
回调给调用方,调用方获取当进度之后再修改 queryModel
的 progress
更新进度,然后回调给 UI 层去更新进度条,UI 层的代码如下
// ViewController.h
#import "ViewController.h"
#import "PHQueryServer.h"
#import "PHTimerQueryHelper.h"
@import Masonry;
@import CHUIPropertyMaker;
@interface ViewController ()
@property (nonatomic, strong) PHQueryModel *queryModel;
@property (nonatomic, strong) PHTimerQueryHelper *helper;
@property (nonatomic, strong) UIView *progressView;
@property (nonatomic, strong) UILabel *progressLabel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self setupViews];
[PHQueryServer defaultServer];
_queryModel = [[PHQueryModel alloc] init];
// 1. 通过 NSTimer 定时器轮询
[self queryByTimer];
}
- (void)setupViews {
UIView *progressBarBgView = [[UIView alloc] init];
[progressBarBgView ch_makeProperties:^(CHViewPropertyMaker *make) {
make.backgroundColor(UIColor.grayColor);
make.superView(self.view);
make.cornerRadius(10);
} constrains:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(self.view).offset(20);
make.right.equalTo(self.view).offset(-20);
make.height.equalTo(@20);
}];
self.progressView = [[UIView alloc] init];
[self.progressView ch_makeProperties:^(CHViewPropertyMaker *make) {
make.backgroundColor(UIColor.greenColor);
make.cornerRadius(10);
make.superView(progressBarBgView);
} constrains:^(MASConstraintMaker *make) {
make.left.bottom.top.equalTo(progressBarBgView);
make.width.equalTo(@0);
}];
self.progressLabel = [[UILabel alloc] init];
[self.progressLabel ch_makeLabelProperties:^(CHLabelPropertyMaker *make) {
make.superView(self.progressView);
make.font([UIFont systemFontOfSize:9]);
make.textColor(UIColor.blueColor);
} constrains:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.progressView);
make.right.equalTo(self.progressView).offset(-10);
make.left.greaterThanOrEqualTo(self.progressView).offset(5);
}];
}
- (void)queryByTimer {
__weak typeof(self) weakSelf = self;
[self.helper startQueryWithModel:self.queryModel callback:^{
[weakSelf updateProgressViewWithProgress:weakSelf.queryModel.progress];
}];
}
- (PHTimerQueryHelper *)helper {
if (!_helper) {
_helper = [[PHTimerQueryHelper alloc] init];
}
return _helper;
}
- (void)updateProgressViewWithProgress:(float)progress {
[UIView animateWithDuration:1 animations:^{
[self.progressView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.top.bottom.equalTo(self.progressView.superview);
make.width.equalTo(self.progressView.superview.mas_width).multipliedBy(progress / 100.0);
}];
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
self.progressLabel.text = [NSString stringWithFormat:@"%.2f", progress];
}];
}
@end
复制代码
使用 Timer 轮询,看似没有问题,但是考虑网络请求是定时触发,可能会导致的问题就是先发的网络请求后回来,例如,0 时刻发送了一条网络请求,3 s 时刻又发送了一条网络请求,3 s 时刻发送的网络请求在 4 s 时刻收到回调,而 0 s 时刻发送的请求 5 s 时刻才收到回调,那么对于先发送后回调的这种网络请求实际是没有意义的,因为 4 s 时刻的回调信息已经是最新的了,5 s 时刻收到的回调信息已经是一个过时的信息。所以在上面的例子用回调的 progress
和当前 queryModel
的 progress
比较,如果大于当前的 progress
才会回调轮询结果。这样显然会浪费一些网络资源,因为发送了一些无意义的请求,其实也有解决办法,就是本地记一个标记上一次的网络请求是否已经回调的变量,如果没有回调,则再下一个 Timer
的回调时不发送网络请求,但这种方法又会导致新的问题。Timer
设置为 3 s 触发一次,如果再 0s 时刻发送了网络请求,但是 4s 时刻才回调,离下一次 Timer
触发还有 2s,这 2s 属于一个空档期,什么也不会做,如此就导致轮询更新不那么及时。
基于异步的 NSOperation
使用 NSOperation
可以在 main
方法中发送网络请求,网络请求回调中更新 Model
, 在 NSOperation
的 completionBlock
中先刷新进度,再判断是已经完成(progress == 100
),如果未完成,则再新建一个 operation
放到串行队列中。
异步的 NSOperation
在 NSOperation
中,当 main
方法执行完成之后,就标志着任务已经执行完成,但网络请求显然是个异步的操作,如此在还没等到网路请求回调的时候,main
方法已经返回了,解决办法:
- 信号量将异步请求变为同步
- 异步
NSOperation
如果使用信号量做同步,在网络请求还未回调的时候,会一直dispatch_semaphore_wait
会阻塞当前线程直到网络请求回调之后 dispatch_semaphore_signal
。
使用异步的 NSOperation
则不会。
// PHQueryOperation.h
@interface PHQueryOperation : NSOperation
- (instancetype)initWithQueryModel:(PHQueryModel *)queryModel;
@end
// PHQueryOperation.m
#import "PHQueryOperation.h"
#import "PHQueryServer.h"
@interface PHQueryOperation()
@property (nonatomic, assign) BOOL ph_isCancelled;
@property (nonatomic, assign) BOOL ph_isFinished;
@property (nonatomic, assign) BOOL ph_isExecuting;
@property (nonatomic, strong) PHQueryModel *queryModel;
@end
@implementation PHQueryOperation
- (instancetype)initWithQueryModel:(PHQueryModel *)queryModel {
if (self = [super init]) {
_queryModel = queryModel;
}
return self;
}
- (void)start {
if (self.ph_isCancelled) {
self.ph_isFinished = YES;
return;
}
self.ph_isExecuting = YES;
[self startQueryTask];
}
- (void)startQueryTask {
__weak typeof(self) weakSelf = self;
[[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
weakSelf.queryModel.progress = currentProgress;
weakSelf.ph_isFinished = YES;
}];
}
- (void)setPh_isFinished:(BOOL)ph_isFinished {
[self willChangeValueForKey:@"isFinished"];
_ph_isFinished = ph_isFinished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setPh_isExecuting:(BOOL)ph_isExecuting {
[self willChangeValueForKey:@"isExecuting"];
_ph_isExecuting = ph_isExecuting;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setPh_isCancelled:(BOOL)ph_isCancelled {
[self willChangeValueForKey:@"isCancelled"];
_ph_isCancelled = ph_isCancelled;
[self didChangeValueForKey:@"isCancelled"];
}
- (BOOL)isFinished {
return _ph_isFinished;
}
- (BOOL)isCancelled {
return _ph_isCancelled;
}
- (BOOL)isExecuting {
return _ph_isExecuting;
}
@end
复制代码
基于 GCD
简单粗暴
- (void)queryByGCD {
__weak typeof(self) weakSelf = self;
[[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
if (currentProgress > weakSelf.queryModel.progress) {
weakSelf.queryModel.progress = currentProgress;
[self updateProgressViewWithProgress:weakSelf.queryModel.progress];
if (currentProgress < 100) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf queryByGCD];
});
}
}
}];
}
复制代码