AFNetworking2.0源碼解析<一>

AFNetworking2.0源碼解析<一>

2014-8-28

最近看AFNetworking2的源碼,學習這個知名網絡框架的實現,順便梳理寫下文章。AFNetworking的代碼還在不斷更新中,我看的是AFNetworking2.3.1

本篇先看看AFURLConnectionOperation,AFURLConnectionOperation繼承自NSOperation,是一個封裝好的任務單元,在這裏構建了NSURLConnection,作爲NSURLConnection的delegate處理請求回調,做好狀態切換,線程管理,可以說是AFNetworking最核心的類,下面分幾部分說下看源碼時注意的點,最後放上代碼的註釋。

0.Tricks

AFNetworking代碼中有一些常用技巧,先說明一下。

A.clang warning

1
2
3
4
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//code
#pragma clang diagnostic pop

表示在這個區間裏忽略一些特定的clang的編譯警告,因爲AFNetworking作爲一個庫被其他項目引用,所以不能全局忽略clang的一些警告,只能在有需要的時候局部這樣做,作者喜歡用?:符號,所以經常見忽略-Wgnu警告的寫法,詳見這裏

B.dispatch_once

爲保證線程安全,所有單例都用dispatch_once生成,保證只執行一次,這也是iOS開發常用的技巧。例如:

1
2
3
4
5
6
7
8
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:

1
2
3
4
__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處於什麼狀態,都執行這個任務。

1
2
3
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的一個線程。

afnetroking (2)

相關代碼:-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+,所以沒問題。

blockTest

5.batchOfRequestOperations

這裏額外提供了一個便捷接口,可以傳入一組請求,在所有請求完成後回調complionBlock,在每一個請求完成時回調progressBlock通知外面有多少個請求已完成。詳情參見代碼註釋,這裏需要說明下dispatch_group_enter和dispatch_group_leave的使用,這兩個方法用於把一個異步任務加入group裏。

一般我們要把一個任務加入一個group裏是這樣:

1
2
3
dispatch_group_async(group, queue, ^{
    block();
});

這個寫法等價於

1
2
3
4
5
dispatch_async(queue, ^{
    dispatch_group_enter(group);
    block()
    dispatch_group_leave(group);
});

如果要把一個異步任務加入group,這樣就行不通了:

1
2
3
4
5
6
dispatch_group_async(group, queue, ^{
    [self performBlock:^(){
        block();
    }];
    //未執行到block() group任務就已經完成了
});

這時需要這樣寫:

1
2
3
4
5
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是保存不了的,需要在取出來發送時重新設置。可以像下面這樣持久化保存和取出任務:

1
2
3
4
5
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.源碼註釋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章