最近在補(bǔ)源碼閱讀方面的短板,第一個選擇的就是AFNetworking,一方面AF的編碼思路、代碼質(zhì)量都屬于開源框架的上乘;另一方面也可以借機(jī)溫習(xí)一下網(wǎng)絡(luò)方面的東西。
AF源碼解析的系列文章有很多(文末有我看過的一些推薦給大家),本文不對AF作全面的解析,僅從常駐線程這個角度解析一下2.0和3.0的差異。
AF2.x為什么需要常駐線程?
NSURLConnection
先來看看 NSURLConnection 發(fā)送請求時的線程情況,NSURLConnection 是被設(shè)計(jì)成異步發(fā)送的,調(diào)用了start方法后,NSURLConnection 會新建一些線程用底層的 CFSocket 去發(fā)送和接收請求,在發(fā)送和接收的一些事件發(fā)生后通知原來線程的Runloop去回調(diào)事件。
大概有三種方法使用NSURLConnection
A.在主線程異步回調(diào)
若直接在主線程異步回調(diào)會存在兩個問題:
[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]
1、當(dāng)在主線程調(diào)用上面的初始化方法時,監(jiān)聽回調(diào)的任務(wù)會加入到主線程的 Runloop 下,主線程的Runloop默認(rèn)的 RunloopMode 是 NSDefaultRunLoopMode。當(dāng)用戶滑動 scrollview 時,RunloopMode會切換到 NSEventTrackingRunLoopMode 模式,這個時候回調(diào)函數(shù)就不會執(zhí)行了,直到用戶停止滑動。
這個問題可以通過如下方法來解決:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
//設(shè)置 RunloopMode 為 NSRunLoopCommonModes(即使用戶滑動 scrollview 也能即時執(zhí)行回調(diào)函數(shù))
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
2、作為網(wǎng)絡(luò)層框架,在 NSURLConnection 回調(diào)回來之后,必定要對 Response 做一些諸如序列化、錯誤處理的操作的。這些通用操作勢必要放在子線程去做掉,接著回到主線程,框架的使用者只需要拿到處理后的 Response 進(jìn)行UI 刷新即可。(PASS)
B.一個請求一條線程
來一個請求開辟一條線程,設(shè)置runloop?;罹€程,等待結(jié)果回調(diào)。這種方式理論上是可行的,但是你也看到了,線程開銷太大了。(PASS)
C.一條常駐線程
只開辟一條子線程,設(shè)置runloop使線程常駐。所有的請求在這個線程上發(fā)起、同時也在這個線程上回調(diào)。
那有人會問:那網(wǎng)絡(luò)請求豈不是變成了單線程?
//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];
});
}
首先,每一個請求對應(yīng)一個AFHTTPRequestOperation實(shí)例對象(以下簡稱operation),每一個operation在初始化完成后都會被添加到一個NSOperationQueue中。
由這個NSOperationQueue來控制并發(fā),系統(tǒng)會根據(jù)當(dāng)前可用的核心數(shù)以及負(fù)載情況動態(tài)地調(diào)整最大的并發(fā) operation 數(shù)量,我們也可以通過setMaxConcurrentoperationCount:方法來設(shè)置最大并發(fā)數(shù)。注意:并發(fā)數(shù)并不等于所開辟的線程數(shù)。具體開辟幾條線程由系統(tǒng)決定。
也就是說此處執(zhí)行operation是并發(fā)的、多線程的。

經(jīng)過上面ABC方案的分析,最后再來小結(jié)一下為什么AF2.x需要一條常駐線程:
首先需要在子線程去start connection,請求發(fā)送后,所在的子線程需要保活以保證正常接收到 NSURLConnectionDelegate 回調(diào)方法。如果每來一個請求就開一條線程,并且?;罹€程,這樣開銷太大了。所以只需要?;钜粭l固定的線程,在這個線程里發(fā)起請求、接收回調(diào)。
AF3.x為什么不再需要常駐線程?
標(biāo)題寫的是“AFNetworking3.0后為什么不再需要常駐線程?”,然而卻花了大半的篇幅解析了AF2.x為什么需要常駐線程?EXO ME??
其實(shí)明白了AF2.x為什么需要常駐線程之后,再看一下AF3.x,很快就能知道答案了~
NSURLConnection的一大痛點(diǎn)就是:發(fā)起請求后,這條線程并不能隨風(fēng)而去,而需要一直處于等待回調(diào)的狀態(tài)。
蘋果也是明白了這一痛點(diǎn),從iOS9.0開始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。當(dāng)然NSURLSession還解決了很多其他的問題,這里不作贅述。
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
為什么說NSURLSession解決了NSURLConnection的痛點(diǎn),從上面的代碼可以看出,NSURLSession發(fā)起的請求,不再需要在當(dāng)前線程進(jìn)行代理方法的回調(diào)!可以指定回調(diào)的delegateQueue,這樣我們就不用為了等待代理回調(diào)方法而苦苦?;罹€程了。
同時還要注意一下,指定的用于接收回調(diào)的Queue的maxConcurrentOperationCount設(shè)為了1,這里目的是想要讓并發(fā)的請求串行的進(jìn)行回調(diào)。
為什么要串行回調(diào)?
- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
NSParameterAssert(task);
AFURLSessionManagerTaskDelegate *delegate = nil;
[self.lock lock];
//給所要訪問的資源加鎖,防止造成數(shù)據(jù)混亂
delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
[self.lock unlock];
return delegate;
}
這邊對 self.mutableTaskDelegatesKeyedByTaskIdentifier 的訪問進(jìn)行了加鎖,目的是保證多線程環(huán)境下的數(shù)據(jù)安全。既然加了鎖,就算maxConcurrentOperationCount不設(shè)為1,當(dāng)某個請求正在回調(diào)時,下一個請求還是得等待一直到上個請求獲取完所要的資源后解鎖,所以這邊并發(fā)回調(diào)也是沒有意義的。相反多task回調(diào)導(dǎo)致的多線程并發(fā),還會導(dǎo)致性能的浪費(fèi)。

補(bǔ)充1:
AF3.x會給每個 NSURLSessionTask 綁定一個 AFURLSessionManagerTaskDelegate ,這個TaskDelegate相當(dāng)于把NSURLSessionDelegate進(jìn)行了一層過濾,最終只保留類似didCompleteWithError這樣對上層調(diào)用者輸出的回調(diào)。
- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
//此處代碼進(jìn)行了大量刪減,只是為了讓大家清楚的看到這個方法做的最重要的事
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);
}
}
}
補(bǔ)充2:
面試官可能會問你:為什么AF3.0中需要設(shè)置
self.operationQueue.maxConcurrentOperationCount = 1;
而AF2.0卻不需要?
這個問題不難,但是卻可以幫助面試官判斷面試者是否真的認(rèn)真研讀了AF的兩個大版本的源碼。
解答:功能不一樣:AF3.0的operationQueue是用來接收NSURLSessionDelegate回調(diào)的,鑒于一些多線程數(shù)據(jù)訪問的安全性考慮,設(shè)置了maxConcurrentOperationCount = 1來達(dá)到串行回調(diào)的效果。
而AF2.0的operationQueue是用來添加operation并進(jìn)行并發(fā)請求的,所以不要設(shè)置為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;
}
補(bǔ)充3:AF中常駐線程的實(shí)現(xiàn)(經(jīng)典案例,不作贅述)
+ (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創(chuàng)建了一個線程,并且這個線程是個單例。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
新建的子線程默認(rèn)是沒有添加Runloop的,因此給這個線程添加了一個runloop,并且加了一個NSMachPort,來防止這個新建的線程由于沒有活動直接退出。
作者:dj_rose
鏈接:http://www.itdecent.cn/p/b5c27669e2c1
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。