2

网络请求优化之取消请求

 2 years ago
source link: https://juejin.cn/post/6844903736968478727
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


图片来之网络

页面返回的时候,将网络请求取消

同一个请求多次请求时,短时间忽略相同的请求

同一个请求多次请求时,取消之前发出的请求

发送的请求,多次尝试并确保成功

最近发现很多网络请求都有可以优化的地方,虽然开发和测试都没有发现问题,但是可以让代码更加的优雅。想到了有四个方面可以优化,亲测有效。

1. 页面返回的时候取消网络请求

在一个界面进行多个请求的时候,而有可能用户马上点击了返回按钮,那么如果是使用了AFNetworking的情况,此时ViewController不会马上销毁,需要等到网络请求返回并执行完毕block后才会销毁此ViewController。 那么会存在2个问题:

  1. 网络请求返回的数据没有使用,浪费流量。
  2. ViewController销毁延迟,内存不能及时释放。

1.1 记录所有的请求

将页面中进行的所有请求记录,包括controller和view中发起的请求,当然设计为不是强制的,而是通过根据业务选择添加。采用BaseViewController的方式,每一个ViewController都需要继承BaseViewController,然后添加添加请求和取消请求的方法。 多谢zl520k的建议,当网络请求完成后,主动将请求从记录中移除,减少返回时的循环遍历操作。感谢喵渣渣提供的NSPointerArray

#pragma mark - Cancel Task

/** 记录将需要在退出VC取消的请求。
 *  在记录的时候,清理已经请求完成的task
 *  如果请求需要有取消功能,那么在failure的block中,需要添加对取消的失败不做任务处理的实现。
 */
- (void)addSessionDataTask:(NSURLSessionDataTask *)task;

/** 取消所有的请求 */
- (void)cancelAllSessionDataTask;

复制代码

BaseViewController.m的实现为:

@property (nonatomic, strong) NSPointerArray *sessionDataTaskMArr;

#pragma mark - Cancel Task

/** 将需要在退出VC取消的请求,记录。
 *  在记录的时候,清理已经请求完成的task
 */
- (void)addSessionDataTask:(NSURLSessionDataTask *)task
{
    if (nil == task) {
        return;
    }

    [self.sessionDataTaskMArr compact];

    [self.sessionDataTaskMArr addPointer:(__bridge void * _Nullable)(task)];
}

/** 取消所有的请求 */
- (void)cancelAllSessionDataTask
{
    if (0 >= [self.sessionDataTaskMArr count]) {
        return;
    }

    [self.sessionDataTaskMArr compact];

    for (NSURLSessionDataTask *dataTask in self.sessionDataTaskMArr) {
        if (NSURLSessionTaskStateRunning == dataTask.state
            || NSURLSessionTaskStateSuspended == dataTask.state) {
            [dataTask cancel];
        }
    }
   
    [self.sessionDataTaskMArr compact];
}

- (NSPointerArray *)sessionDataTaskMArr
{
    if (nil == _sessionDataTaskMArr) {
        _sessionDataTaskMArr = [NSPointerArray weakObjectsPointerArray];
    }

    return _sessionDataTaskMArr;
}

复制代码

1.2 ViewController添加请求

在ViewController发起的请求,那么直接将请求返回的NSURLSessionDataTask,调用BaseViewController的- (void)addSessionDataTask:(NSURLSessionDataTask *)task;记录。

1.3 View的添加请求

如果是在View中发起的请求,那么需要根据View来获取所在的ViewController。创建BaseView,让发起请求的View继承BaseView,在BaseView中实现添加记录请求的方法。实现如下:

#pragma mark - Cancel Task

/** 记录将需要在退出VC取消的请求。
 *  在记录的时候,清理已经请求完成的task
 *  如果请求需要有取消功能,那么在failure的block中,需要添加对取消的失败不做任务处理的实现。
 */
- (void)addSessionDataTask:(NSURLSessionDataTask *)task;

复制代码

BaseView.m的实现为:

@property (nonatomic, weak) UIViewController *rootViewController;

#pragma mark - Cancel Task

/** 将需要在退出VC取消的请求,记录。
 *  在记录的时候,清理已经请求完成的task
 */
- (void)addSessionDataTask:(NSURLSessionDataTask *)task
{
    UIViewController *currentVC = self.rootViewController;

    if ([currentVC isKindOfClass:[HXSBaseViewController class]]) {
        [(HXSBaseViewController *)currentVC addSessionDataTask:task];
    }
}


#pragma mark - Private

- (UIViewController *)rootViewController
{
    if (nil == _rootViewController) {
        for (UIView *next = [self superview]; next; next = next.superview) {
            UIResponder *nextResponder = [next nextResponder];
            if ([nextResponder isKindOfClass:[UIViewController class]]) {
                _rootViewController = (UIViewController *)nextResponder;

                return _rootViewController;
            }
        }
    }

    return _rootViewController;
}


复制代码

1.3 取消所有请求

viewController的消失,分为dismiss和pop两种情况,所以在BaseViewController中,添加取消请求:


#pragma mark - Override Methods

- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion NS_AVAILABLE_IOS(5_0)
{
    [self cancelAllSessionDataTask];

    [super dismissViewControllerAnimated:flag completion:completion];
}

复制代码

然后需要实现一个BaseNavigationController来重载pop的3个方法,并对所有的viewController进行取消请求,如下:


- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    // 取消请求
    UIViewController *viewController = [super popViewControllerAnimated:animated];
    if ([viewController isKindOfClass:[HXSBaseViewController class]]) {
        [(HXSBaseViewController *)viewController cancelAllSessionDataTask];
    }

    return viewController;
}
- (nullable NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    NSArray *viewControllerVCs = [super popToViewController:viewController animated:animated];

    for (UIViewController *vc in viewControllerVCs) {
        if ([vc isKindOfClass:[HXSBaseViewController class]]) {
            [(HXSBaseViewController *)vc cancelAllSessionDataTask];
        }
    }

    return viewControllerVCs;
}
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated
{
    NSArray *viewControllerVCs = [super popToRootViewControllerAnimated:animated];

    for (UIViewController *vc in viewControllerVCs) {
        if ([vc isKindOfClass:[HXSBaseViewController class]]) {
            [(HXSBaseViewController *)vc cancelAllSessionDataTask];
        }
    }

    return viewControllerVCs;
}


复制代码

Done,取消网络请求搞定。使用这样的实现方式是为了避免修改之前的代码,可以做到零侵入。可以对需要添加的ViewController进行添加。 *注意:*取消请求的返回需要进行特殊处理。

2. 同一个请求多次请求时,短时间忽略相同的请求

当进行刷新操作时,如果在请求还没有返回之前,一直在刷新操作,不管是狂点还是乱点。那么第一个请求发出后,短时间内可以不进行重复请求。 代码实现见下面的BaseViewModel。

3. 同一个请求多次请求时,取消之前发出的请求

如果是在搜索操作,那么每次输入关键字的时候,之前发出的请求可以取消,仅仅显示最后的请求结果。 采用的方法为创建一个BaseViewModel,所有的请求操作继承BaseViewModel,在发起请求之前进行一次判断。代码如下:

#pragma mark - 忽略请求

/** 忽略请求,当请求的url和参数都是一样的时候,在短时间内不发起再次请求, 默认3秒 */
- (BOOL)ignoreRequestWithUrl:(NSString *)url params:(NSDictionary *)params;

/** 忽略请求,当请求的url和参数都是一样的时候,在短时间内不发起再次请求 */
- (BOOL)ignoreRequestWithUrl:(NSString *)url params:(NSDictionary *)params timeInterval:(NSTimeInterval)timeInterval;


#pragma mark - 取消之前的请求

/** 取消之前的同一个url的网络请求
 *  在failure分支中,判断如果是取消操作,那么不做任何处理
 *  在success和failure分支中,都要调用clearTaskSessionWithUrl:方法,进行内存释放
 */
- (void)cancelLastTaskSessionWithUrl:(NSString *)url currentTaskSession:(NSURLSessionTask *)task;

/** 清除url绑定的sessionTask */
- (void)clearTaskSessionWithUrl:(NSString *)url;

复制代码

BaseViewModel.m的实现:

@property (nonatomic, strong) NSMapTable *requestTimeMDic;
@property (nonatomic, strong) NSMapTable *cancelTaskMDic;

- (BOOL)ignoreRequestWithUrl:(NSString *)url params:(NSDictionary *)params
{
    return [self ignoreRequestWithUrl:url params:params timeInterval:kRequestTimeInterval];
}

- (BOOL)ignoreRequestWithUrl:(NSString *)url params:(NSDictionary *)params timeInterval:(NSTimeInterval)timeInterval
{
    NSString *requestStr = [NSString stringWithFormat:@"%@%@", url, [params uq_URLQueryString]];
    NSString *requestMD5 = [NSString md5:requestStr];
    NSTimeInterval nowTime = [[NSDate date] timeIntervalSince1970];
    NSNumber *lastTimeNum = [self.requestTimeMDic objectForKey:requestMD5];

    WS(weakSelf);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 超过忽略时间后,将值清空
        [weakSelf.requestTimeMDic removeObjectForKey:requestMD5];
    });


    if (timeInterval < (nowTime - [lastTimeNum doubleValue])) {
        if (0.01 > [lastTimeNum doubleValue]) {
            [self.requestTimeMDic setObject:@(nowTime) forKey:requestMD5];
        }

        return NO;
    } else {
        return YES;
    }
}

- (void)cancelLastTaskSessionWithUrl:(NSString *)url currentTaskSession:(NSURLSessionTask *)task
{
    NSURLSessionTask *lastSessionTask = [self.cancelTaskMDic objectForKey:url];

    if (nil == lastSessionTask) {
        [self.cancelTaskMDic setObject:task forKey:url];

        return;
    }

    [lastSessionTask cancel];
}

- (void)clearTaskSessionWithUrl:(NSString *)url
{
    [self.cancelTaskMDic removeObjectForKey:url];
}




#pragma mark - Remove Unused Things


#pragma mark - Private Methods


#pragma mark - Getter Methods

- (NSMapTable *)requestTimeMDic
{
    if (nil == _requestTimeMDic) {
        _requestTimeMDic = [NSMapTable weakToWeakObjectsMapTable];
    }

    return _requestTimeMDic;
}

- (NSMapTable *)cancelTaskMDic
{
    if (nil == _cancelTaskMDic) {
        _cancelTaskMDic = [NSMapTable weakToWeakObjectsMapTable];
    }

    return _cancelTaskMDic;
}

复制代码

思路很简单,self.requestTimeMDic字典里记录的内容中,key:为请求的url和参数进行一次MD5计算得到的结果作为key,value:为发生的时间。那么就知道url和参数的发生时间,与当前时间进行判断是否相同的请求发生时间过短,过短就放弃这次请求。

4. 发送的请求,多次尝试并确保成功

需要确保请求成功,并且有可能页面已经摧毁。那么请求需要加入到单例中,在单例中进行多次请求。添加一个网络是否可用的判断,当网络不能使用时,暂停尝试。 设计的再完美一点,就是(1)做本地化缓存.(2)添加一个成功后的反馈这个看业务需求吧。 先创建一个Model类,用来记录申请的请求参数。

@interface HXWebServiceRequestModel : HXBaseJSONModel

/** 重试的剩余次数 */
@property (nonatomic, assign) NSInteger times;

/** 请求类型 */
@property (nonatomic, assign) RequestType requestType;

/** 请求url */
@property (nonatomic, strong) NSString *urlStr;

/** 请求参数 */
@property (nonatomic, strong) NSDictionary *params;

/** upload时的数组 */
@property (nonatomic, strong) NSArray *formDataArray;

/** 是否在请求 */
@property (nonatomic, assign) BOOL isRequesting;

@end

@implementation HXWebServiceRequestModel

@end

复制代码

WebServiceManager代码如下:

/** 重试的次数,默认为3次 */
@property (nonatomic, assign) NSUInteger maxRetryTimes;

/** 创建单例,可以在界面消失后,继续执行 */
+ (instancetype)shareInstace;

/** 将执行的请求保存,进行多次重试,指导成功 */
- (void)requestWithType:(RequestType)type url:(NSString *)url params:(NSDictionary *)param formDataArray:(NSArray *)formDataArray;

复制代码

WebServicemanager.m的实现:

static NSTimeInterval kTimeInterval = 3.0;

@property (nonatomic, strong) NSTimer *timer;

@property (nonatomic, strong) NSMutableArray<HXWebServiceRequestModel *> *requestMArr;


+ (instancetype)shareInstace
{
    static HXWebServiceManager *webServiceManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        webServiceManager = [[HXWebServiceManager alloc] init];

        webServiceManager.maxRetryTimes = 3;

        [webServiceManager initialNetwork];
    });

    return webServiceManager;
}

- (void)requestWithType:(RequestType)type url:(NSString *)url params:(NSDictionary *)param formDataArray:(NSArray *)formDataArray
{
    HXWebServiceRequestModel *model = [[HXWebServiceRequestModel alloc] init];
    model.times = self.maxRetryTimes;
    model.requestType = type;
    model.urlStr = url;
    model.params = param;
    model.formDataArray = formDataArray;
    model.isRequesting = NO;

    [self.requestMArr addObject:model];

    if (![self.timer isValid]) {
        [self.timer fire];
    }
}



#pragma mark - Initial Methods

- (void)initialNetwork
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(networkChanged:)
                                                 name:AFNetworkingReachabilityDidChangeNotification
                                               object:nil];
}

- (void)networkChanged:(NSNotification *)notification
{
    NSNumber *status = [notification.userInfo objectForKey:AFNetworkingReachabilityNotificationStatusItem];

    if (AFNetworkReachabilityStatusNotReachable == [status integerValue]) {
        if (self.timer.isValid) {
            self.timer.fireDate = [NSDate distantFuture];
        }
    } else {
        if (![self.timer isValid]) {
            [self.timer fire];
        } else {
            self.timer.fireDate = [NSDate date];
        }
    }
}


#pragma mark - Target Methods

- (void)requestNetwork
{
    if (0 >= [self.requestMArr count]
        || ![[AFNetworkReachabilityManager sharedManager] isReachable]) {
        [self.timer invalidate];
        self.timer = nil;

        return;
    }

    for (HXWebServiceRequestModel *model in self.requestMArr) {
        [self requestWithModel:model];
    }
}

- (void)requestWithModel:(HXWebServiceRequestModel *)model
{
    if (model.isRequesting) {
        return;
    }

    WS(weakSelf);
    switch (model.requestType) {
        case kRequestTypeGet:
        {
            [HXQWebService getRequest:model.urlStr
                           parameters:model.params
                             progress:nil
                              success:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                  model.isRequesting = NO;

                                  if (status == kNoError
                                      || 0 >= model.times) {
                                      [weakSelf.requestMArr removeObject:model];
                                  }

                              } failure:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                  model.isRequesting = NO;
                              }];
        }
            break;

        case kRequestTypePut:
        {
            [HXQWebService putRequest:model.urlStr
                           parameters:model.params
                              success:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                  model.isRequesting = NO;

                                  if (status == kNoError
                                      || 0 >= model.times) {
                                      [weakSelf.requestMArr removeObject:model];
                                  }
                              } failure:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                  model.isRequesting = NO;
                              }];
        }
            break;

        case kRequestTypePost:
        {
            [HXQWebService postRequest:model.urlStr
                            parameters:model.params
                              progress:nil
                               success:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                   model.isRequesting = NO;

                                   if (status == kNoError
                                       || 0 >= model.times) {
                                       [weakSelf.requestMArr removeObject:model];
                                   }
                               } failure:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                   model.isRequesting = NO;
                               }];
        }
            break;

        case kRequestTypeUpload:
        {
            [HXQWebService uploadRequest:model.urlStr
                              parameters:model.params
                           formDataArray:model.formDataArray
                                progress:nil
                                 success:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                     model.isRequesting = NO;

                                     if (status == kNoError
                                         || 0 >= model.times) {
                                         [weakSelf.requestMArr removeObject:model];
                                     }
                                 } failure:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                     model.isRequesting = NO;
                                 }];
        }
            break;

        case kRequestTypeDelete:
        {
            [HXQWebService deleteRequest:model.urlStr
                              parameters:model.params
                                 success:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                     model.isRequesting = NO;

                                     if (status == kNoError
                                         || 0 >= model.times) {
                                         [weakSelf.requestMArr removeObject:model];
                                     }
                                 } failure:^(ErrorCode status, NSString *msg, NSDictionary *data) {
                                     model.isRequesting = NO;
                                 }];
        }
            break;

        default:
            break;
    }

    model.isRequesting = YES;
    model.times = (0 < model.times--) ?:0;
}



#pragma mark - Getter Methods

- (NSTimer *)timer
{
    if (nil == _timer) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:kTimeInterval target:self selector:@selector(requestNetwork) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }

    return _timer;
}

- (NSMutableArray *)requestMArr
{
    if (nil == _requestMArr) {
        _requestMArr = [[NSMutableArray alloc] initWithCapacity:5];
    }

    return _requestMArr;
}

复制代码

用到的HTTP宏定义:

typedef NS_ENUM(NSInteger, RequestType) {
    kRequestTypeGet         = 0,
    kRequestTypePost        = 1,
    kRequestTypeUpload      = 2,
    kRequestTypePut         = 3,
    kRequestTypeDelete      = 4,
};

复制代码

需要保证请求成功,那么直接调用这个方法就可以,并且直接返回成功。

PS: 代码中用到了很多项目封装的类,可以查看给我留言,也可以看看我的Github

// END 做一个记录,很希望能够将自己的记录做成一个Kit,但是发现一直没有完成,都是零零星星的知识点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK