1. 程式人生 > >AFNetworking3.0後為什麼不再需要常駐執行緒?

AFNetworking3.0後為什麼不再需要常駐執行緒?

最近在補原始碼閱讀方面的短板,第一個選擇的就是AFNetworking,一方面AF的編碼思路、程式碼質量都屬於開源框架的上乘;另一方面也可以藉機溫習一下網路方面的東西。

 

AF原始碼解析的系列文章有很多(文末有我看過的一些推薦給大家),本文不對AF作全面的解析,僅從常駐執行緒這個角度解析一下2.0和3.0的差異。

 

AF2.x為什麼需要常駐執行緒?

 

NSURLConnection

 

先來看看 NSURLConnection 傳送請求時的執行緒情況,NSURLConnection 是被設計成非同步傳送的,呼叫了start方法後,NSURLConnection 會新建一些執行緒用底層的 CFSocket 去傳送和接收請求,在傳送和接收的一些事件發生後通知原來執行緒的Runloop去回撥事件。

 

大概有三種方法使用NSURLConnection

 

A.在主執行緒非同步回撥

 

若直接在主執行緒非同步回撥會存在兩個問題:

 

[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 

 

1、當在主執行緒呼叫上面的初始化方法時,監聽回撥的任務會加入到主執行緒的 Runloop 下,主執行緒的Runloop預設的 RunloopMode 是 NSDefaultRunLoopMode。當用戶滑動 scrollview 時,RunloopMode會切換到 NSEventTrackingRunLoopMode 模式,這個時候回撥函式就不會執行了,直到使用者停止滑動。

 

這個問題可以通過如下方法來解決:

 

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; 
//設定 RunloopMode 為 NSRunLoopCommonModes(即使使用者滑動 scrollview 也能即時執行回撥函式)
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
[connection start];

 

2、作為網路層框架,在 NSURLConnection 回調回來之後,必定要對 Response 做一些諸如序列化、錯誤處理的操作的。這些通用操作勢必要放在子執行緒去做掉,接著回到主執行緒,框架的使用者只需要拿到處理後的 Response 進行UI 重新整理即可。(PASS)

 

B.一個請求一條執行緒

 

來一個請求開闢一條執行緒,設定runloop保活執行緒,等待結果回撥。這種方式理論上是可行的,但是你也看到了,執行緒開銷太大了。(PASS)

 

C.一條常駐執行緒

 

只開闢一條子執行緒,設定runloop使執行緒常駐。所有的請求在這個執行緒上發起、同時也在這個執行緒上回調。

 

那有人會問:那網路請求豈不是變成了單執行緒?

 

//networkRequestThread即常駐執行緒
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

 

- (void)operationDidStart {
    [self.lock lock];
    if (![self isCancelled]) {
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        for (NSString *runLoopMode in self.runLoopModes) {
            [self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
            [self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
        }
        [self.outputStream open];
        [self.connection start];
    }
    [self.lock unlock];
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
    });
}

 

首先,每一個請求對應一個AFHTTPRequestOperation例項物件(以下簡稱operation),每一個operation在初始化完成後都會被新增到一個NSOperationQueue中。

 

由這個NSOperationQueue來控制併發,系統會根據當前可用的核心數以及負載情況動態地調整最大的併發 operation 數量,我們也可以通過setMaxConcurrentoperationCount:方法來設定最大併發數。注意:併發數並不等於所開闢的執行緒數。具體開闢幾條執行緒由系統決定。

 

也就是說此處執行operation是併發的、多執行緒的。

 

 

經過上面ABC方案的分析,最後再來小結一下為什麼AF2.x需要一條常駐執行緒:

 

首先需要在子執行緒去start connection,請求傳送後,所在的子執行緒需要保活以保證正常接收到 NSURLConnectionDelegate 回撥方法。如果每來一個請求就開一條執行緒,並且保活執行緒,這樣開銷太大了。所以只需要保活一條固定的執行緒,在這個執行緒裡發起請求、接收回調。

 

AF3.x為什麼不再需要常駐執行緒?

 

標題寫的是“AFNetworking3.0後為什麼不再需要常駐執行緒?”,然而卻花了大半的篇幅解析了AF2.x為什麼需要常駐執行緒?EXO ME??

 

其實明白了AF2.x為什麼需要常駐執行緒之後,再看一下AF3.x,很快就能知道答案了~

 

NSURLConnection的一大痛點就是:發起請求後,這條執行緒並不能隨風而去,而需要一直處於等待回撥的狀態。

 

蘋果也是明白了這一痛點,從iOS9.0開始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。當然NSURLSession還解決了很多其他的問題,這裡不作贅述。

 

self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

 

為什麼說NSURLSession解決了NSURLConnection的痛點,從上面的程式碼可以看出,NSURLSession發起的請求,不再需要在當前執行緒進行代理方法的回撥!可以指定回撥的delegateQueue,這樣我們就不用為了等待代理回撥方法而苦苦保活執行緒了。

 

同時還要注意一下,指定的用於接收回調的Queue的maxConcurrentOperationCount設為了1,這裡目的是想要讓併發的請求序列的進行回撥。

 

為什麼要序列回撥?

 

- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
    NSParameterAssert(task);
    AFURLSessionManagerTaskDelegate *delegate = nil;
    [self.lock lock];
    //給所要訪問的資源加鎖,防止造成資料混亂
    delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
    [self.lock unlock];
    return delegate;
}

 

這邊對 self.mutableTaskDelegatesKeyedByTaskIdentifier 的訪問進行了加鎖,目的是保證多執行緒環境下的資料安全。既然加了鎖,就算maxConcurrentOperationCount不設為1,當某個請求正在回撥時,下一個請求還是得等待一直到上個請求獲取完所要的資源後解鎖,所以這邊併發回撥也是沒有意義的。相反多task回撥導致的多執行緒併發,還會導致效能的浪費。

 

 

補充1:

 

AF3.x會給每個 NSURLSessionTask 繫結一個 AFURLSessionManagerTaskDelegate ,這個TaskDelegate相當於把NSURLSessionDelegate進行了一層過濾,最終只保留類似didCompleteWithError這樣對上層呼叫者輸出的回撥。

 

- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
//此處程式碼進行了大量刪減,只是為了讓大家清楚的看到這個方法做的最重要的事
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
            if (self.completionHandler) {
                self.completionHandler(task.response, responseObject, error);
            }
    }
}

 

補充2:

 

面試官可能會問你:為什麼AF3.0中需要設定

 

self.operationQueue.maxConcurrentOperationCount = 1;

 

而AF2.0卻不需要?

 

這個問題不難,但是卻可以幫助面試官判斷面試者是否真的認真研讀了AF的兩個大版本的原始碼。

 

解答:功能不一樣:AF3.0的operationQueue是用來接收NSURLSessionDelegate回撥的,鑑於一些多執行緒資料訪問的安全性考慮,設定了maxConcurrentOperationCount = 1來達到序列回撥的效果。

 

而AF2.0的operationQueue是用來新增operation並進行併發請求的,所以不要設定為1。

 

- (AFHTTPRequestOperation *)POST:(NSString *)URLString
                      parameters:(id)parameters
                         success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                         failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
    [self.operationQueue addOperation:operation];
    return operation;
}

 

補充3:AF中常駐執行緒的實現(經典案例,不作贅述)

 

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

 

首先用NSThread建立了一個執行緒,並且這個執行緒是個單例。

 

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

 

新建的子執行緒預設是沒有新增Runloop的,因此給這個執行緒添加了一個runloop,並且加了一個NSMachPort,來防止這個新建的執行緒由於沒有活動直接退出