『iOS开发』如何优雅地写一个轮询

语言: CN / TW / HK

文章首发地址(Mr黄黄黄黄黄先森的博客 (thatisawesome.club))

业务背景

想想这样一个业务场景,客户端通过 /api/commit 接口向 Server 发起一个提交任务请求,Server 收到请求之后返回一个提交成功的 Response , 客户端为了获取任务的执行进度,需要每隔一段时间调用 /api/query 接口查询当前任务的执行状态知道任务执行完成。基于此,我们怎样写这样一个轮询请求呢?

基于以上的业务,笔者封装了一个 PHQueryServer 单例对象,该对象内部维护着一个 Timer 和一个浮点型变量 progressTimer 每隔 2 秒会随机在 progress 的基础上加 0% - 10% 来模拟 Server 的处理进度, 外部提供了一个

objc - (void)getCurrentProgressWithCompletion:(void (^)(float currentProgress))completion; 接口获取当前进度。

```objc // PHQueryServer.h

import

@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.

```objc // PHTimerQueryHelper.h

import

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回调给调用方,调用方获取当进度之后再修改queryModelprogress` 更新进度,然后回调给 UI 层去更新进度条,UI 层的代码如下

```objc // 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 和当前 queryModelprogress 比较,如果大于当前的 progress 才会回调轮询结果。这样显然会浪费一些网络资源,因为发送了一些无意义的请求,其实也有解决办法,就是本地记一个标记上一次的网络请求是否已经回调的变量,如果没有回调,则再下一个 Timer 的回调时不发送网络请求,但这种方法又会导致新的问题。Timer 设置为 3 s 触发一次,如果再 0s 时刻发送了网络请求,但是 4s 时刻才回调,离下一次 Timer 触发还有 2s,这 2s 属于一个空档期,什么也不会做,如此就导致轮询更新不那么及时。

基于异步的 NSOperation

使用 NSOperation 可以在 main 方法中发送网络请求,网络请求回调中更新 Model, 在 NSOperationcompletionBlock 中先刷新进度,再判断是已经完成(progress == 100),如果未完成,则再新建一个 operation 放到串行队列中。

异步的 NSOperation

NSOperation 中,当 main 方法执行完成之后,就标志着任务已经执行完成,但网络请求显然是个异步的操作,如此在还没等到网路请求回调的时候,main 方法已经返回了,解决办法: * 信号量将异步请求变为同步 * 异步 NSOperation

如果使用信号量做同步,在网络请求还未回调的时候,会一直dispatch_semaphore_wait 会阻塞当前线程直到网络请求回调之后 dispatch_semaphore_signal

使用异步的 NSOperation 则不会。

```objc // 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

简单粗暴 objc - (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]; }); } } }]; }