1. 程式人生 > >iOS如何在頁面銷燬時取消網路請求

iOS如何在頁面銷燬時取消網路請求

一,說明

轉載自http://blog.csdn.net/u010124617
大家都知道,當一個網路請求發出去之後,如果不管不顧,有可能出現以下情況:
進入某個頁面,做了某種操作(退出頁面、切換某個tab等等)導致之前的請求變成無用請求,這時候有可能出現雖然頁面已經銷燬了,但是網路請求還在外面飛的情況,如果放任不管,那麼這個請求既浪費流量,又浪費效能,尤其是在網路比較差時,一個超時的無用請求更讓人不爽。這時候,我們最好的辦法是cancel掉這些無用的請求。

二,問題描述

傳統的cancel方式是這樣的:
1.在類裡面需要持有請求物件
@property (strong/weak, nonatomic) XXRequest *xxrequest1;
屬性具體用strong還是weak取決於你的網路層設計,有些網路層request是完全的臨時變數,出了方法就直接銷燬的需要用strong,有些設計則具有自持有的特性,請求結束前不會銷燬的可以用weak。
2.在請求發起的地方,賦值請求

xxrequest1 = xxx;
self.xxrequest1 = xxrequest1;
[xxrequest1 start];

3.在需要銷燬的地方,一般是本類的dealloc裡面

[self.xxrequest1 cancel];

可以看到為了cancel一個request,我們的請求物件到處都是,如果再來幾個請求,那處理起來就更噁心了。。

有沒有什麼方式可以讓我們省心省力呢?

三,解決

目標:

我們希望可以控制一部分請求,在頁面銷燬、manager釋放等時機,自動的cancel掉我們發出去的請求,而不需要我們手動去寫上面這種到處都是的程式碼

方案:

監聽類的dealloc方法呼叫,當dealloc執行時,順帶著執行下request的cancel方法

很快,我們就發現了問題:
ARC下不允許hook類的dealloc方法,所以hook是不行的。那還有別的方式可以知道一個類被dealloc了嗎?

其實我們可以採用一些變通的方案得到,我們知道associated繫結的屬性,是可以根據繫結時的設定,在dealloc時自動釋放的,所以我們可以利用這一點做到監聽dealloc呼叫:

構建一箇中間類A,該類在銷燬執行dealloc時,順便執行請求的cancel方法
通過associate繫結的方式,將銷燬類繫結到任意執行類B上
這樣,當執行類B銷燬時,銷燬內部的associate的屬性時,我們就可以得到相應的執行時機。
下面給出核心程式碼:

建立用於cancel請求的類:

@interface YRWeakRequest : NSObject
@property (weak, nonatomic) id request;
@end
@implementation YRWeakRequest
@end
@interface YRDeallocRequests : NSObject
@property (strong, nonatomic) NSMutableArray<YRWeakRequest*> *weakRequests;
@property (strong, nonatomic) NSLock *lock;
@end
@implementation YRDeallocRequests
- (instancetype)init{
    if (self = [super init]) {
        _weakRequests = [NSMutableArray arrayWithCapacity:20];
        _lock = [[NSLock alloc]init];
    }
    return self;
}
- (void)addRequest:(YRWeakRequest*)request{
    if (!request||!request.request) {
        return;
    }
    [_lock lock];
    [self.weakRequests addObject:request];
    [_lock unlock];
}
- (void)clearDeallocRequest{
    [_lock lock];
    NSInteger count = self.weakRequests.count;
    for (NSInteger i=count-1; i>0; i--) {
        YRWeakRequest *weakRequest = self.weakRequests[i];
        if (!weakRequest.request) {
            [self.weakRequests removeObject:weakRequest];
        }
    }
    [_lock unlock];
}
- (void)dealloc{
    for (YRWeakRequest *weakRequest in _weakRequests) {
        [weakRequest.request cancel];
    }
}
@end

3.對任意類繫結該中間類

@implementation NSObject (YRRequest)

- (YRDeallocRequests *)deallocRequests{
    YRDeallocRequests *requests = objc_getAssociatedObject(self, _cmd);
    if (!requests) {
        requests = [[YRDeallocRequests alloc]init];
        objc_setAssociatedObject(self, _cmd, requests, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return requests;
}

- (void)autoCancelRequestOnDealloc:(id)request{
    [[self deallocRequests] clearDeallocRequest];
    YRWeakRequest *weakRequest = [[YRWeakRequest alloc] init];
    weakRequest.request = request;
    [[self deallocRequests] addRequest:weakRequest];
}
@end

4.對外暴露的標頭檔案

@interface NSObject (YRRequest)

/*!
 *  @brief  add request to auto cancel when obj dealloc
 *  @note   will call request's cancel method , so the request must have cancel method..
 */
- (void)autoCancelRequestOnDealloc:(id)request;

@end

使用方式
怎麼樣,看標頭檔案是不是覺得很簡單,使用方式就很簡單了,
比如說我們需要在某個VC裡,釋放時自動cancel網路請求:

//請求發起的地方:

xxrequest1 = xxx;
[xxrequest1 start];
[self autoCancelRequestOnDealloc:xxrequest1];

好了,從此不再擔心該類銷燬時請求亂飛了。

四,備註:

1.我的實現類裡面,預設呼叫的是cancel方法,所以理論上,所有帶有cancel方法的request都可以直接用這個方法呼叫(如AFNetworking、NSURLSessionTask等等)
2.有些人會說,我是用自己的網路層,自己封裝的requset發起的請求,不呼叫cancel,自己封裝的物件也會銷燬的;我要提醒的是,有可能你自己封裝的物件銷燬了,但是其下層,無論對接的是AF還是系統的,又或者是其他的請求庫,一定是具有自持有性質的,如果不這麼說,風險在於資料返回前底層的請求就會銷燬掉,一般不會有人這麼設計的。
3.例子中我繫結的是self,其實還可以繫結到任意物件上,比如某個類的內部屬性等等,這樣可以根據業務需求進一步控制請求的cancel時機