首先來介紹下AFNetWorking,官方介紹如下:
AFNetworking is a delightful networking library for iOS and Mac OS X. It’s built on top of theFoundation URL Loading System, extending the powerful high-level networking abstractions built into Cocoa. It has a modular architecture with well-designed, feature-rich APIs that are a joy to use.
Perhaps the most important feature of all, however, is the amazing community of developers who use and contribute to AFNetworking every day. AFNetworking powers some of the most popular and critically-acclaimed apps on the iPhone, iPad, and Mac.
Choose AFNetworking for your next project, or migrate over your existing projects—you’ll be happy you did!
翻譯過來簡單來說就是
AFNetworking是一個適用於iOS和Mac OS X兩個平臺的網路庫,它是基於Foundation URL Loading System上進行了一套封裝,並且提供了豐富且優美的API介面給使用者使用
相信從star數和fork數來看,大家都能明白這個庫是多麼的受歡迎了,所以瞭解這個庫對於一個iOS開發來說是極為重要的!
這個是AFNetworking的github地址:GitHub – AFNetworking/AFNetworking: A delightful networking framework for iOS
在使用前閱讀README是非常重要的,裡面往往包括了這個庫的介紹、安裝和使用等等,對於快速瞭解一個庫來說,這是非常有幫助的
首先我們在AFNetWorking原始碼地址裡download下來,開啟工程檔案,可以看到裡面內容分為兩個部分,一個是AFNetworking,另一個是UIKit+AFNetworking
很明顯,第一個是用來做網路請求相關的,第二個則是和UI使用相關的,我們先看第一個
在看完標頭檔案和README之後,你會發現AFURLSessionManager
和AFHTTPSessionManager
是裡面比較重要的兩個類
這裡我先講AFURLSessionManager
這個類
首先瀏覽完這個類從API,發現其主要提供了資料的請求、上傳和下載功能
在屬性方面:
1 2 3 4 5 6 7 |
@property(readonly,nonatomic,strong)NSArray *tasks; @property(readonly,nonatomic,strong)NSArray *dataTasks; @property(readonly,nonatomic,strong)NSArray *uploadTasks; @property(readonly,nonatomic,strong)NSArray *downloadTasks; |
通過這四個屬性,我們分別可以拿到總的任務集合、資料任務集合、上傳任務集合和下載任務集合
1 |
@property(nonatomic,assign)BOOL attemptsToRecreateUploadTasksForBackgroundSessions; |
這個屬性非常重要,註釋裡面寫到,在iOS7中存在一個bug,在建立後臺上傳任務時,有時候會返回nil,所以為了解決這個問題,AFNetworking遵照了蘋果的建議,在建立失敗的時候,會重新嘗試建立,次數預設為3次,所以你的應用如果有場景會有在後臺上傳的情況的話,記得將該值設為YES,避免出現上傳失敗的問題
1 |
FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidResumeNotification; |
在對外提供的notification key裡面,使用了FOUNDATION_EXPORT
來定義常量,使用FOUNDATION_EXPORT
和extern
或者define
有什麼區別呢?
FOUNDATION_EXPORT
在c檔案編譯下是和extern等同,在c++檔案編譯下是和extern “C”等同,在32位機的環境下又是另外編譯情況,在相容性方面,FOUNDATION_EXPORT
做的會更好。
這裡還提到了效率方面的問題:iOS開發的一些奇巧淫技3
進入到實現檔案裡面,我們可以看到在外部API呼叫dataTask、uploadTask、downloadTask方法實際上都是completionHanlder block返回出來的,但是我們知道網路請求是delegate返回結果的,AF內部做了巧妙的操作,他對每個task都增加代理設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler { __block NSURLSessionDataTask *dataTask = nil; url_session_manager_create_task_safely(^{ dataTask = [self.session dataTaskWithRequest:request]; }); // 每個task裡面都會呼叫addDelegate方法 [self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler]; return dataTask; } |
在設定裡面,每個task會在內部建立AFURLSessionManagerTaskDelegate
物件,並設定completionHandler、uploadProgressBlock、downloadProgressBlock回撥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler { // 初始化delegate物件 AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init]; delegate.manager = self; // 將task的completionHandler賦給delegate,系統網路請求delegate 呼叫該block,返回結果 delegate.completionHandler = completionHandler; dataTask.taskDescription = self.taskDescriptionForSessionTasks; // 對task進行delegate [self setDelegate:delegate forTask:dataTask]; // 設定上傳和下載進度回撥 delegate.uploadProgressBlock = uploadProgressBlock; delegate.downloadProgressBlock = downloadProgressBlock; } |
然後delegate物件利用kvo將task對一些方法進行監聽,並且監聽到變化時,通過block返回,將delegate轉成block出去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate forTask:(NSURLSessionTask *)task { // 斷言 NSParameterAssert(task); NSParameterAssert(delegate); [self.lock lock]; self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate; // task使用kvo對一些方法監聽,返回上傳或者下載的進度 [delegate setupProgressForTask:task]; // sessionManager對暫停task和恢復task進行註冊通知 [self addNotificationObserverForTask:task]; [self.lock unlock]; } |
在原先IM的設計時,因為介面的數量並不多,所以在AsyncSocket的delegate回撥後,我們依舊是採用delegate回撥給業務層,但是隨著介面數量的增加,業務層對於回撥的處理更加困難和不可控,在重構IM的時候,我們也參考學習了AF的做法,我們通過對唯一標識和每個請求做一一繫結,將請求的上下文關聯起來,這樣讓socket長連線的請求的也想http請求一樣,都由block回去,對於業務層的處理也方便更多
setupProgressForTask
方法主要是對task和progress設定監聽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
- (void)setupProgressForTask:(NSURLSessionTask *)task { __weak __typeof__(task) weakTask = task; // 設定上傳和下載的大小 // 設定上傳和下載中允許取消和暫停 // 設定上傳和下載響應恢復處理方法後恢復上傳或下載 // task對接收到的位元組數、期望接收到的位元組數、傳送的位元組數、期望傳送的位元組數設定監聽 [task addObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived)) options:NSKeyValueObservingOptionNew context:NULL]; [task addObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive)) options:NSKeyValueObservingOptionNew context:NULL]; [task addObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent)) options:NSKeyValueObservingOptionNew context:NULL]; [task addObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToSend)) options:NSKeyValueObservingOptionNew context:NULL]; // 上傳和下載設定完成的分數監聽 [self.downloadProgress addObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) options:NSKeyValueObservingOptionNew context:NULL]; [self.uploadProgress addObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) options:NSKeyValueObservingOptionNew context:NULL]; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object isKindOfClass:[NSURLSessionTask class]] || [object isKindOfClass:[NSURLSessionDownloadTask class]]) { // 設定上傳和下載的新值 } else if ([object isEqual:self.downloadProgress]) { if (self.downloadProgressBlock) { self.downloadProgressBlock(object); } } else if ([object isEqual:self.uploadProgress]) { if (self.uploadProgressBlock) { self.uploadProgressBlock(object); } } } |
在第一個if判斷裡面,object判斷是否是NSURLSessionTask
類或者是否是NSURLSessionDownloadTask
類,但是進到NSURLSessionDownloadTask
的時候,我們可以看到NSURLSessionDownloadTask
是NSURLSessionTask
的子類,那為什麼還要判斷這個呢?
NSURLSessionTask
實際上是Class cluster,通過NSURLSession
生成的task返回的並不一定是指定的task型別。因此kindOfClass並不總會生效,具體可以參見AFURLSessionManager.m在load方法中的說明。
特定於當前問題,是由於iOS 7上NSCFURLSessionDownloadTask的基類並不是NSCFURLSessionTask
,因此isKindOfClass會出錯。檢視對應的commit就可以知道了。
在NSURLSessionTaskDelegate
的代理裡面,只是做了兩件事情,第一個是獲取資料,將responseSerializer和downloadFileURL或data存到userInfo裡面,第二個是根據error是否為空值,做下一步處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#pragma mark - NSURLSessionTaskDelegate - (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu" // 獲取資料,將responseSerializer和downloadFileURL或data存到userInfo裡面 __strong AFURLSessionManager *manager = self.manager; __block id responseObject = nil; __block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[AFNetworkingTaskDidCompleteResponseSerializerKey] = manager.responseSerializer; //Performance Improvement from #2672 NSData *data = nil; if (self.mutableData) { data = [self.mutableData copy]; //We no longer need the reference, so nil it out to gain back some memory. self.mutableData = nil; } if (self.downloadFileURL) { userInfo[AFNetworkingTaskDidCompleteAssetPathKey] = self.downloadFileURL; } else if (data) { userInfo[AFNetworkingTaskDidCompleteResponseDataKey] = data; } if (error) { // 有error時處理 } else { // 無error時正常處理 } #pragma clang diagnostic pop } |
在有error時,userInfo先儲存error,然後檢查manager是否有completionGroup和completionQueue,沒有的話,就建立一個dispatch_group_t和在主執行緒上做completionHandler的操作,並在主執行緒中傳送一個AFNetworkingTaskDidCompleteNotification通知,這個通知在UIKit+AFNetworking裡UIRefreshControl +AFNetworking裡也會接收到,用來停止重新整理,如果你不使用AF的UI部分,你可以通過接收這個通知來做操作
1 2 3 4 5 6 7 8 9 10 11 |
userInfo[AFNetworkingTaskDidCompleteErrorKey] = 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); } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo]; }); }); |
在沒有error時,會先對資料進行一次序列化操作,然後下面的處理就和有error的那部分一樣了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
dispatch_async(url_session_manager_processing_queue(), ^{ NSError *serializationError = nil; responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError]; if (self.downloadFileURL) { responseObject = self.downloadFileURL; } if (responseObject) { userInfo[AFNetworkingTaskDidCompleteSerializedResponseKey] = responseObject; } if (serializationError) { userInfo[AFNetworkingTaskDidCompleteErrorKey] = serializationError; } dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{ if (self.completionHandler) { self.completionHandler(task.response, responseObject, serializationError); } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo]; }); }); }); |
一開始我們就看到了clang命令,這個的作用是用來消除特定區域的clang的編譯警告,-Wgnu則是消除?:警告,這個是clang的警告message列表Which Clang Warning Is Generating This Message?
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu" // some codes #pragma clang diagnostic pop } |
再下面兩個則是收到資料和下載檔案的回撥處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#pragma mark - NSURLSessionDataTaskDelegate - (void)URLSession:(__unused NSURLSession *)session dataTask:(__unused NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { [self.mutableData appendData:data]; } #pragma mark - NSURLSessionDownloadTaskDelegate - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSError *fileManagerError = nil; self.downloadFileURL = nil; if (self.downloadTaskDidFinishDownloading) { self.downloadFileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location); if (self.downloadFileURL) { [[NSFileManager defaultManager] moveItemAtURL:location toURL:self.downloadFileURL error:&fileManagerError]; if (fileManagerError) { [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:fileManagerError.userInfo]; } } } } |
在剛才說到的load方法裡面,對系統的resume和suspend方法進行了替換
1 2 3 4 5 6 7 8 9 10 11 12 |
+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass { Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume)); Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend)); if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) { af_swizzleSelector(theClass, @selector(resume), @selector(af_resume)); } if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) { af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend)); } } |
替換之後,只是增加了通知處理而已
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (void)af_resume { NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state"); NSURLSessionTaskState state = [self state]; [self af_resume]; if (state != NSURLSessionTaskStateRunning) { [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self]; } } - (void)af_suspend { NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state"); NSURLSessionTaskState state = [self state]; [self af_suspend]; if (state != NSURLSessionTaskStateSuspended) { [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self]; } } |
在呼叫替換和增加方法時候,用到了關鍵字inline,inline是為了防止反彙編之後,在符號表裡面看不到你所呼叫的該方法,否則別人可以通過篡改你的返回值來造成攻擊,iOS安全–使用static inline方式編譯函式,防止靜態分析,特別是在使用swizzling的時候,那除了使用swizzling動態替換函式方法之外,還有別的方法麼?有,修改IMP指標指向的方法,輕鬆學習之 IMP指標的作用 – CocoaChina_讓移動開發更簡單
1 2 3 4 5 6 7 8 9 |
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(theClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) { return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method)); } |
在+ load方法中,我們又看到了GCC命令,那clang和GCC在使用的時機有沒有什麼區別?通常情況下,在GCC特有的處理或者是在GCC,clang和其他相容GCC的編譯器時,儘量使用#pragma GCC,clang特有的處理時,使用#pragma clang,這個是GCC的message表
1 2 3 4 5 6 7 8 |
+ (void)load { // ... #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnonnull" NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil]; #pragma clang diagnostic pop // ... } |
看完之後,有個疑問,查了資料也沒有找到:
在NSURLSessionDelegate的URLSession:didReceiveChallenge:completionHandler:方法裡面disposition會對credential物件做非空判斷然後再賦值校驗型別,但是NSURLSessionTaskDelegate的– [URLSession:task:didReceiveChallenge:completionHandler:]方法裡面disposition並不對credential物件做判斷,而是直接就賦值校驗型別,有知道的,歡迎留言交流