iOS AFNetWorking源码详解(一)
原文地址:
首先来介绍下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接口给使用者使用
AFNetworking相信从star数和fork数来看,大家都能明白这个库是多么的受欢迎了,所以了解这个库对于一个iOS开发来说是极为重要的!
这个是AFNetworking的github地址:GitHub - AFNetworking/AFNetworking: A delightful networking framework for iOS
在使用前阅读README是非常重要的,里面往往包括了这个库的介绍、安装和使用等等,对于快速了解一个库来说,这是非常有帮助的
因为AFNetworking里面包含的内容非常丰富,所以我会分几节来讲
首先我们在AFNetWorking源码地址里download下来,打开工程文件,可以看到里面内容分为两个部分,一个是AFNetworking,另一个是UIKit+AFNetworking
工程目录很明显,第一个是用来做网络请求相关的,第二个则是和UI使用相关的,我们先看第一个
在看完头文件和README之后,你会发现AFURLSessionManager和AFHTTPSessionManager是里面比较重要的两个类
这里我先讲AFURLSessionManager这个类
首先浏览完这个类从API,发现其主要提供了数据的请求、上传和下载功能
在属性方面:
@property(readonly,nonatomic,strong)NSArray *tasks;
@property(readonly,nonatomic,strong)NSArray *dataTasks;
@property(readonly,nonatomic,strong)NSArray *uploadTasks;
@property(readonly,nonatomic,strong)NSArray *downloadTasks;
通过这四个属性,我们分别可以拿到总的任务集合、数据任务集合、上传任务集合和下载任务集合
@property(nonatomic,assign)BOOL attemptsToRecreateUploadTasksForBackgroundSessions;
这个属性非常重要,注释里面写到,在iOS7中存在一个bug,在创建后台上传任务时,有时候会返回nil,所以为了解决这个问题,AFNetworking遵照了苹果的建议,在创建失败的时候,会重新尝试创建,次数默认为3次,所以你的应用如果有场景会有在后台上传的情况的话,记得将该值设为YES,避免出现上传失败的问题
API方面:
-(void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks;
如果将cancelPendingTasks设为YES的话,会在主线程直接关闭掉当前会话,NO的话,会等待当前task结束后再关闭
-(NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
-(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;
数据请求里,request则是你发出的HTTP请求,uploadProgressBlock和downloadProgressBlock则是在如果上传和下载进度有更新的情况下才会调用,completionHandler就是在请求结束之后返回的内容
-(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request fromFile:(NSURL*)fileURL progress:(nullable void(^)(NSProgress*uploadProgress)) uploadProgressBlock completionHandler:(nullable void(^ (NSURLResponse*response,id_Nullable responseObject,NSError*_Nullable error))completionHandler;
-(NSURLSessionUploadTask*)uploadTaskWithRequest:(NSURLRequest*)request fromData:(nullableNSData*)bodyData progress:(nullable void(^)(NSProgress*uploadProgress)) uploadProgressBlock completionHandler:(nullable void(^)(NSURLResponse*response,id_Nullable responseObject,NSError*_Nullable error))completionHandler;
-(NSURLSessionUploadTask*)uploadTaskWithStreamedRequest:(NSURLRequest*)request progress:(nullable void(^)(NSProgress*uploadProgress)) uploadProgressBlock completionHandler:(nullable void(^)(NSURLResponse*response,id_Nullable responseObject,NSError*_Nullable error))completionHandler;
这是三种不同的数据上传方法,第一种是通过fileURL(需要上传的本地文件URL路径)上传,第二种是通过bodyData(需要上传的HTTP body体的数据),第三种是使用流请求的方法,在使用该方法的时候,一定要设置setTaskNeedNewBodyStreamBlock回调,否则session没办法在重新发送steam的时候找到数据源
-(NSURLSessionDownloadTask*)downloadTaskWithRequest:(NSURLRequest*)request progress:(nullable void(^)(NSProgress*downloadProgress)) downloadProgressBlock destination:(nullable NSURL* (^ (NSURL*targetPath,NSURLResponse*response))destination completionHandler:(nullable void(^)(NSURLResponse*response,NSURL*_Nullable filePath,NSError*_Nullable error))completionHandler;
-(NSURLSessionDownloadTask*)downloadTaskWithResumeData:(NSData*)resumeData progress:(nullable void(^)(NSProgress*downloadProgress)) downloadProgressBlock destination:(nullable NSURL* (^)(NSURL*targetPath,NSURLResponse*response))destination completionHandler:(nullable void(^)(NSURLResponse*response,NSURL*_Nullable filePath,NSError*_Nullable error))completionHandler;
下载方法也有两种,第一种是通过HTTP请求方式下载,另一种则是通过之前的下载数据来恢复下载,destination在下载的过程中文件会先存放在一个临时的位置,等到下载完成之后,文件会转移到目标位置
FOUNDATION_EXPORT NSString*constAFNetworkingTaskDidResumeNotification;
在对外提供的notification key里面,使用了FOUNDATION_EXPORT来定义常量,使用FOUNDATION_EXPORT和extern或者define有什么区别呢?
FOUNDATION_EXPORT在c文件编译下是和extern等同,在c++文件编译下是和extern "C"等同,在32位机的环境下又是另外编译情况,在兼容性方面,FOUNDATION_EXPORT做的会更好。
这里还提到了效率方面的问题:iOS开发的一些奇巧淫技3 - CocoaChina_让移动开发更简单
进入到实现文件里面,一些常量的定义和初始化,我就不展开分析了
实现文件里面具体分为三部分:
第一部分:AFURLSessionManagerTaskDelegate
Delegate里面做的内容有:
1、设置task的progress
2、KVO监听task、progress的方法调用
3、实现URLSessionTask的回调,将完成的结果block给manager里面相关的调用方法,并且通知给接收者
4、对接收到的数据进行了拼接
5、对下载完成后的文件进行转移
第二部分:_AFURLSessionTaskSwizzling
这部分主要做的是将NSURLSessionDataTask及父类里的resume和suspend方法替换成自己自定义的方法
第三部分:AFURLSessionManager
1、初始化session配置及增加task代理
2、外部API的block通过delegate里面的block来获得
- (void)URLSession:(__unused NSURLSession*)session task:(NSURLSessionTask*)task didCompleteWithError:(NSError*)error
在NSURLSessionTaskDelegate的这个方法里面,我们首先看到了
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
// some codes
#pragma clang diagnostic pop
这个的作用是用来消除特定区域的clang的编译警告,-Wgnu则是消除?:警告
这个是clang的警告message列表Which Clang Warning Is Generating This Message?
在这个代理里面,将请求获得数据,进行组装通过completionHandler回调给外面,并且会发出task完成的通知,这个在UIKit+AFNetworking里UIRefreshControl +AFNetworking里会接收到,用来停止刷新,如果你不使用AF的UI部分,你可以通过接收这个通知来做操作
- (void)URLSession:(__unused NSURLSession*)session dataTask (__unused NSURLSessionDataTask*)dataTask didReceiveData:(NSData*)data
则是使用一个可变的data,对接收到的data进行拼接,直到是完成
在往下看
static inline void af_swizzleSelector(Class theClass,SEL originalSelector,SEL swizzledSelector)
static inline BOOL af_addMethod(Class theClass,SEL selector,Method method)
这里为什么要使用inline呢?
为了防止反汇编之后,在符号表里面看不到你所调用的该方法,否则别人可以通过篡改你的返回值来造成攻击,iOS安全–使用static inline方式编译函数,防止静态分析,特别是在使用swizzling的时候
那另一个话题,除了使用swizzling动态替换函数方法之外,还有别的方法么?有,修改IMP指针指向的方法,轻松学习之 IMP指针的作用 - CocoaChina_让移动开发更简单
在AFURLSessionManager实现里面,是接收到通知的处理,设置代理,增加数据、上传和下载任务的代理以及移除代理,设置block等代码
-(NSArray *)tasksForKeyPath:(NSString*)keyPath {
__block NSArray*tasks =nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks,NSArray *uploadTasks,NSArray *downloadTasks) {
// do something
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
return tasks;
}
这里使用到了GCD的信号量控制并发的处理,首先它创建了一个semaphore,并且设置数量为0,表示只能是一个线程运行,在dispatch_semaphore_signal发送一个信号之后,如果线程仍在运行,dispatch_semaphore_wait则会一直等待,直到这个线程结束增加一个信号量,才能继续执行下一个线程
- (void)URLSession:(NSURLSession*)session task:(NSURLSessionTask*)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
这个方法里面会去判断totalBytesExpectedToSend这个的大小,我们知道上传请求的时候有三种方法,一个是文件,一个是data,另一个是流,前两种方法session会自动计算Content-Length,但是流却不一样,它的大小是未知的,所以在header信息里面一定要提供Content-Length
在看完这一部分的时候,我有两个疑问:
1、在AFURLSessionManagerTaskDelegate获取监听事件observeValueForKeyPath里面,object要判断是否是NSURLSessionDownloadTask的类
这个问题我在github上面进行了提问:Repeated judgment in AFURLSessionManager · Issue #3354 · AFNetworking/AFNetworking · GitHub
kcharwood大神也做了回答
2、URLSession:didReceiveChallenge:completionHandler在实例化credential后会判断是否是nil,但是在URLSession:task:didReceiveChallenge:completionHandler里却没有判断
这个问题希望有人可以解答一下
如果有什么意见或者建议,欢迎大家留言,知识是需要交流的,我相信会有更好更简洁的方法来处理
这个是我的个人微信公众号,会不定期发表一些iOS开发文章以及疑难问题和我在阅读技术和非技术书籍的一些感悟,欢迎大家订阅!
宫城Dev