iOS网络请求优化之取消请求

释放双眼,带上耳机,听听看~!

 

在这里插入图片描述

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

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

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;
/** 移除已经请求成功的请求
* 在请求完成的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, strong) UIViewController *rootViewController;
#pragma mark - Cancel Task
/** 将需要在退出VC取消的请求,记录。
*  在记录的时候,清理已经请求完成的task
*/
- (void)addSessionDataTask:(NSURLSessionDataTask *)task
{
UIViewController *currentVC = self.rootViewController;
if ([currentVC isKindOfClass:[HQBaseViewController class]]) {
[(HQBaseViewController *)currentVC addSessionDataTask:task];
}
}
/** 移除已经请求成功的请求
* 在请求完成的block中,添加移除的操作
*/
- (void)removeSessionDataTask:(NSURLSessionDataTask *)task
{
UIViewController *currentVC = self.rootViewController;
if ([currentVC isKindOfClass:[HQBaseViewController class]]) {
[(HQBaseViewController *)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.4 取消所有请求

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:[HQBaseViewController class]]) {
[(HQBaseViewController *)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:[HQBaseViewController class]]) {
[(HQBaseViewController *)vc cancelAllSessionDataTask];
}
}
return viewControllerVCs;
}
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated
{
NSArray *viewControllerVCs = [super popToRootViewControllerAnimated:animated];
for (UIViewController *vc in viewControllerVCs) {
if ([vc isKindOfClass:[HQBaseViewController class]]) {
[(HQBaseViewController *)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<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

人已赞赏
iOS文章

iOS自适应Cell的高度缓存

2020-5-4 0:46:49

iOS文章

iOS所有线程Autorelease对象的内存管理

2020-5-4 2:02:00

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索