iOS 开源库源码分析之AFNetworking

此文基于AFNetworking-3.2.0

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 的。

流程如下图所示:

线程安全性

    1. 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

results matching ""

    No results matching ""