最近看AFNetworking2的源碼,學習這個知名網絡框架的實現,順便梳理寫下文章。AFNetworking2的大體架構和思路在這篇文章已經說得挺清楚了,就不再贅述了,只說說實現的細節。AFNetworking的代碼還在不斷更新中,我看的是AFNetworking2.3.1。
本篇先看看AFURLConnectionOperation,AFURLConnectionOperation繼承自NSOperation,是一個封裝好的任務單元,在這里構建了NSURLConnection,作為NSURLConnection的delegate處理請求回調,做好狀態切換,線程管理,可以說是AFNetworking最核心的類
0.Tricks
AFNetworking代碼中有一些常用技巧,先說明一下。
A.clang warning
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//code
#pragma clang diagnostic pop
表示在這個區間里忽略一些特定的clang的編譯警告,因為AFNetworking作為一個庫被其他項目引用,所以不能全局忽略clang的一些警告,只能在有需要的時候局部這樣做,作者喜歡用?:符號,所以經常見忽略-Wgnu警告的寫法,詳見這里。
B.dispatch_once
為保證線程安全,所有單例都用dispatch_once生成,保證只執行一次,這也是iOS開發常用的技巧。例如:
static dispatch_queue_t url_request_operation_completion_queue() {
static dispatch_queue_t af_url_request_operation_completion_queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue", DISPATCH_QUEUE_CONCURRENT );
});
return af_url_request_operation_completion_queue;
}
C.weak & strong self
常看到一個block要使用self,會處理成在外部聲明一個weak變量指向self,在block里又聲明一個strong變量指向weakSelf:
__weak __typeof(self)weakSelf = self;
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
}];
weakSelf是為了block不持有self,避免循環引用,而再聲明一個strongSelf是因為一旦進入block執行,就不允許self在這個執行過程中釋放。block執行完后這個strongSelf會自動釋放,沒有循環引用問題。
1.線程
先來看看NSURLConnection發送請求時的線程情況,NSURLConnection是被設計成異步發送的,調用了start方法后,NSURLConnection會新建一些線程用底層的CFSocket去發送和接收請求,在發送和接收的一些事件發生后通知原來線程的Runloop去回調事件。
NSURLConnection的同步方法sendSynchronousRequest方法也是基于異步的,同樣要在其他線程去處理請求的發送和接收,只是同步方法會手動block住線程,發送狀態的通知也不是通過RunLoop進行。
使用NSURLConnection有幾種選擇:
A.在主線程調異步接口
若直接在主線程調用異步接口,會有個Runloop相關的問題:
當在主線程調用[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]時,請求發出,偵聽任務會加入到主線程的Runloop下,RunloopMode會默認為NSDefaultRunLoopMode。這表明只有當前線程的Runloop處于NSDefaultRunLoopMode時,這個任務才會被執行。但當用戶滾動tableview或scrollview時,主線程的Runloop是處于NSEventTrackingRunLoopMode模式下的,不會執行NSDefaultRunLoopMode的任務,所以會出現一個問題,請求發出后,如果用戶一直在操作UI上下滑動屏幕,那在滑動結束前是不會執行回調函數的,只有在滑動結束,RunloopMode切回NSDefaultRunLoopMode,才會執行回調函數。蘋果一直把動畫效果性能放在第一位,估計這也是蘋果提升UI動畫性能的手段之一。
所以若要在主線程使用NSURLConnection異步接口,需要手動把RunloopMode設為NSRunLoopCommonModes。這個mode意思是無論當前Runloop處于什么狀態,都執行這個任務。
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
B.在子線程調同步接口
若在子線程調用同步接口,一條線程只能處理一個請求,因為請求一發出去線程就阻塞住等待回調,需要給每個請求新建一個線程,這是很浪費的,這種方式唯一的好處應該是易于控制請求并發的數量。
C.在子線程調異步接口
子線程調用異步接口,子線程需要有Runloop去接收異步回調事件,這里也可以每個請求都新建一條帶有Runloop的線程去偵聽回調,但這一點好處都沒有,既然是異步回調,除了處理回調內容,其他時間線程都是空閑可利用的,所有請求共用一個響應的線程就夠了。
AFNetworking用的就是第三種方式,創建了一條常駐線程專門處理所有請求的回調事件,這個模型跟nodejs有點類似。網絡請求回調處理完,組裝好數據后再給上層調用者回調,這時候回調是拋回主線程的,因為主線程是最安全的,使用者可能會在回調中更新UI,在子線程更新UI會導致各種問題,一般使用者也可以不需要關心線程問題。
以下是相關線程大致的關系,實際上多個NSURLConnection會共用一個NSURLConnectionLoader線程,這里就不細化了,除了處理socket的CFSocket線程,還有一些Javascript:Core的線程,目前不清楚作用,歸為NSURLConnection里的其他線程。因為NSURLConnection是系統控件,每個iOS版本可能都有不一樣,可以先把NSURLConnection當成一個黑盒,只管它的start和callback就行了。如果使用AFHttpRequestOperationManager的接口發送請求,這些請求會統一在一個NSOperationQueue里去發,所以多了上面NSOperationQueue的一個線程。
相關代碼:-networkRequestThread:, -start:, -operationDidStart:。
2.狀態機
繼承NSOperation有個很麻煩的東西要處理,就是改變狀態時需要發KVO通知,否則這個類加入NSOperationQueue不可用了。NSOperationQueue是用KVO方式偵聽NSOperation狀態的改變,以判斷這個任務當前是否已完成,完成的任務需要在隊列中除去并釋放。
AFURLConnectionOperation對此做了個狀態機,統一搞定狀態切換以及發KVO通知的問題,內部要改變狀態時,就只需要類似self.state = AFOperationReadyState的調用而不需要做其他了,狀態改變的KVO通知在setState里發出。
總的來說狀態管理相關代碼就三部分,一是限制一個狀態可以切換到其他哪些狀態,避免狀態切換混亂,二是狀態Enum值與NSOperation四個狀態方法的對應,三是在setState時統一發KVO通知。詳見代碼注釋。
相關代碼:AFKeyPathFromOperationState, AFStateTransitionIsValid, -setState:, -isPaused:, -isReady:, -isExecuting:, -isFinished:.
3.NSURLConnectionDelegate
處理NSURLConnection Delegate的內容不多,代碼也是按請求回調的順序排列下去,十分易讀,主要流程就是接收到響應的時候打開outputStream,接著有數據過來就往outputStream寫,在上傳/接收數據過程中會回調上層傳進來的相應的callback,在請求完成回調到connectionDidFinishLoading時,關閉outputStream,用outputStream組裝responseData作為接收到的數據,把NSOperation狀態設為finished,表示任務完成,NSOperation會自動調用completeBlock,再回調到上層。
4.setCompleteBlock
NSOperation在iOS4.0以后提供了個接口setCompletionBlock,可以傳入一個block作為任務執行完成時(state狀態機變為finished時)的回調,AFNetworking直接用了這個接口,并通過重寫加了幾個功能:
A.消除循環引用
在NSOperation的實現里,completionBlock是NSOperation對象的一個成員,NSOperation對象持有著completionBlock,若傳進來的block用到了NSOperation對象,或者block用到的對象持有了這個NSOperation對象,就會造成循環引用。這里執行完block后調用[strongSelf setCompletionBlock:nil]把completionBlock設成nil,手動釋放self(NSOperation對象)持有的completionBlock對象,打破循環引用。
可以理解成對外保證傳進來的block一定會被釋放,解決外部使用使很容易出現的因對象關系復雜導致循環引用的問題,讓使用者不知道循環引用這個概念都能正確使用。
B.dispatch_group
這里允許用戶讓所有operation的completionBlock在一個group里執行,但我沒看出這樣做的作用,若想組裝一組請求(見下面的batchOfRequestOperations)也不需要再讓completionBlock在group里執行,求解。
C.”The Deallocation Problem”
作者在注釋里說這里重寫的setCompletionBlock方法解決了”The Deallocation Problem”,實際上并沒有。”The Deallocation Problem”簡單來說就是不要讓UIKit的東西在子線程釋放。
這里如果傳進來的block持有了外部的UIViewController或其他UIKit對象(下面暫時稱為A對象),并且在請求完成之前其他所有對這個A對象的引用都已經釋放了,那么這個completionBlock就是最后一個持有這個A對象的,這個block釋放時A對象也會釋放。這個block在什么線程釋放,A對象就會在什么線程釋放。我們看到block釋放的地方是url_request_operation_completion_queue(),這是AFNetworking特意生成的子線程,所以按理說A對象是會在子線程釋放的,會導致UIKit對象在子線程釋放,會有問題。
但AFNetworking實際用起來卻沒問題,想了很久不得其解,后來做了實驗,發現iOS5以后蘋果對UIKit對象的釋放做了特殊處理,只要發現在子線程釋放這些對象,就自動轉到主線程去釋放,斷點出來是由一個叫_objc_deallocOnMainThreadHelper的方法做的。如果不是UIKit對象就不會跳到主線程釋放。AFNetworking2.0只支持iOS6+,所以沒問題。
5.batchOfRequestOperations
這里額外提供了一個便捷接口,可以傳入一組請求,在所有請求完成后回調complionBlock,在每一個請求完成時回調progressBlock通知外面有多少個請求已完成。詳情參見代碼注釋,這里需要說明下dispatch_group_enter和dispatch_group_leave的使用,這兩個方法用于把一個異步任務加入group里。
一般我們要把一個任務加入一個group里是這樣:
dispatch_group_async(group, queue, ^{
block();
});
這個寫法等價于
dispatch_async(queue, ^{
dispatch_group_enter(group);
block()
dispatch_group_leave(group);
});
如果要把一個異步任務加入group,這樣就行不通了:
dispatch_group_async(group, queue, ^{
[self performBlock:^(){
block();
}];
//未執行到block() group任務就已經完成了
});
這時需要這樣寫:
dispatch_group_enter(group);
[self performBlock:^(){
block();
dispatch_group_leave(group);
}];
異步任務回調后才算這個group任務完成。對batchOfRequest的實現來說就是請求完成并回調后,才算這個任務完成。
其實這跟retain/release差不多,都是計數,dispatch_group_enter時任務數+1,dispatch_group_leave時任務數-1,任務數為0時執行dispatch_group_notify的內容。
相關代碼:-batchOfRequestOperations:progressBlock:completionBlock:
6.其他
A.鎖
AFURLConnectionOperation有一把遞歸鎖,在所有會訪問/修改成員變量的對外接口都加了鎖,因為這些對外的接口用戶是可以在任意線程調用的,對于訪問和修改成員變量的接口,必須用鎖保證線程安全。
B.序列化
AFNetworking的多數類都支持序列化,但實現的是NSSecureCoding的接口,而不是NSCoding,區別在于解數據時要指定Class,用-decodeObjectOfClass:forKey:方法代替了-decodeObjectForKey:。這樣做更安全,因為序列化后的數據有可能被篡改,若不指定Class,-decode出來的對象可能不是原來的對象,有潛在風險。另外,NSSecureCoding是iOS6以上才有的。詳見這里。
這里在序列化時保存了當前任務狀態,接收的數據等,但回調block是保存不了的,需要在取出來發送時重新設置。可以像下面這樣持久化保存和取出任務:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:operation];
AFHTTPRequestOperation *operationFromDB = [NSKeyedUnarchiver unarchiveObjectWithData:data];
[operationFromDB start];
C.backgroundTask
這里提供了setShouldExecuteAsBackgroundTaskWithExpirationHandler接口,決定APP進入后臺后是否繼續發送接收請求,并在后臺執行時間超時后取消所有請求。在dealloc里需要調用[application endBackgroundTask:],告訴系統這個后臺任務已經完成,不然系統會一直讓你的APP運行在后臺,直到超時。
相關代碼:-setShouldExecuteAsBackgroundTaskWithExpirationHandler:, -dealloc:
7.AFHTTPRequestOperation
AFHTTPRequestOperation繼承了AFURLConnectionOperation,把它放一起說是因為它沒做多少事情,主要多了responseSerializer,暫停下載斷點續傳,以及提供接口請求成功失敗的回調接口-setCompletionBlockWithSuccess:failure:。