AFNetworking2.0源碼解析<一>

最近看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:。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,199評論 30 471
  • ———————————————回答好下面的足夠了---------------------------------...
    恒愛DE問候閱讀 1,749評論 0 4
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,354評論 0 6
  • OC的理解與特性 OC作為一門面向對象的語言,自然具有面向對象的語言特性:封裝、繼承、多態。它既具有靜態語言的特性...
    克魯德李閱讀 457評論 0 0
  • 序言 目前形勢,參加到iOS隊伍的人是越來越多,甚至已經到供過于求了。今年,找過工作人可能會更深刻地體會到今年的就...
    Jack_lin閱讀 78,486評論 110 1,946