最近在補源碼閱讀方面的短板,第一個選擇的就是AFNetworking,一方面AF的編碼思路、代碼質量都屬於開源框架的上乘;另外一方面也能夠藉機溫習一下網絡方面的東西。 AF源碼解析的系列文章有不少(文末有我看過的一些推薦給你們),本文不對AF做全面的解析,僅從常駐線程這個角度解析一下2.0和3.0的差別。html
NSURLConnectionios
先來看看 NSURLConnection 發送請求時的線程狀況,NSURLConnection 是被設計成異步發送的,調用了start方法後,NSURLConnection 會新建一些線程用底層的 CFSocket 去發送和接收請求,在發送和接收的一些事件發生後通知原來線程的Runloop去回調事件。 大概有三種方法使用NSURLConnection面試
A.在主線程異步回調安全
若直接在主線程異步回調會存在兩個問題:bash
[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]
複製代碼
一、當在主線程調用上面的初始化方法時,監聽回調的任務會加入到主線程的 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];
複製代碼
二、做爲網絡層框架,在 NSURLConnection 回調回來以後,一定要對 Response 作一些諸如序列化、錯誤處理的操做的。這些通用操做勢必要放在子線程去作掉,接着回到主線程,框架的使用者只須要拿處處理後的 Response 進行UI 刷新便可。(PASS)網絡
B.一個請求一條線程session
來一個請求開闢一條線程,設置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 回調方法。若是每來一個請求就開一條線程,而且保活線程,這樣開銷太大了。因此只須要保活一條固定的線程,在這個線程裏發起請求、接收回調。
標題寫的是「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回調致使的多線程併發,還會致使性能的浪費。
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);
}
}
}
複製代碼
面試官可能會問你:爲何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;
}
複製代碼
+ (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,來防止這個新建的線程因爲沒有活動直接退出。
參考資料
轉載於做者:dj_rose
連接:www.jianshu.com/p/b5c27669e…
小編這呢,給你們推薦一個優秀的iOS交流平臺,平臺裏的夥伴們都是很是優秀的iOS開發人員,咱們專一於技術的分享與技巧的交流,你們能夠在平臺上討論技術,交流學習。歡迎你們的加入(想要進入的可加小編微信17585454165)。