iOS 网络(3)——YTKNetwork

注意:在阅读本文之前建议先阅读《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 核心功能

上图所示为 YTKNetwork 核心功能的类的引用关系示意图。YTKNetwork 核心功能的基本思想是:

  • 把每一个网络请求封装成一个对象,每个请求对象继承自 YTKBaseRequest
  • 使用 YTKNetworkAgent 单例对象持有一个 AFHTTPSessionManager 对象来管理所有请求对象

YTKNetwork 核心功能主要涉及到 3 个类:

  • YTKBaseRequest
  • YTKNetworkConfig
  • YTKNetworkAgent

下面我们分别进行介绍。

YTKBaseRequest

YTKBaseRequest 类用于表示一个请求对象,它提供了一系列属性来充分表示一个网络请求。我们可以看一下它所定义的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@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 提供了高层级的网络抽象,体现在提供了一些高层级的配置方法,并允许用户通过覆写这些方法来进行自定义配置。一些常用的配置方法包括如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/// 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 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 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 主要包含以下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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: 方法来添加并执行请求对象。我们可以来看一下其内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (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: 方法内部会做一下几个步骤的工作:

  1. 初始化请求对象的关键属性 requestTask,即任务对象。
  2. 设置请求优先级
  3. 以任务对象的 taskIdentifier 为键,请求对象为值,建立映射关系,存入 记录表(即上图中的 _requestRecord,后文还会提到)。
  4. 执行请求,本质上是执行任务对象。

我们重点看一下第 1 步。这一步默认调用了 sessionTaskForRequest:error: 方法进行初始化。该方法内部实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (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: 方法。其内部实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (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: 方法中,初始化任务对象时会设置完成回调。

我们来看看完成回调做了什么工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- (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];
});
}
在这个回调中,主要做了一下几个工作:

  1. 根据任务对象的 taskIdentifier 从记录表 _requestRecord 中获取请求对象。
  2. 对于获取到的请求对象,根据不同的响应序列化器,序列化响应数据。
  3. 检查请求是否成功,并获取请求异常。
  4. 调用请求成功处理 或 调用请求失败处理
  5. 从记录表中删除请求对象。

其中第 4 步,无论是成功回调还是失败回调,都会依次调用代理对象实现的 requestFinished:requestFailed,以及请求对象的 successCompletionBlockfailureCompletionBlock

下载任务与缓存

关于下载任务,我们先来看一下上述 sessionTaskForRequest:error: 方法中,当请求对象的请求类型是 YTKRequestMethodGET 且设置了请求对象的 resumableDownloadPath 属性时,会调用 downloadTaskWithDownloadPath:requestSerializer:URLString:parameters:progress:error: 方法。该方法的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
- (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;
}
下载任务的创建过程中,有三个关键步骤:

  1. 确保下载存储路径是文件路径,而非目录路径。
  2. 读取 未完成下载暂存路径 的数据,并判断是否可继续下载。
  3. 如果可以继续下载,则创建请求继续下载;否则,创建请求重新下载。

从上面代码中,我们可以知道下载存储路径有两种可能:

  1. resumableDownloadPath
  2. resumableDownloadPath + filename

那么未完成下载暂存路径是什么呢?我们来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (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 链式请求

链式请求主要是通过 YTKNetwork 提供的两个类,并结合 YTKNetwork 核心功能实现的。这两类分别是:

  • YTKChainRequest
  • YTKChainRequestAgent

下面,我们分别介绍一下 YTKChainRequestYTKChainRequestAgent

YTKChainRequest

YTKChainRequest 继承自 NSObject,主要包含一下这些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 公开属性
@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 个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 获取链式请求队列
- (NSArray<YTKBaseRequest *> *)requestArray;

/// 添加实现了 YTKRequestAccessory 协议的对象
- (void)addAccessory:(id<YTKRequestAccessory>)accessory;

/// 开始执行链式请求
- (void)start;

/// 停止执行链式请求
- (void)stop;

/// 向链式请求队列中添加请求
- (void)addRequest:(YTKBaseRequest *)request callback:(nullable YTKChainCallback)callback;

我们通过源代码来看一下其中比较关键的 start 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (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

1
2
3
4
5
6
7
8
9
10
11
12
- (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 的代理都是链式请求 YTKChainRequestYTKChainRequest 实现了 YTKRequestDelegate 协议。每一个请求执行完成后,开始执行下一个请求。如果有一个请求失败,即整个链式请求失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (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 提供的方法如下:

1
2
3
4
5
6
7
+ (YTKChainRequestAgent *)sharedAgent;

/// 添加链式请求
- (void)addChainRequest:(YTKChainRequest *)request;

/// 移除链式请求
- (void)removeChainRequest:(YTKChainRequest *)request;

YTKNetwork 批量请求

YTKNetwork 批量请求的实现原理其实与链式请求的实现原理是一样的,也提供了两个类:

  • YTKBatchRequest
  • YTKBatchRequestAgent

不同之处在于,YTKBatchRequest 中的单个请求并不是 YTKBaseRequest 请求,而是它的子类 YTKRequest

我们来看看 YTKRequest 在父类 YTKBaseRequest 的基础上做了些什么。

YTKRequest

首先,我们来看一下 YTKRequest 所提供的外部属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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 中相关的缓存目录。首先来看以下几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

- (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 对象表示,其定义如下:

1
2
3
4
5
6
7
8
9
@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 主要用户验证缓存是否有效。验证方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (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 做了一个简单的封装,提供了面向对象的使用方法,使用起来也是非常简单。不过也存在缺点,就是每一个请求都需要定义一个类。

参考

  1. YTKNetwork