注意:在阅读本文之前建议先阅读《iOS 网络——NSURLSession》和《iOS 网络——AFNetworking》。
在《iOS 网络——AFNetworking》一文中我们介绍了基于 NSURLSession
进行封装的 AFNetworking 的核心功能原理。本文,我们进一步介绍基于 AFNetworking 进行封装的 YTKNetwork 开源框架。本文,我们通过阅读 YTKNetwork 源代码(版本号:2.0.4
)。
YTKNetwork 概述
YTKNetwork 是猿题库技术团队开源的一个网络请求框架,内部封装了 AFNetworking。YTKNetwork 实现了一套高层级的 API,提供更高层次的网络访问抽象。目前,猿题库公司的所有产品的 iOS 客户端都使用了 YTKNetwork,包括:猿题库、小猿搜题、猿辅导、小猿口算、斑马系列等。
YTKNetwork 架构
YTKNetwork 开源框架主要包含 3 个部分:
- YTKNetwork 核心功能
- YTKNetwork 链式请求
- YTKNetwork 批量请求
其中,链式请求和批量请求都是基于 YTKNetwork 的核心功能实现的。下面我们分别进行介绍。
YTKNetwork 核心功能
image上图所示为 YTKNetwork 核心功能的类的引用关系示意图。YTKNetwork 核心功能的基本思想是:
-
把每一个网络请求封装成一个对象,每个请求对象继承自
YTKBaseRequest
类。 -
使用
YTKNetworkAgent
单例对象持有一个AFHTTPSessionManager
对象来管理所有请求对象。
YTKNetwork 核心功能主要涉及到 3 个类:
YTKBaseRequest
YTKNetworkConfig
YTKNetworkAgent
下面我们分别进行介绍。
YTKBaseRequest
YTKBaseRequest
类用于表示一个请求对象,它提供了一系列属性来充分表示一个网络请求。我们可以看一下它所定义的属性:
@interface YTKBaseRequest : NSObject
/// 请求相关属性
@property (nonatomic, strong, readonly) NSURLSessionTask *requestTask;
@property (nonatomic, strong, readonly) NSURLRequest *currentRequest;
@property (nonatomic, strong, readonly) NSURLRequest *originalRequest;
@property (nonatomic, strong, readonly) NSHTTPURLResponse *response;
/// 响应相关属性
@property (nonatomic, readonly) NSInteger responseStatusCode;
@property (nonatomic, strong, readonly, nullable) NSDictionary *responseHeaders;
@property (nonatomic, strong, readonly, nullable) NSData *responseData;
@property (nonatomic, strong, readonly, nullable) NSString *responseString;
@property (nonatomic, strong, readonly, nullable) id responseObject;
@property (nonatomic, strong, readonly, nullable) id responseJSONObject;
/// 异常
@property (nonatomic, strong, readonly, nullable) NSError *error;
/// 状态
@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled;
@property (nonatomic, readonly, getter=isExecuting) BOOL executing;
/// 标识符,默认是 0
@property (nonatomic) NSInteger tag;
/// 附加信息,默认是 nil
@property (nonatomic, strong, nullable) NSDictionary *userInfo;
/// 代理
@property (nonatomic, weak, nullable) id<YTKRequestDelegate> delegate;
/// 成功/失败回调
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock successCompletionBlock;
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock failureCompletionBlock;
/// 用于在 POST 请求时构建 HTTP 主体。默认是 nil
@property (nonatomic, copy, nullable) AFConstructingBlock constructingBodyBlock;
/// 用于下载任务时指定本地下载路径
@property (nonatomic, strong, nullable) NSString *resumableDownloadPath;
/// 用于跟踪下载进度的回调
@property (nonatomic, copy, nullable) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;
/// 请求优先级
@property (nonatomic) YTKRequestPriority requestPriority;
/// YTKRequestAccessory 是一个协议,声明了三个方法,允许开发者分别在请求执行的三个阶段(start、willStop、didStop)调用。
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;
@end
事实上,YTKBaseRequest
类就是围绕 NSURLSessionTask
类进行封装的, requestTask
是它最重要的属性。YTKBaseRequest
的其他多个属性都源自于 requestTask
的属性。如:
-
currentRequest
:即requestTask.currentRequest
-
originalRequest
:即requestTask.originalRequest
-
response
:即requestTask.response
-
responseHeaders
:即requestTask.allHeaderFields
-
responseStatusCode
:即requestTask.statusCode
YTKBaseRequest
提供了高层级的网络抽象,体现在提供了一些高层级的配置方法,并允许用户通过覆写这些方法来进行自定义配置。一些常用的配置方法包括如下:
/// Base URL,因为一个应用程序中的网络请求的 BaseURL 几乎都是相同的。
- (NSString *)baseUrl {
return @"";
}
/// 请求的 URL 路径
- (NSString *)requestUrl {
return @"";
}
/// 网络请求的超时间隔。默认 60 秒
- (NSTimeInterval)requestTimeoutInterval {
return 60;
}
/// HTTP 请求方法。默认是 GET
- (YTKRequestMethod)requestMethod {
return YTKRequestMethodGET;
}
/// 请求序列化器类型。默认是 HTTP
- (YTKRequestSerializerType)requestSerializerType {
return YTKRequestSerializerTypeHTTP;
}
/// 响应序列化器类型。默认是 JSON
- (YTKResponseSerializerType)responseSerializerType {
return YTKResponseSerializerTypeJSON;
}
/// 请求参数对象,会根据配置的请求序列化器进行编码。
- (id)requestArgument {
return nil;
}
/// 是否允许蜂窝网络。默认 YES
- (BOOL)allowsCellularAccess {
return YES;
}
/// 是否使用 CDN。默认 NO
- (BOOL)useCDN {
return NO;
}
/// CDN URL。根据 useCDN 决定是否使用。
- (NSString *)cdnUrl {
return @"";
}
...
关于 YTKBaseRequest
对象的执行,它也提供了几个简单的方法以供开发者使用,如下所示。通过 start
方法,我们可以发现 YTKBaseRequest
被加入到了 YTKNetworkAgent
单例中。可见 YTKNetworkAgent
管理了多个 YTKBaseRequest
对象。
/// YTKBaseRequest 开始执行
- (void)start {
// 执行 YTKRequestAccessory 协议定义的 requestWillStart: 方法。
[self toggleAccessoriesWillStartCallBack];
// 将请求对象加入 YTKNetworkAgent 单例
[[YTKNetworkAgent sharedAgent] addRequest:self];
}
/// YTKBaseRequest 停止执行
- (void)stop {
// 执行 YTKRequestAccessory 协议定义的 requestWillStop: 方法。
[self toggleAccessoriesWillStopCallBack];
self.delegate = nil;
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
// 执行 YTKRequestAccessory 协议定义的 requestDidStop: 方法。
[self toggleAccessoriesDidStopCallBack];
}
/// 一个便利方法。执行 YTKBaseRequest。
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
failure:(YTKRequestCompletionBlock)failure {
[self setCompletionBlockWithSuccess:success failure:failure];
[self start];
}
YTKNetworkConfig
YTKNetworkConfig
是用于 YTKNetworkAgent
初始化的配置对象,是一个 单例。
YTKNetworkConfig
主要包含以下属性:
@interface YTKNetworkConfig : NSObject
/// 请求的 Base URL。默认是 ""
@property (nonatomic, strong) NSString *baseUrl;
/// CDN URL. 默认是 ""
@property (nonatomic, strong) NSString *cdnUrl;
/// URL 过滤器。YTKUrlFilterProtocol 声明的 filterUrl:withRequest: 方法会返回最终被使用的 URL
@property (nonatomic, strong, readonly) NSArray<id<YTKUrlFilterProtocol>> *urlFilters;
/// 缓存路径过滤器。YTKCacheDirPathFilterProtocol 声明的 filterCacheDirPath:withRequest: 方法会返回最终被使用的缓存路径。
@property (nonatomic, strong, readonly) NSArray<id<YTKCacheDirPathFilterProtocol>> *cacheDirPathFilters;
/// 安全策略。
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
/// 是否打印调试日志信息。默认是 NO
@property (nonatomic) BOOL debugLogEnabled;
/// 会话配置对象
@property (nonatomic, strong) NSURLSessionConfiguration* sessionConfiguration;
@end
YTKNetworkConfig
持有了一个 NSURLSessionConfiguration
类型的属性 sessionConfiguration
,用于 YTKNetworkAgent
中初始化 AFHTTPSessionManager
(本质上是用于初始化 NSURLSession
)。
YTKNetworkAgent
YTKNetworkAgent
的内部结构如下图所示。下面我们将以该图为指导进行介绍。
初始化
YTKNetworkAgent
初始化过程会使用 YTKNetworkConfig
单例对象(配置对象)。使用配置对象的会话配置对象 sessionConfiguration
初始化会话管理器 AFHTTPSessionManager
。
YTKNetwork
框架默认只能使用 YTKNetworkAgent
单例对象。
添加并执行请求
YTKNetworkAgent
提供了 addRequest:
方法来添加并执行请求对象。我们可以来看一下其内部实现。
- (void)addRequest:(YTKBaseRequest *)request {
NSParameterAssert(request != nil);
NSError * __autoreleasing requestSerializationError = nil;
// 初始化请求对象的关键属性 requestTask,即任务对象
NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
if (customUrlRequest) {
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
// 完成回调
[self handleRequestResult:dataTask responseObject:responseObject error:error];
}];
request.requestTask = dataTask;
} else {
// 默认方式
request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
}
// 请求序列化异常处理
if (requestSerializationError) {
[self requestDidFailWithRequest:request error:requestSerializationError];
return;
}
NSAssert(request.requestTask != nil, @"requestTask should not be nil");
// 设置请求优先级
// !!Available on iOS 8 +
if ([request.requestTask respondsToSelector:@selector(priority)]) {
switch (request.requestPriority) {
case YTKRequestPriorityHigh:
request.requestTask.priority = NSURLSessionTaskPriorityHigh;
break;
case YTKRequestPriorityLow:
request.requestTask.priority = NSURLSessionTaskPriorityLow;
break;
case YTKRequestPriorityDefault:
/*!!fall through*/
default:
request.requestTask.priority = NSURLSessionTaskPriorityDefault;
break;
}
}
YTKLog(@"Add request: %@", NSStringFromClass([request class]));
// 将 请求对象 加入记录表
[self addRequestToRecord:request];
// 执行请求,即执行任务对象
[request.requestTask resume];
}
addRequest:
方法内部会做一下几个步骤的工作:
- 初始化请求对象的关键属性
requestTask
,即任务对象。 - 设置请求优先级
- 以任务对象的
taskIdentifier
为键,请求对象为值,建立映射关系,存入 记录表(即上图中的_requestRecord
,后文还会提到)。 - 执行请求,本质上是执行任务对象。
我们重点看一下第 1 步。这一步默认调用了 sessionTaskForRequest:error:
方法进行初始化。该方法内部实现如下:
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
// 获取请求方法
YTKRequestMethod method = [request requestMethod];
// 获取请求URL
NSString *url = [self buildRequestUrl:request];
// 获取请求参数
id param = request.requestArgument;
// 获取 HTTP 主体
AFConstructingBlock constructingBlock = [request constructingBodyBlock];
// 获取请求序列化器
AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];
// 根据请求方法以及下载路径值,初始化相应的任务对象
switch (method) {
case YTKRequestMethodGET:
if (request.resumableDownloadPath) {
return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];
} else {
return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
}
case YTKRequestMethodPOST:
return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];
case YTKRequestMethodHEAD:
return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPUT:
return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodDELETE:
return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPATCH:
return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error];
}
}
sessionTaskForRequest:error:
方法会根据请求对象的 requestMethod
初始化相应的任务对象。以 POST
请求为例,这里最终会调用 dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error:
方法。其内部实现如下:
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error {
NSMutableURLRequest *request = nil;
// 初始化一个 URLRequest 对象
if (block) {
request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
} else {
request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
}
// 利用 URLRequest 对象,初始化任务对象,并返回该任务对象
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:request
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
// 设置完成回调
[self handleRequestResult:dataTask responseObject:responseObject error:_error];
}];
return dataTask;
}
dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error:
方法根据入参初始化一个 URLRequest 对象,并使用该对象初始化一个任务对象,并返回该任务对象。
完成回调
上述 dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error:
方法中,初始化任务对象时会设置完成回调。
我们来看看完成回调做了什么工作。
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
Lock();
// 根据任务对象的 taskIdentifier 从记录表中获取请求对象。
YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
Unlock();
if (!request) {
return;
}
YTKLog(@"Finished Request: %@", NSStringFromClass([request class]));
NSError * __autoreleasing serializationError = nil;
NSError * __autoreleasing validationError = nil;
NSError *requestError = nil;
BOOL succeed = NO;
// 根据不同的响应序列化器,序列化响应数据
request.responseObject = responseObject;
if ([request.responseObject isKindOfClass:[NSData class]]) {
request.responseData = responseObject;
request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];
switch (request.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Default serializer. Do nothing.
break;
case YTKResponseSerializerTypeJSON:
request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
request.responseJSONObject = request.responseObject;
break;
case YTKResponseSerializerTypeXMLParser:
request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
break;
}
}
// 检查请求是否成功,并获取请求异常
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}
// 调用请求成功处理 或 调用请求失败处理
if (succeed) {
[self requestDidSucceedWithRequest:request];
} else {
[self requestDidFailWithRequest:request error:requestError];
}
// 从记录表中删除请求对象
dispatch_async(dispatch_get_main_queue(), ^{
[self removeRequestFromRecord:request];
[request clearCompletionBlock];
});
}
在这个回调中,主要做了一下几个工作:
- 根据任务对象的
taskIdentifier
从记录表_requestRecord
中获取请求对象。 - 对于获取到的请求对象,根据不同的响应序列化器,序列化响应数据。
- 检查请求是否成功,并获取请求异常。
- 调用请求成功处理 或 调用请求失败处理
- 从记录表中删除请求对象。
其中第 4 步,无论是成功回调还是失败回调,都会依次调用代理对象实现的 requestFinished:
或 requestFailed
,以及请求对象的 successCompletionBlock
或 failureCompletionBlock
。
下载任务与缓存
关于下载任务,我们先来看一下上述 sessionTaskForRequest:error:
方法中,当请求对象的请求类型是 YTKRequestMethodGET
且设置了请求对象的 resumableDownloadPath
属性时,会调用 downloadTaskWithDownloadPath:requestSerializer:URLString:parameters:progress:error:
方法。该方法的具体实现如下:
- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
error:(NSError * _Nullable __autoreleasing *)error {
// 使用请求参数、请求URL、请求类型,初始化 URLRequest 对象
NSMutableURLRequest *urlRequest = [requestSerializer requestWithMethod:@"GET" URLString:URLString parameters:parameters error:error];
NSString *downloadTargetPath;
// 检查 resumableDownloadPath 指定的下载存储路径是否是目录
BOOL isDirectory;
if(![[NSFileManager defaultManager] fileExistsAtPath:downloadPath isDirectory:&isDirectory]) {
isDirectory = NO;
}
// 预处理下载存储路径,确保不是目录,而是文件
if (isDirectory) {
NSString *fileName = [urlRequest.URL lastPathComponent];
downloadTargetPath = [NSString pathWithComponents:@[downloadPath, fileName]];
} else {
downloadTargetPath = downloadPath;
}
// 清理该路径原有的文件
if ([[NSFileManager defaultManager] fileExistsAtPath:downloadTargetPath]) {
[[NSFileManager defaultManager] removeItemAtPath:downloadTargetPath error:nil];
}
// 检查未完成下载暂存路径是否有数据 并 读取此路径暂存的数据
BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:[self incompleteDownloadTempPathForDownloadPath:downloadPath].path];
NSData *data = [NSData dataWithContentsOfURL:[self incompleteDownloadTempPathForDownloadPath:downloadPath]];
BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data];
BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid;
BOOL resumeSucceeded = NO;
__block NSURLSessionDownloadTask *downloadTask = nil;
if (canBeResumed) {
// 对于可恢复的下载请求,使用已下载的数据初始化一个下载任务,继续发起下载请求。
@try {
downloadTask = [_manager downloadTaskWithResumeData:data progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
} completionHandler:
^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self handleRequestResult:downloadTask responseObject:filePath error:error];
}];
resumeSucceeded = YES;
} @catch (NSException *exception) {
YTKLog(@"Resume download failed, reason = %@", exception.reason);
resumeSucceeded = NO;
}
}
if (!resumeSucceeded) {
// 如果尝试继续下载失败,则创建一个下载任务,重新开始发起下载请求。
downloadTask = [_manager downloadTaskWithRequest:urlRequest progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
// 指定下载的存储路径
return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
} completionHandler:
^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self handleRequestResult:downloadTask responseObject:filePath error:error];
}];
}
return downloadTask;
}
下载任务的创建过程中,有三个关键步骤:
- 确保下载存储路径是文件路径,而非目录路径。
- 读取 未完成下载暂存路径 的数据,并判断是否可继续下载。
- 如果可以继续下载,则创建请求继续下载;否则,创建请求重新下载。
从上面代码中,我们可以知道下载存储路径有两种可能:
resumableDownloadPath
-
resumableDownloadPath
+filename
那么未完成下载暂存路径是什么呢?我们来看代码:
- (NSString *)incompleteDownloadTempCacheFolder {
NSFileManager *fileManager = [NSFileManager new];
static NSString *cacheFolder;
if (!cacheFolder) {
NSString *cacheDir = NSTemporaryDirectory();
cacheFolder = [cacheDir stringByAppendingPathComponent:kYTKNetworkIncompleteDownloadFolderName];
}
NSError *error = nil;
if(![fileManager createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error]) {
YTKLog(@"Failed to create cache directory at %@", cacheFolder);
cacheFolder = nil;
}
return cacheFolder;
}
- (NSURL *)incompleteDownloadTempPathForDownloadPath:(NSString *)downloadPath {
NSString *tempPath = nil;
NSString *md5URLString = [YTKNetworkUtils md5StringFromString:downloadPath];
tempPath = [[self incompleteDownloadTempCacheFolder] stringByAppendingPathComponent:md5URLString];
return [NSURL fileURLWithPath:tempPath];
}
从上述代码,可以看出未完成下载暂存路径其实就是:
-
NSTemporaryDirectory()
+ 下载存储路径目录的 md5 值
注意,NSTemporaryDirectory()
目录就是 UNIX 中的 /tmp
目录,该目录下的文件会在系统重启后被清空。
YTKNetwork 链式请求
image链式请求主要是通过 YTKNetwork 提供的两个类,并结合 YTKNetwork 核心功能实现的。这两类分别是:
YTKChainRequest
YTKChainRequestAgent
下面,我们分别介绍一下 YTKChainRequest
和 YTKChainRequestAgent
。
YTKChainRequest
YTKChainRequest
继承自 NSObject
,主要包含一下这些属性。
/// 公开属性
@interface YTKChainRequest : NSObject
/// 代理对象
@property (nonatomic, weak, nullable) id<YTKChainRequestDelegate> delegate;
/// YTKRequestAccessory 是一个协议,声明了三个方法,允许开发者分别在请求执行的三个阶段(start、willStop、didStop)调用。
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;
@end
/// ------------------------------------------
/// 私有属性
@interface YTKChainRequest()<YTKRequestDelegate>
/// 链式请求队列
@property (strong, nonatomic) NSMutableArray<YTKBaseRequest *> *requestArray;
/// 链式请求回调队列
@property (strong, nonatomic) NSMutableArray<YTKChainCallback> *requestCallbackArray;
///
@property (assign, nonatomic) NSUInteger nextRequestIndex;
@property (strong, nonatomic) YTKChainCallback emptyCallback;
@end
YTKChainRequest
提供了 4 个方法。
/// 获取链式请求队列
- (NSArray<YTKBaseRequest *> *)requestArray;
/// 添加实现了 YTKRequestAccessory 协议的对象
- (void)addAccessory:(id<YTKRequestAccessory>)accessory;
/// 开始执行链式请求
- (void)start;
/// 停止执行链式请求
- (void)stop;
/// 向链式请求队列中添加请求
- (void)addRequest:(YTKBaseRequest *)request callback:(nullable YTKChainCallback)callback;
我们通过源代码来看一下其中比较关键的 start
方法。
- (void)start {
// 判断链式请求是否已经启动
if (_nextRequestIndex > 0) {
YTKLog(@"Error! Chain request has already started.");
return;
}
// 链式请求队列非空,则开始执行请求
if ([_requestArray count] > 0) {
[self toggleAccessoriesWillStartCallBack];
[self startNextRequest];
[[YTKChainRequestAgent sharedAgent] addChainRequest:self];
} else {
YTKLog(@"Error! Chain request array is empty.");
}
}
start
方法内部首先判断链式请求是否已经启动,这是通过请求索引 _nextRequestIndex
来判断的。如果链式请求未启动,则开始执行链式请求,这里调用了一个关键的方法 startNextRequest
。
- (BOOL)startNextRequest {
if (_nextRequestIndex < [_requestArray count]) {
YTKBaseRequest *request = _requestArray[_nextRequestIndex];
_nextRequestIndex++;
request.delegate = self;
[request clearCompletionBlock];
[request start];
return YES;
} else {
return NO;
}
}
每调用一次 startNextRequest
,会移动请求索引、设置请求代理并执行。
链式请求中的每一个请求 YTKBaseRequest
的代理都是链式请求 YTKChainRequest
。YTKChainRequest
实现了 YTKRequestDelegate
协议。每一个请求执行完成后,开始执行下一个请求。如果有一个请求失败,即整个链式请求失败。
- (void)requestFinished:(YTKBaseRequest *)request {
NSUInteger currentRequestIndex = _nextRequestIndex - 1;
YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
callback(self, request);
// 执行下一个请求
if (![self startNextRequest]) {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
// 所有请求执行完毕,调用代理方法 chainRequestFinished:
[_delegate chainRequestFinished:self];
[[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
}
[self toggleAccessoriesDidStopCallBack];
}
}
- (void)requestFailed:(YTKBaseRequest *)request {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
// 有一个请求失败,即调用 chainRequestFailed:
[_delegate chainRequestFailed:self failedBaseRequest:request];
[[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
}
[self toggleAccessoriesDidStopCallBack];
}
YTKChainRequestAgent
YTKChainRequestAgent
的作用非常简单,就是作为一个单例,持有多个链式请求。YTKChainRequestAgent
提供的方法如下:
+ (YTKChainRequestAgent *)sharedAgent;
/// 添加链式请求
- (void)addChainRequest:(YTKChainRequest *)request;
/// 移除链式请求
- (void)removeChainRequest:(YTKChainRequest *)request;
YTKNetwork 批量请求
imageYTKNetwork
批量请求的实现原理其实与链式请求的实现原理是一样的,也提供了两个类:
YTKBatchRequest
YTKBatchRequestAgent
不同之处在于,YTKBatchRequest
中的单个请求并不是 YTKBaseRequest
请求,而是它的子类 YTKRequest
。
我们来看看 YTKRequest
在父类 YTKBaseRequest
的基础上做了些什么。
YTKRequest
首先,我们来看一下 YTKRequest
所提供的外部属性和方法。
@interface YTKRequest : YTKBaseRequest
// 是否忽略缓存
@property (nonatomic) BOOL ignoreCache;
/// 请求响应数据是否来自本地缓存
- (BOOL)loadCacheWithError:(NSError * __autoreleasing *)error;
/// 请求不使用缓存数据
- (void)startWithoutCache;
/// 将响应数据保存至缓存
- (void)saveResponseDataToCacheFile:(NSData *)data;
#pragma mark - Subclass Override
/// 缓存时间
- (NSInteger)cacheTimeInSeconds;
/// 缓存版本
- (long long)cacheVersion;
/// 缓存敏感数据,用于验证缓存是否失效
- (nullable id)cacheSensitiveData;
/// 是否异步写入缓存
- (BOOL)writeCacheAsynchronously;
@end
很明显,YTKRequest
在父类的基础上支持了本地缓存功能。
缓存目录
我们来重点看一下 YTKRequest
中相关的缓存目录。首先来看以下几个方法:
- (NSString *)cacheBasePath {
NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];
// Filter cache base path
NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
if (filters.count > 0) {
for (id<YTKCacheDirPathFilterProtocol> f in filters) {
path = [f filterCacheDirPath:path withRequest:self];
}
}
[self createDirectoryIfNeeded:path];
return path;
}
- (NSString *)cacheFileName {
NSString *requestUrl = [self requestUrl];
NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
(long)[self requestMethod], baseUrl, requestUrl, argument];
NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
return cacheFileName;
}
- (NSString *)cacheFilePath {
NSString *cacheFileName = [self cacheFileName];
NSString *path = [self cacheBasePath];
path = [path stringByAppendingPathComponent:cacheFileName];
return path;
}
- (NSString *)cacheMetadataFilePath {
NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata", [self cacheFileName]];
NSString *path = [self cacheBasePath];
path = [path stringByAppendingPathComponent:cacheMetadataFileName];
return path;
}
默认情况下,cacheBasePath
方法返回的基本路径是:/Library/LazyRequestCache
。
cacheFileName
方法则根据请求的基本信息生成缓存的文件名:Method:xxx Host:xxx Url:xxx Argument:xxx
,并使用 md5 进行编码。
cacheFilePath
则是请求数据的完整存储路径:/Library/LazyRequestCache/
+ md5(Method:xxx Host:xxx Url:xxx Argument:xxx
)。
cacheMetadataFilePath
则存储了缓存元数据,其路径是:cacheFilePath
+ .medata
。
缓存元数据使用 YTKCacheMetaData
对象表示,其定义如下:
@interface YTKCacheMetadata : NSObject<NSSecureCoding>
@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;
@end
YTKCacheMetaData
主要用户验证缓存是否有效。验证方法如下:
- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// Date
NSDate *creationDate = self.cacheMetadata.creationDate;
NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
if (duration < 0 || duration > [self cacheTimeInSeconds]) {
// ...
return NO;
}
// Version
long long cacheVersionFileContent = self.cacheMetadata.version;
if (cacheVersionFileContent != [self cacheVersion]) {
// ...
return NO;
}
// Sensitive data
NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
if (sensitiveDataString || currentSensitiveDataString) {
// If one of the strings is nil, short-circuit evaluation will trigger
if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
// ...
return NO;
}
}
// App version
NSString *appVersionString = self.cacheMetadata.appVersionString;
NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
if (appVersionString || currentAppVersionString) {
if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
// ...
return NO;
}
}
return YES;
}
总结
YTKNetwork 设计原理非常简单,仅仅是对 AFNetworking 做了一个简单的封装,提供了面向对象的使用方法,使用起来也是非常简单。不过也存在缺点,就是每一个请求都需要定义一个类。
参考
(完)
网友评论