網絡請求優化之取消請求

圖片來之網絡

頁面返回的時候,將網絡請求取消git

同一個請求屢次請求時,短期忽略相同的請求github

同一個請求屢次請求時,取消以前發出的請求web

發送的請求,屢次嘗試並確保成功數組

最近發現不少網絡請求都有能夠優化的地方,雖然開發和測試都沒有發現問題,可是可讓代碼更加的優雅。想到了有四個方面能夠優化,親測有效。緩存

1. 頁面返回的時候取消網絡請求

在一個界面進行多個請求的時候,而有可能用戶立刻點擊了返回按鈕,那麼若是是使用了AFNetworking的狀況,此時ViewController不會立刻銷燬,須要等到網絡請求返回並執行完畢block後纔會銷燬此ViewController。 那麼會存在2個問題:bash

  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的實現爲:session

@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;記錄。ide

1.3 View的添加請求

若是是在View中發起的請求,那麼須要根據View來獲取所在的ViewController。建立BaseView,讓發起請求的View繼承BaseView,在BaseView中實現添加記錄請求的方法。實現以下:oop

#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,可是發現一直沒有完成,都是零零星星的知識點。

相關文章
相關標籤/搜索