iOS 开源库源码分析之AFNetworking
AFNetworking 是 Mattt Thompson 写的一个 Objective-C 的网络框架,是目前这个语言内使用最为广泛的库。另外作品还有 Swift 的著名网络请求框架 Alamofire,Mattt 还是 nshipster.com 的创立者。
本文主要探讨的内容包括请求回调的设计,请求和响应序列化的设计思路,AFNetworking 关于线程安全性的应用,SSL 的问题等
AFNetworking 的请求流程可以从下图一窥究竟。
多重代理的实现
这里 AFNetworking 对回调的处理有一个精妙的设计,
对于发起一个请求并且接收回调是在
- (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;
里面完成的
首先对于 AFURLSessionManager 作为请求管理类是有几个特点的,
- 1.NSURLSession 回调有多个并且是以 delegate 形式出现,需要转换为一个较为方便的形式
- 2.管理会出现有多个请求同时出现,并且可能出现在不同线程
对于第一个问题,首先是转换为 block 的形式,并且直接在调用请求的地方作为参数返回,会在 NSURLSession 成功的 delegate 方法里面转化为 block 返回。
对于第二个问题,多个请求就出现了处理不同对象回调的问题,处理多重代理需要 Manager 有个数组或者字典来保存多个 delegate,我认为数组是用来处理一物对应多个接收对象,而字典是用来处理一物对一个接收对象,只不过会由一个 manager 来管理多个事物。所以 AFNetworking 这里采用 dict,这里用 taskIdentifier 作为 key 来对应每一个请求 task。
由于线程安全性,所以在新增加一个 delegate 时,会通过 NSLock 来进行访问控制,如下
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate forTask:(NSURLSessionTask *)task { [self.lock lock]; self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate; [self.lock unlock]; }
最终,通过 AFURLSessionManagerTaskDelegate 这个类来简化 NSURLSession 的 delegate 和返回 block 的对应关系
method swizzling
method swizzling 的用法就不多说,讲一下这里为什么用 method swizzling,这里主要是 hook 了 NSURLSessionTask 的 resume 和 suspend 方法,用于发送通知给外部以控制 UIActivityIndicatorView 和 UIRefreshControl 的开始结束动画行为,因为 resume 和 suspend 没有代理回调,并且外部可以控制 task 的 resume 和 suspend 行为,所以这里只有通过 hook 的行为来处理。
SSL的问题
SSL(Secure Sockets Layer)位于 TCP 和 HTTP 之间,Netscape 在推出 HTTPS 协议的时候使用 SSL 进行加密,这便是 SSL 的起源。后来发展成为了 TLS(Transport Layer Security)。主要用于加密通信。
在进行加密通行前会进行握手。
- 1 客户端给出协议版本,一个客户端的随机数,支持的加密方法
- 2 服务端确认加密方法,给出数字证书(公钥在内),一个服务端的随机数
- 3 客户端确认证书,然后生成一个新的随机数并且用证书加密给服务端
- 4 服务端用私钥获取第三个随机数
- 5 客户端和服务端根据约定的加密方法,使用前面三个随机数,生成对话"对话密钥",用来加密接下来整个对话过程
需要注意的是,由于非对称加密耗时较长,所以只用于生产对话密钥,之后的对话都使用对称加密
SSL 证书验证工作,会在 NSURLSessionDelegate 的 didReceiveChallenge 方法里面验证。
SSL 的相关主要在 AFSecurityPolicy 类中,提供三种 SSLPinningMode。
目前有两种策略会验证,AFSSLPinningModeCertificate 和 AFSSLPinningModePublicKey,
AFSSLPinningModeNone 这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
AFSSLPinningModeCertificate 模式会根据 didReceiveChallenge 返回的证书数据与自动扫描 bundle 中 cer 后缀的客户端证书进行比对来验证。
AFSSLPinningModePublicKey 模式类似,根据 didReceiveChallenge 返回的 publicKey 与自动扫描 bundle 中 cer 后缀客户端证书获取的 publicKey 进行对比来验证。
最经典的非对称加密算法是RSA算法。
对称加密有AES、ChaCha20
HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master secret对传输数据进行加密解密。
我们来看最简单的情况:一个证书颁发机构(CA),颁发了一个证书A,服务器用这个证书建立HTTPS连接。客户端在信任列表里有这个CA机构的根证书。
首先CA机构颁发的证书A里包含有证书内容F,以及证书加密内容F1,加密内容F1就是用这个证书机构的私钥对内容F加密的结果。(这中间还有一次hash算法,略过。)
建立https连接时,服务端返回证书A给客户端,客户端的系统里的CA机构根证书有这个CA机构的公钥,用这个公钥对证书A的加密内容F1解密得到F2,跟证书A里内容F对比,若相等就通过验证。整个流程大致是:F->CA私钥加密->F1->客户端CA公钥解密->F。因为中间人不会有CA机构的私钥,客户端无法通过CA公钥解密,所以伪造的证书肯定无法通过验证。
什么是SSL Pinning?
可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机App事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。
为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。
为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。
AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证。
UIImageView的图片下载
UIImageView 设置一张网络图片原本是需要先把图片下载,然后把图片对象赋予给 UIImageView,这里过程则可以通过 category 来简化。
- 1.通过 AFHTTPSessionManager 把图片下载下来
- 2.同样要处理多重代理的情况,多个图片下载请求需要回调,首先同样有一个 mergedTasks 的 dict 来保存不同 task 的回调,mergedTasks 保存的对象是 AFImageDownloaderMergedTask,这里面对于同一个 task 的多次调用,也会返回给 AFImageDownloaderMergedTask 对应的 responseHandlers(NSArray)。
- 3.针对缓存的处理,首先 AFNetworking 里面是通过 AFAutoPurgingImageCache 保存在内存中,这里通过 dict 实现,key 为 request,value 为 image。并且返回 cache 只针对 request 的 cachePolicy 为使用 cache 的。
流程如下图所示:
线程安全性
- AFNetworkActivityIndicatorManager 对于 NSInteger 类型的 activityCount 通过 @synchronized() 线程安全性读写,诸如此类的还有 BOOL,枚举,这里可以单独保证读或者写是唯一的。
- 2.AFURLSessionManager 对于 dict 的mutableTaskDelegatesKeyedByTaskIdentifier 读写都使用一个 NSLock 控制,以保证当前 dict 只有一个行为
[self.lock lock]; delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)]; [self.lock unlock];
还有就是 imageWithData: 也用了 lock 来控制
+ (UIImage *)af_safeImageWithData:(NSData *)data { UIImage* image = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ imageLock = [[NSLock alloc] init]; }); [imageLock lock]; image = [UIImage imageWithData:data]; [imageLock unlock]; return image; }
3.AFImageDownloader 里面有一个 NSMutableArray 的 queuedMergedTasks 和 NSMutableDictionary 的 mergedTasks 属性,对于这里所有的操作都使用一个串行队列通过 sync 来控制相关读写。
4.AFHTTPRequestSerializer 中对与 dict 对象mutableHTTPRequestHeaders 的读写处理则是通过一个 GCD 的并发队列来实现的,这里面对于 mutableHTTPRequestHeaders 读使用的是 sync,写则使用的是 dispatch_barrier_async。如第三点,sync 到一个串行队列来读写,而这里 sync 到一个并行队列是只能读不能写的,举例写操作 A sync 达到一个并发,接着写操作 B 又 sync 到这个并发,但是因为是并发,所以不能保证按照进入的顺序最后写的是 B,所以会造成数据写错误。
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field { dispatch_barrier_async(self.requestHeaderModificationQueue, ^{ [self.mutableHTTPRequestHeaders setValue:value forKey:field]; }); } - (NSString *)valueForHTTPHeaderField:(NSString *)field { NSString __block *value; dispatch_sync(self.requestHeaderModificationQueue, ^{ value = [self.mutableHTTPRequestHeaders valueForKey:field]; }); return value; }
- 5.通过 semaphore 信号量的控制来读到异步回调里面进行同步的作用
- (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) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) { tasks = dataTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) { tasks = uploadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) { tasks = downloadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) { tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"]; } dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); return tasks; }
AFURLResponseSerialization的设计理念
AFNetworking 为什么要 serialization,这里最重要的作用是组装 request 和 response,并且基于 JSON,XML 等风格
由于 response 的 MIMEType 不同造成需要解析的 responseObject 方式也不同,所以在设计上有一个 AFURLResponseSerialization 协议
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response data:(nullable NSData *)data error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
不同的 MIMEType 去实现不同的解析
比如
- MIMEType 为 @"application/json", @"text/json", @"text/javascript" 的 AFJSONResponseSerializer,需要使用 NSJSONSerialization 来解析成为 dict
- MIMEType 为 @"application/xml", @"text/xml" 的 AFXMLParserResponseSerializer,通过 NSXMLParser 解析
- MIMEType 为 @"application/x-plist" 的 AFPropertyListResponseSerializer,通过 NSPropertyListSerialization 解析
- MIMEType 为 @"image/png" 等的 AFImageResponseSerializer,转化为 UIImage,并且可以通过 automaticallyInflatesResponseImage 属性支持自动压缩
这样只要请求前设置不同的 responseSerializer 就可以处理不同类型的数据了。
AFStreamingMultipartFormData
POST 主要集中方式如下
application/x-www-form-urlencoded
POST /testPath HTTP/1.1 Host: test.com Content-Type: application/x-www-form-urlencoded Connection: keep-alive Accept: */* User-Agent: AFNetworkingDemo/3030757 CFNetwork/889.9 Darwin/17.2.0 Accept-Language: zh-cn Accept-Encoding: gzip, deflate Content-Length: 232 machine=iphoneos&netType=WiFi
application/json
POST /restapi HTTP/1.1 Host: m.test.com Content-Type: application/json Connection: keep-alive Accept: */* User-Agent: AFNetworkingDemo/7.10.2 (iPhone; iOS 11.1.1; Scale/3.00) Accept-Language: zh-Hans-CN;q=1 Content-Length: 32 Accept-Encoding: gzip, deflate {"ConfigKey":"V5"}
multipart/form-data
POST /image/upload HTTP/1.1 Host: upload.planet.youku.com Content-Type: multipart/form-data; boundary=Boundary+0xAbCdEfGbOuNdArY Connection: close User-Agent: walkman/20416391 (iPhone; iOS 11.1.1; Scale/3.00) Accept-Language: en;q=1, fr;q=0.9, de;q=0.8, zh-Hans;q=0.7, zh-Hant;q=0.6, ja;q=0.5 Accept-Encoding: gzip,deflate Content-Length: 830211 --Boundary+0xAbCdEfGbOuNdArY Content-Disposition: form-data; name="businessType" hello --Boundary+0xAbCdEfGbOuNdArY Content-Disposition: form-data; name="fileData"; filename="YoukuCircleShareBmp.png" Content-Type: image/png ...contents of image.png... --Boundary+0xAbCdEfGbOuNdArY--
multipart/form-data 就是这里要提到的表单上传,主要用于上传文件,boundary是用于分割不同字段的,每部分都是以 --boundary 开始,消息主体最后以 --boundary-- 标示结束。
表单上传 AFNetworking 可以使 -POST:parameters:constructingBodyWithBlock: 方法,字典类型的 parameters 里面每一个键值对就一个 boundary 的一部分,用 AFHTTPBodyPart 类表达,block 里面可以继续组装 boundary,最后合并为消息主体 AFMultipartBodyStream。
流程如下图所示:
HTTP连接
- 串行连接
HTTP1.1 规定只能用串行连接,就是下一个请求事务的发起需要等待上一个请求事务的返回。所以网络延时严重,一个页面上的四张图片只有等第一张加载完成才能进行下一张。
- 持久连接
持久连接是多个请求共用一个连接,后继请求使用上一个请求的连接,方式依然是串行
HTTP1.0+ 的请求头有 Connection 字段,支持 close 或 Keep-Alive,HTTP1.1 是默认开启的。
客户端发送
Connection: Keep-Alive
服务器如果支持 keep-alive,并且准备在下一个请求中复用连接就可以在响应头发送下面的内容
Connection: Keep-Alive Keep-Alive: max=5, timeout=120
max 表示长连接最大的请求数,timeout 表示长连接时长。
- 管道化连接
HTTP1.1 允许在持久连接上可选使 pipeling,就是客户端并行发送请求,但服务端必须按顺序返回。
几点问题是:
服务器不支持管道化的话就会造成返回数据乱序,比如 AFNetworking 曾经设置 HTTPShouldUsePipelining 默认为 YES,导致了用户下载的图片乱序,详细见这issue
由于需要服务端按照顺序返回,假设服务端先收到客户端第100个请求,那么也需要等待前面99个请求,反而会有更大的延时,这就是人们常说的队头阻塞的问题,所以用这个的人比较少
最后文章到这里已经结束了,其实我觉得 AFNetworking 最重要的是对于 HTTP 的了解,以及它是如何管理这个请求任务的,知道这两点自己实现网络请求库的时候就所行无阻了。
本文作者coderyi