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,來防止這個新建的執行緒由於沒有活動直接退出