AFNetworking2.0的源碼解析

EnidGilbert 8年前發布 | 10K 次閱讀 iOS開發 移動開發

來自: http://blog.csdn.net//likendsl/article/details/39002267




(via: bang's blog
 
最近看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 是 iOS 6 以上才有的。 詳見這里
 
這里在序列化時保存了當前任務狀態,接收的數據等,但回調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:。詳見源碼注釋。

8.源碼注釋
 AFURLConnectionOperation.m 
 
 AFHTTPRequestOperation.m 

 

 本文由用戶 EnidGilbert 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!