"iOS development" how to write a poll gracefully

Posted by Beans on Mon, 10 Jan 2022 05:46:07 +0100

Article starting address( Mr Huang xiansen's blog (thatisawesome.club))

Business background

Think of a business scenario where the client initiates a task submission request to the Server through the / api/commit interface, and the Server returns a successful Response after receiving the request
, in order to obtain the execution progress of the task, the client needs to call the / api/query interface every once in a while to query the execution status of the current task until the task is completed. Based on this, how do we write such a polling request?

Based on the above business, the author encapsulates a PHQueryServer singleton object, which internally maintains a Timer and a floating-point variable progress. The Timer randomly adds 0% - 10% to the progress every 2 seconds to simulate the processing progress of the Server. An external

- (void)getCurrentProgressWithCompletion:(void (^)(float currentProgress))completion;

Interface to get the current progress.

// 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 {
    // Simulate Server processing asynchronous tasks
    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), ^{
        // Simulation network sending process takes time
        sleep(arc4random() % 3);
        float currentProgress = [self currentProgress];
        // Simulating the network acceptance process takes time
        sleep(arc4random() % 2);
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(currentProgress);
            });
        }
    });
}
@end

Based on NSTimer

Considering the need to poll at regular intervals, NSTimer is perfect. Every once in a while, the timer sends a network request and updates the Model after obtaining the Response. If the status of the task is Finished, that is, the current progress > = 100, the invalidate timer ends the polling.
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();
                        });
                    }
                }
                // End polling
                if (currentProgress >= 100) {
                    [weakSelf.queryTimer invalidate];
                    weakSelf.queryTimer = nil;
                }
            }];
        }];
    }
    return _queryTimer;
}
@end

PHQueryServer will execute the time-consuming sleep() function in the sub thread to simulate the time-consuming network request. Then, the main thread will call back the current progress to the caller through completion. After obtaining the current progress, the caller will modify the progress of queryModel to update the progress, and then call back to the UI layer to update the progress bar. The code of the UI layer is as follows

// 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. Polling through NSTimer timer
    [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

There seems to be no problem with using Timer polling, but considering that the network request is triggered regularly, the possible problem is that the first network request comes back. For example, a network request is sent at time 0, another network request is sent at time 3 s, and the network request sent at time 3 s receives a callback at time 4 s, The request sent at 0 s time receives the callback only at 5 s time, so the network request sent first and then called back is actually meaningless, because the callback information at 4 s time is already the latest, and the callback information received at 5 s time is already an outdated information. Therefore, in the above example, the progress of the callback is compared with the progress of the current queryModel. If it is greater than the current progress, the polling result will be recalled. This will obviously waste some network resources, because some meaningless requests are sent. In fact, there is a solution, that is, locally record a variable indicating whether the last network request has been recalled. If there is no callback, the network request will not be sent when the next Timer callback, but this method will lead to new problems. The Timer is set to trigger once every 3 seconds. If the network request is sent at the time of 0s, but the callback is only made at the time of 4s, there are still 2s before the next Timer trigger. These 2s belong to a gap period and nothing will be done, which will lead to the delay of polling update.

Asynchronous based NSOperation

Using NSOperation, you can send a network request in the main method, update the Model in the network request callback, refresh the progress in the completionBlock of NSOperation, and then judge whether it has been completed (progress == 100). If it has not been completed, create a new operation and put it in the serial queue.

Asynchronous NSOperation

In NSOperation, when the main method is completed, it indicates that the task has been completed, but the network request is obviously an asynchronous operation. Therefore, the main method has returned before the network request callback. The solution is as follows:

  • Semaphores make asynchronous requests synchronous
  • Asynchronous NSOperation

If a semaphore is used for synchronization, it will always dispatch when the network request has not been recalled_ semaphore_ Wait blocks the current thread until the network request is called back_ semaphore_ signal.

Using asynchronous nsoperations does not.

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

Based on GCD

Simple and rough

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

Topics: iOS xcode objective-c