页面返回的时候,将网络请求取消
同一个请求多次请求时,短时间忽略相同的请求
同一个请求多次请求时,取消之前发出的请求
发送的请求,多次尝试并确保成功
最近发现很多网络请求都有可以优化的地方,虽然开发和测试都没有发现问题,但是可以让代码更加的优雅。想到了有四个方面可以优化,亲测有效。
1. 页面返回的时候取消网络请求
在一个界面进行多个请求的时候,而有可能用户马上点击了返回按钮,那么如果是使用了AFNetworking的情况,此时ViewController不会马上销毁,需要等到网络请求返回并执行完毕block后才会销毁此ViewController。
那么会存在2个问题:
网络请求返回的数据没有使用,浪费流量。
ViewController销毁延迟,内存不能及时释放。
1.1 记录所有的请求
将页面中进行的所有请求记录,包括controller和view中发起的请求,当然设计为不是强制的,而是通过根据业务选择添加。采用BaseViewController的方式,每一个ViewController都需要继承BaseViewController,然后添加添加请求和取消请求的方法。
#pragma mark - Cancel Task /** 记录将需要在退出VC取消的请求。 * 在记录的时候,清理已经请求完成的task * 如果请求需要有取消功能,那么在failure的block中,需要添加对取消的失败不做任务处理的实现。 */ - (void)addSessionDataTask:(NSURLSessionDataTask *)task; /** 取消所有的请求 */ - (void)cancelAllSessionDataTask;
BaseViewController.m的实现为:
@property (nonatomic, strong) NSMutableArray*sessionDataTaskMArr; #pragma mark - Cancel Task /** 将需要在退出VC取消的请求,记录。 * 在记录的时候,清理已经请求完成的task */ - (void)addSessionDataTask:(NSURLSessionDataTask *)task { NSMutableArray *tempMArr = [[NSMutableArray alloc] initWithCapacity:5]; for (NSURLSessionDataTask *dataTask in self.sessionDataTaskMArr) { if (NSURLSessionTaskStateCompleted == dataTask.state) { [tempMArr addObject:dataTask]; } } [self.sessionDataTaskMArr removeObjectsInArray:tempMArr]; [self.sessionDataTaskMArr addObject:task]; } /** 取消所有的请求 */ - (void)cancelAllSessionDataTask { 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;
BaseView.m的实现为:
@property (nonatomic, strong) 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) 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; }
思路很简单,将请求的url和参数进行一次MD5记录,然后将时间作为值。
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*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,但是发现一直没有完成,都是零零星星的知识点。
作者:蝴蝶之梦天使
链接:https://www.jianshu.com/p/20f6172524d6