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

4,782 阅读5分钟

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

业务背景

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

基于以上的业务,笔者封装了一个 PHQueryServer 单例对象,该对象内部维护着一个 Timer 和一个浮点型变量 progressTimer 每隔 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回调给调用方,调用方获取当进度之后再修改 queryModelprogress 更新进度,然后回调给 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 和当前 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 则不会。

// 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];
                    });
                }
            }
    }];
}