阅读 2571

AFNetworking底层源码解析

一.简介

AFNetworking是适用于iOS,macOS,watchOS和tvOS的的网络库。它构建于Foundation URL系统之上,扩展了Cocoa内置的强大的高级网络抽象。它采用模块化架构,设计精良,功能丰富的API,使用起来非常简单。本文重点介绍缓存和安全两个模块;

二. 组织架构图

三. 整体流程、缓存模块和安全模块介绍

3.1 整体流程图

3.1.1 发送一个Get请求的流程图

3.2 缓存

3.2.1 简介

AFNetWorking是基于NSURLSession(iOS7以上的网络请求框架),在生成配置的时候有三种缓存配置选择:

// 默认会话模式:默认添加内存缓存和磁盘缓存。
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;  

// 瞬时会话模式:只添加内存缓存,不实现磁盘缓存。
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;  

// 后台会话模式:内存和磁盘都不进行缓存。
+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier;  
复制代码

我们还可以对缓存的大小进行设置,只需要对NSURLCache进行初始化就可以了。

3.2.2 实现初始化

-application:didFinishLaunchingWithOptions:中对[NSURLCache sharedURLCache]进行初始化设置:

NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                                         diskCapacity:20 * 1024 * 1024
                                                             diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
复制代码

也可以单独对NSURLSession的configuration进行设置, 在AFNetWorking中对于图片网络请求设置了20M的内存缓存和150M的硬盘缓存:

+ (NSURLCache *)defaultURLCache {
return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
                                     diskCapacity:150 * 1024 * 1024
	                                       diskPath:@"com.alamofire.imagedownloader"];
}
复制代码

3.2.3 缓存策略

简介:缓存策略是指对网络请求缓存处理是使用缓存还是不使用:

NSURLRequestUseProtocolCachePolicy:对特定的URL请求使用网络协议中实现的缓存逻辑,这是默认的策略。
NSURLRequestReloadIgnoringLocalCacheData:数据需要从原始地址加载,不使用现有缓存。NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存。
NSURLRequestReturnCacheDataElseLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
NSURLRequestReturnCacheDataDontLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。
NSURLRequestReloadRevalidatingCacheData:从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
复制代码

3.2.4 图片硬盘缓存

简介:就是我们常说的把数据保存在本地,比如FMDB、CoreData、归档、NSUserDefaults、NSFileManager等等,这里就不多说了。 AFNetWorking3.0没有直接做图片硬盘缓存,而是通过URL缓存做的硬盘缓存。也就是说,如果内存缓存没有读取到图片,就会调用下载逻辑,通过下载缓存的内存缓存硬盘缓存来获取到已下载过的图片,如果没有下载过,就会重新下载。 如果我们自己做图片硬盘缓存建议使用NSFileManager,因为一般图片data会比较大,测试证明路径缓存会比放在数据库有更高的性能。

3.2.5 内存缓存映射的类:AFAutoPurgingImageCache

1) 简介

AFAutoPurgingImageCache是协议定义了一组API,用于同步地从缓存中添加、删除和获取图像。

2)主要API介绍如下

3)内存缓存流程图

4)内存缓存流程介绍

​ 每次下载完图片将图片添加到缓存中之前,先去检测一下当前已缓存memoryUsage的大小,如果超过总缓存memoryCapacity,则获取需要清理的缓存大小,需要清理的缓存大小 = 总缓存大小 - 预留缓存大小(memoryCapacity - preferredMemoryUsageAfterPurge),然后将缓存里面的图片按照插入时间排序,将最先插入的图片依次删除,如果删除缓存大小大于需要清理的缓存大小,则结束。

5)内存缓存核心源码解析
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
​    dispatch_barrier_async(self.synchronizationQueue, ^{
​        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
​        // 第一步:如果identifier的缓存对象存在,要为identifier赋予新的缓存对象,并将之前的缓存对象的内存减去
​        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
​        if (previousCachedImage != nil) {
​            self.currentMemoryUsage -= previousCachedImage.totalBytes;
​        }
​        self.cachedImages[identifier] = cacheImage;
​        self.currentMemoryUsage += cacheImage.totalBytes;
​    });

​    dispatch_barrier_async(self.synchronizationQueue, ^{
​        if (self.currentMemoryUsage > self.memoryCapacity) {
​            /* 第二步:计算出将要移除的内存大小 */
​            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
						/*第三步: 将所有的缓存按最后使用时间从远到近排列,然后依次删除,每次删除时都累加删除的缓存的内存大小,累加的内存大于等于要移除的内存大小时停止删除。*/
​            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];

​            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate" ascending:YES];
​            [sortedImages sortUsingDescriptors:@[sortDescriptor]];
​            UInt64 bytesPurged = 0;
​            for (AFCachedImage *cachedImage in sortedImages) {
​                [self.cachedImages removeObjectForKey:cachedImage.identifier];
​                bytesPurged += cachedImage.totalBytes;
​                if (bytesPurged >= bytesToPurge) {
​                    break ;
​                }
​            }
​            /* 第四步:更新当前使用的缓存内存大小 */
​            self.currentMemoryUsage -= bytesPurged;
​        }
​    });
}
复制代码

3.3 AFSecurityPolicy

首先介绍一下HTTPS:

HTTPS协议中的加密是用共享密钥加密与公开密钥加密的混合加密。共享密钥加密,加解密使用同一个密钥,即对称加密;公开密钥加密,分为公钥与私钥,公钥加密公开使用,而私钥则用于解密。HTTPS协议在交换密钥时使用公开密钥加密,在通信报文交换的过程中使用共享密钥。首先使用公开密钥加密的方式安全地交换将在稍后的共享密钥加密中要使用的密钥,在确保交换的密钥时安全的前提下,再使用共享密钥加密方式进行通讯交互。

3.3.1 简介

AFSecurityPolicy类只做了一件事,就是完成HTTPS认证,是对系统类库<Security/Security.h>的进一步封装;AFNetworking的默认证书认证流程是客户端单项认证,假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证,需要用户自行实现。

3.3.2 AFNetworking的单项认证流程

3.3.3 代码详细解析

AFSecurityPolicy的三种验证模式:
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
​    AFSSLPinningModeNone,               //代表无条件信任服务器的证书
​    AFSSLPinningModePublicKey,          //代表会对服务器返回的证书中的PublicKey进行验证
​    AFSSLPinningModeCertificate,        //代表会对服务器返回的证书同本地证书全部进行校验
}

接下来声明了四个属性:
  SSLPinningMode:返回SSL Pinning的类型,默认的是AFSSLPinningModeNone;
  pinnedCertificates:保存着所有的可用做校验的证书的集合,evaluateServerTrust:forDomain: 就会返回true,即通过校验;
  allowInvalidCertificates:使用允许无效或过期的证书,默认是NO不允许;
  validatesDomainName:是否验证证书中的域名domain,默认是YES;
复制代码

3.3.4 流程图

3.3.5 证书挑战核心代码解析

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
​    // 挑战处理类型为默认
​    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
​    __block NSURLCredential *credential = nil;
​    // 自定义的方法,用来如何应对服务器端的认证挑战
​    if (self.sessionDidReceiveAuthenticationChallenge) {
​        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
​    } else {
​        // 判断接收服务器挑战的方法是否是信任证书
​        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
​            // 去验证服务器端的证书是否安全,即HTTPS的单项认证,这是AF的默认处理的认证方式,其它的认证方式只能由我们自己的block去实现
​            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
​                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
​                if (credential) {
​                    // 证书挑战
​                    disposition = NSURLSessionAuthChallengeUseCredential;
​                } else {
​                    // 默认挑战方式 (不提供证书)
​                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
​                }
​            } else {
​                // 取消挑战 (取消连接)
​                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
​            }

​        } else {
​            // 默认挑战方式 (不提供证书)
​            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
​        }
​    }
    
​    /* 完成挑战,将信任凭证发送给服务器 */
​    if (completionHandler) {
​        completionHandler(disposition, credential);
​    }
}
复制代码

3.3.6 证书认证的核心代码解析

// 根据当前的安全策略,返回是否受信
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust

​                  forDomain:(NSString *)domain
{
​    /*
​     评估必须保证是一个有效的过程。
​     如果允许无效证书,但是没有证书或者采用AFSSLPinningModeNone模式,其他信息齐全的时候,这时候会被告知,这是一个无效的验证过程。
​    */
​    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
​        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
​        return NO;
​    }
​    // 创建安全策略,如果需要对域名进行验证,则创建附带入参域名的 SSL 安全策略。 否则创建一个基于 X.509 的安全策略。
​    NSMutableArray *policies = [NSMutableArray array];
​    if (self.validatesDomainName) {
​        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
​    } else {
​        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
​    }
​    // 将创建的安全策略加入到服务器给予的信任评估中。 这个评估认证将会和本地的证书或者公钥进行评估得出结果。
​    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
​    // 在 AFSSLPinningModeNone,不会进行公钥或者证书的认证。 只要确保服务器给的信任评估是有效的(能够获取到CA根证书)。  或者,如果用户设置允许无效证书,那么也会直接返回通过。
​    if (self.SSLPinningMode == AFSSLPinningModeNone) {
​        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
​    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
​        return NO;
​    }
​    // 根据不同的模式进行相应的认证操作
​    switch (self.SSLPinningMode) {
​        case AFSSLPinningModeNone:
​        default:
​            return NO;
​        // 上面已经对 AFSSLPinningModeNone 做了出了,这里直接当成默认的情况。返回NO
​          case AFSSLPinningModeCertificate: {
​            // 验证本地证书和服务器发过来的信任进行甄别。
​            // 这里本地使用的证书 "pinnedCertificates" 可能有很多个,于是转化成 CFData 放入数组。
​            NSMutableArray *pinnedCertificates = [NSMutableArray array];
​            for (NSData *certificateData in self.pinnedCertificates) {
​                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
​            }
​            // 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,
​              //即验证的数字证书是由锚点证书对应CA或子CA签发的,
​              //或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
​            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
​            if (!AFServerTrustIsValid(serverTrust)) {
​                return NO;
​            }
​            // 获取所有的服务器的证书链,注意这和 AnchorCertificates 是不相同的。

​            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
​            // 遍历服务器的证书链
​            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
​                // 如果证书链中包含了本地的证书,说明 serverTrust 是有效的服务器信任凭证。返回YES
​                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
​                    return YES;
​                }
​            }
​            return NO;
​        }
​        case AFSSLPinningModePublicKey: {

​            // 验证本地公钥和服务器发过来的信任进行甄别
​            // 获取服务器的公钥链
​            NSUInteger trustedPublicKeyCount = 0;
​            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
​            // 遍历公钥链 在本地查找合适的公钥,如果有至少一个符合,则为验证通过。
​            for (id trustChainPublicKey in publicKeys) {
​                for (id pinnedPublicKey in self.pinnedPublicKeys) {
​                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
​                        trustedPublicKeyCount += 1;
​                    }
​               }
​            }
​            return trustedPublicKeyCount > 0;
​        }
​    } 
​    return NO;
}
复制代码

作者简介

简介:闫名月,民生科技有限公司,用户体验技术部Firefly移动金融开发平台iOS开发工程师。