頁面返回的時候,將網路請求取消
同一個請求多次請求時,短時間忽略相同的請求
同一個請求多次請求時,取消之前發出的請求
傳送的請求,多次嘗試並確保成功
最近發現很多網路請求都有可以優化的地方,雖然開發和測試都沒有發現問題,但是可以讓程式碼更加的優雅。想到了有四個方面可以優化,親測有效。
1. 頁面返回的時候取消網路請求
在一個介面進行多個請求的時候,而有可能使用者馬上點選了返回按鈕,那麼如果是使用了AFNetworking的情況,此時ViewController不會馬上銷燬,需要等到網路請求返回並執行完畢block後才會銷燬此ViewController。 那麼會存在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,但是發現一直沒有完成,都是零零星星的知識點。