网络请求优化之取消请求

图片来之网络

页面返回的时候,将网络请求取消
同一个请求多次请求时,短时间忽略相同的请求
同一个请求多次请求时,取消之前发出的请求
发送的请求,多次尝试并确保成功

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

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

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

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

1.1 记录所有的请求

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

#pragma mark - Cancel Task

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

/** 移除已经请求成功的请求
 * 在请求完成的block中,添加移除的操作
 */
- (void)removeSessionDataTask:(NSURLSessionDataTask *)task;

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

BaseViewController.m的实现为:

@property (nonatomic, strong) NSMutableArray<NSURLSessionDataTask *> *sessionDataTaskMArr;

#pragma mark - Cancel Task

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

    [self.sessionDataTaskMArr addObject:task];
}

/** 移除已经请求成功的请求 */
- (void)removeSessionDataTask:(NSURLSessionDataTask *)task
{
    [self.sessionDataTaskMArr removeObject:task];
}

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

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

    [self.sessionDataTaskMArr removeAllObjects];
}

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

    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;

/** 移除已经请求成功的请求
 * 在请求完成的block中,添加移除的操作
 */
- (void)removeSessionDataTask:(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];
    }
}

/** 移除已经请求成功的请求
 * 在请求完成的block中,添加移除的操作
 */
- (void)removeSessionDataTask:(NSURLSessionDataTask *)task
{
    UIViewController *currentVC = self.rootViewController;

    if ([currentVC isKindOfClass:[HXSBaseViewController class]]) {
        [(HXSBaseViewController *)currentVC removeSessionDataTask: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) NSMutableDictionary *requestTimeMDic;
@property (nonatomic, strong) NSMutableDictionary *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

- (NSMutableDictionary *)requestTimeMDic
{
    if (nil == _requestTimeMDic) {
        _requestTimeMDic = [[NSMutableDictionary alloc] initWithCapacity:5];
    }

    return _requestTimeMDic;
}

- (NSMutableDictionary *)cancelTaskMDic
{
    if (nil == _cancelTaskMDic) {
        _cancelTaskMDic = [[NSMutableDictionary alloc] initWithCapacity:5];
    }

    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,但是发现一直没有完成,都是零零星星的知识点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容