注意:在閱讀本文之前建議先閱讀《iOS 網(wǎng)絡(luò)——NSURLSession》和《iOS 網(wǎng)絡(luò)——AFNetworking》。
在《iOS 網(wǎng)絡(luò)——AFNetworking》一文中我們介紹了基于 NSURLSession 進(jìn)行封裝的 AFNetworking 的核心功能原理。本文,我們進(jìn)一步介紹基于 AFNetworking 進(jìn)行封裝的 YTKNetwork 開源框架。本文,我們通過閱讀 YTKNetwork 源代碼(版本號(hào):2.0.4)。
YTKNetwork 概述
YTKNetwork 是猿題庫技術(shù)團(tuán)隊(duì)開源的一個(gè)網(wǎng)絡(luò)請(qǐng)求框架,內(nèi)部封裝了 AFNetworking。YTKNetwork 實(shí)現(xiàn)了一套高層級(jí)的 API,提供更高層次的網(wǎng)絡(luò)訪問抽象。目前,猿題庫公司的所有產(chǎn)品的 iOS 客戶端都使用了 YTKNetwork,包括:猿題庫、小猿搜題、猿輔導(dǎo)、小猿口算、斑馬系列等。
YTKNetwork 架構(gòu)
YTKNetwork 開源框架主要包含 3 個(gè)部分:
- YTKNetwork 核心功能
- YTKNetwork 鏈?zhǔn)秸?qǐng)求
- YTKNetwork 批量請(qǐng)求
其中,鏈?zhǔn)秸?qǐng)求和批量請(qǐng)求都是基于 YTKNetwork 的核心功能實(shí)現(xiàn)的。下面我們分別進(jìn)行介紹。
YTKNetwork 核心功能

上圖所示為 YTKNetwork 核心功能的類的引用關(guān)系示意圖。YTKNetwork 核心功能的基本思想是:
-
把每一個(gè)網(wǎng)絡(luò)請(qǐng)求封裝成一個(gè)對(duì)象,每個(gè)請(qǐng)求對(duì)象繼承自
YTKBaseRequest類。 -
使用
YTKNetworkAgent單例對(duì)象持有一個(gè)AFHTTPSessionManager對(duì)象來管理所有請(qǐng)求對(duì)象。
YTKNetwork 核心功能主要涉及到 3 個(gè)類:
YTKBaseRequestYTKNetworkConfigYTKNetworkAgent
下面我們分別進(jìn)行介紹。
YTKBaseRequest
YTKBaseRequest 類用于表示一個(gè)請(qǐng)求對(duì)象,它提供了一系列屬性來充分表示一個(gè)網(wǎng)絡(luò)請(qǐng)求。我們可以看一下它所定義的屬性:
@interface YTKBaseRequest : NSObject
/// 請(qǐng)求相關(guān)屬性
@property (nonatomic, strong, readonly) NSURLSessionTask *requestTask;
@property (nonatomic, strong, readonly) NSURLRequest *currentRequest;
@property (nonatomic, strong, readonly) NSURLRequest *originalRequest;
@property (nonatomic, strong, readonly) NSHTTPURLResponse *response;
/// 響應(yīng)相關(guān)屬性
@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;
/// 狀態(tài)
@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled;
@property (nonatomic, readonly, getter=isExecuting) BOOL executing;
/// 標(biāo)識(shí)符,默認(rèn)是 0
@property (nonatomic) NSInteger tag;
/// 附加信息,默認(rèn)是 nil
@property (nonatomic, strong, nullable) NSDictionary *userInfo;
/// 代理
@property (nonatomic, weak, nullable) id<YTKRequestDelegate> delegate;
/// 成功/失敗回調(diào)
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock successCompletionBlock;
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock failureCompletionBlock;
/// 用于在 POST 請(qǐng)求時(shí)構(gòu)建 HTTP 主體。默認(rèn)是 nil
@property (nonatomic, copy, nullable) AFConstructingBlock constructingBodyBlock;
/// 用于下載任務(wù)時(shí)指定本地下載路徑
@property (nonatomic, strong, nullable) NSString *resumableDownloadPath;
/// 用于跟蹤下載進(jìn)度的回調(diào)
@property (nonatomic, copy, nullable) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;
/// 請(qǐng)求優(yōu)先級(jí)
@property (nonatomic) YTKRequestPriority requestPriority;
/// YTKRequestAccessory 是一個(gè)協(xié)議,聲明了三個(gè)方法,允許開發(fā)者分別在請(qǐng)求執(zhí)行的三個(gè)階段(start、willStop、didStop)調(diào)用。
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;
@end
事實(shí)上,YTKBaseRequest 類就是圍繞 NSURLSessionTask 類進(jìn)行封裝的, requestTask 是它最重要的屬性。YTKBaseRequest 的其他多個(gè)屬性都源自于 requestTask 的屬性。如:
-
currentRequest:即requestTask.currentRequest -
originalRequest:即requestTask.originalRequest -
response:即requestTask.response -
responseHeaders:即requestTask.allHeaderFields -
responseStatusCode:即requestTask.statusCode
YTKBaseRequest 提供了高層級(jí)的網(wǎng)絡(luò)抽象,體現(xiàn)在提供了一些高層級(jí)的配置方法,并允許用戶通過覆寫這些方法來進(jìn)行自定義配置。一些常用的配置方法包括如下:
/// Base URL,因?yàn)橐粋€(gè)應(yīng)用程序中的網(wǎng)絡(luò)請(qǐng)求的 BaseURL 幾乎都是相同的。
- (NSString *)baseUrl {
return @"";
}
/// 請(qǐng)求的 URL 路徑
- (NSString *)requestUrl {
return @"";
}
/// 網(wǎng)絡(luò)請(qǐng)求的超時(shí)間隔。默認(rèn) 60 秒
- (NSTimeInterval)requestTimeoutInterval {
return 60;
}
/// HTTP 請(qǐng)求方法。默認(rèn)是 GET
- (YTKRequestMethod)requestMethod {
return YTKRequestMethodGET;
}
/// 請(qǐng)求序列化器類型。默認(rèn)是 HTTP
- (YTKRequestSerializerType)requestSerializerType {
return YTKRequestSerializerTypeHTTP;
}
/// 響應(yīng)序列化器類型。默認(rèn)是 JSON
- (YTKResponseSerializerType)responseSerializerType {
return YTKResponseSerializerTypeJSON;
}
/// 請(qǐng)求參數(shù)對(duì)象,會(huì)根據(jù)配置的請(qǐng)求序列化器進(jìn)行編碼。
- (id)requestArgument {
return nil;
}
/// 是否允許蜂窩網(wǎng)絡(luò)。默認(rèn) YES
- (BOOL)allowsCellularAccess {
return YES;
}
/// 是否使用 CDN。默認(rèn) NO
- (BOOL)useCDN {
return NO;
}
/// CDN URL。根據(jù) useCDN 決定是否使用。
- (NSString *)cdnUrl {
return @"";
}
...
關(guān)于 YTKBaseRequest 對(duì)象的執(zhí)行,它也提供了幾個(gè)簡(jiǎn)單的方法以供開發(fā)者使用,如下所示。通過 start 方法,我們可以發(fā)現(xiàn) YTKBaseRequest 被加入到了 YTKNetworkAgent 單例中。可見 YTKNetworkAgent 管理了多個(gè) YTKBaseRequest 對(duì)象。
/// YTKBaseRequest 開始執(zhí)行
- (void)start {
// 執(zhí)行 YTKRequestAccessory 協(xié)議定義的 requestWillStart: 方法。
[self toggleAccessoriesWillStartCallBack];
// 將請(qǐng)求對(duì)象加入 YTKNetworkAgent 單例
[[YTKNetworkAgent sharedAgent] addRequest:self];
}
/// YTKBaseRequest 停止執(zhí)行
- (void)stop {
// 執(zhí)行 YTKRequestAccessory 協(xié)議定義的 requestWillStop: 方法。
[self toggleAccessoriesWillStopCallBack];
self.delegate = nil;
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
// 執(zhí)行 YTKRequestAccessory 協(xié)議定義的 requestDidStop: 方法。
[self toggleAccessoriesDidStopCallBack];
}
/// 一個(gè)便利方法。執(zhí)行 YTKBaseRequest。
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
failure:(YTKRequestCompletionBlock)failure {
[self setCompletionBlockWithSuccess:success failure:failure];
[self start];
}
YTKNetworkConfig
YTKNetworkConfig 是用于 YTKNetworkAgent 初始化的配置對(duì)象,是一個(gè) 單例。
YTKNetworkConfig 主要包含以下屬性:
@interface YTKNetworkConfig : NSObject
/// 請(qǐng)求的 Base URL。默認(rèn)是 ""
@property (nonatomic, strong) NSString *baseUrl;
/// CDN URL. 默認(rèn)是 ""
@property (nonatomic, strong) NSString *cdnUrl;
/// URL 過濾器。YTKUrlFilterProtocol 聲明的 filterUrl:withRequest: 方法會(huì)返回最終被使用的 URL
@property (nonatomic, strong, readonly) NSArray<id<YTKUrlFilterProtocol>> *urlFilters;
/// 緩存路徑過濾器。YTKCacheDirPathFilterProtocol 聲明的 filterCacheDirPath:withRequest: 方法會(huì)返回最終被使用的緩存路徑。
@property (nonatomic, strong, readonly) NSArray<id<YTKCacheDirPathFilterProtocol>> *cacheDirPathFilters;
/// 安全策略。
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
/// 是否打印調(diào)試日志信息。默認(rèn)是 NO
@property (nonatomic) BOOL debugLogEnabled;
/// 會(huì)話配置對(duì)象
@property (nonatomic, strong) NSURLSessionConfiguration* sessionConfiguration;
@end
YTKNetworkConfig 持有了一個(gè) NSURLSessionConfiguration 類型的屬性 sessionConfiguration,用于 YTKNetworkAgent 中初始化 AFHTTPSessionManager(本質(zhì)上是用于初始化 NSURLSession)。
YTKNetworkAgent
YTKNetworkAgent 的內(nèi)部結(jié)構(gòu)如下圖所示。下面我們將以該圖為指導(dǎo)進(jìn)行介紹。

初始化
YTKNetworkAgent 初始化過程會(huì)使用 YTKNetworkConfig 單例對(duì)象(配置對(duì)象)。使用配置對(duì)象的會(huì)話配置對(duì)象 sessionConfiguration 初始化會(huì)話管理器 AFHTTPSessionManager。
YTKNetwork 框架默認(rèn)只能使用 YTKNetworkAgent 單例對(duì)象。
添加并執(zhí)行請(qǐng)求
YTKNetworkAgent 提供了 addRequest: 方法來添加并執(zhí)行請(qǐng)求對(duì)象。我們可以來看一下其內(nèi)部實(shí)現(xiàn)。
- (void)addRequest:(YTKBaseRequest *)request {
NSParameterAssert(request != nil);
NSError * __autoreleasing requestSerializationError = nil;
// 初始化請(qǐng)求對(duì)象的關(guān)鍵屬性 requestTask,即任務(wù)對(duì)象
NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
if (customUrlRequest) {
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
// 完成回調(diào)
[self handleRequestResult:dataTask responseObject:responseObject error:error];
}];
request.requestTask = dataTask;
} else {
// 默認(rèn)方式
request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
}
// 請(qǐng)求序列化異常處理
if (requestSerializationError) {
[self requestDidFailWithRequest:request error:requestSerializationError];
return;
}
NSAssert(request.requestTask != nil, @"requestTask should not be nil");
// 設(shè)置請(qǐng)求優(yōu)先級(jí)
// !!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]));
// 將 請(qǐng)求對(duì)象 加入記錄表
[self addRequestToRecord:request];
// 執(zhí)行請(qǐng)求,即執(zhí)行任務(wù)對(duì)象
[request.requestTask resume];
}
addRequest: 方法內(nèi)部會(huì)做一下幾個(gè)步驟的工作:
- 初始化請(qǐng)求對(duì)象的關(guān)鍵屬性
requestTask,即任務(wù)對(duì)象。 - 設(shè)置請(qǐng)求優(yōu)先級(jí)
- 以任務(wù)對(duì)象的
taskIdentifier為鍵,請(qǐng)求對(duì)象為值,建立映射關(guān)系,存入 記錄表(即上圖中的_requestRecord,后文還會(huì)提到)。 - 執(zhí)行請(qǐng)求,本質(zhì)上是執(zhí)行任務(wù)對(duì)象。
我們重點(diǎn)看一下第 1 步。這一步默認(rèn)調(diào)用了 sessionTaskForRequest:error: 方法進(jìn)行初始化。該方法內(nèi)部實(shí)現(xiàn)如下:
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
// 獲取請(qǐng)求方法
YTKRequestMethod method = [request requestMethod];
// 獲取請(qǐng)求URL
NSString *url = [self buildRequestUrl:request];
// 獲取請(qǐng)求參數(shù)
id param = request.requestArgument;
// 獲取 HTTP 主體
AFConstructingBlock constructingBlock = [request constructingBodyBlock];
// 獲取請(qǐng)求序列化器
AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];
// 根據(jù)請(qǐng)求方法以及下載路徑值,初始化相應(yīng)的任務(wù)對(duì)象
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: 方法會(huì)根據(jù)請(qǐng)求對(duì)象的 requestMethod 初始化相應(yīng)的任務(wù)對(duì)象。以 POST 請(qǐng)求為例,這里最終會(huì)調(diào)用 dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error: 方法。其內(nèi)部實(shí)現(xiàn)如下:
- (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;
// 初始化一個(gè) URLRequest 對(duì)象
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 對(duì)象,初始化任務(wù)對(duì)象,并返回該任務(wù)對(duì)象
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:request
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
// 設(shè)置完成回調(diào)
[self handleRequestResult:dataTask responseObject:responseObject error:_error];
}];
return dataTask;
}
dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error: 方法根據(jù)入?yún)⒊跏蓟粋€(gè) URLRequest 對(duì)象,并使用該對(duì)象初始化一個(gè)任務(wù)對(duì)象,并返回該任務(wù)對(duì)象。
完成回調(diào)
上述 dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error: 方法中,初始化任務(wù)對(duì)象時(shí)會(huì)設(shè)置完成回調(diào)。
我們來看看完成回調(diào)做了什么工作。
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
Lock();
// 根據(jù)任務(wù)對(duì)象的 taskIdentifier 從記錄表中獲取請(qǐng)求對(duì)象。
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;
// 根據(jù)不同的響應(yīng)序列化器,序列化響應(yīng)數(shù)據(jù)
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;
}
}
// 檢查請(qǐng)求是否成功,并獲取請(qǐng)求異常
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}
// 調(diào)用請(qǐng)求成功處理 或 調(diào)用請(qǐng)求失敗處理
if (succeed) {
[self requestDidSucceedWithRequest:request];
} else {
[self requestDidFailWithRequest:request error:requestError];
}
// 從記錄表中刪除請(qǐng)求對(duì)象
dispatch_async(dispatch_get_main_queue(), ^{
[self removeRequestFromRecord:request];
[request clearCompletionBlock];
});
}
在這個(gè)回調(diào)中,主要做了一下幾個(gè)工作:
- 根據(jù)任務(wù)對(duì)象的
taskIdentifier從記錄表_requestRecord中獲取請(qǐng)求對(duì)象。 - 對(duì)于獲取到的請(qǐng)求對(duì)象,根據(jù)不同的響應(yīng)序列化器,序列化響應(yīng)數(shù)據(jù)。
- 檢查請(qǐng)求是否成功,并獲取請(qǐng)求異常。
- 調(diào)用請(qǐng)求成功處理 或 調(diào)用請(qǐng)求失敗處理
- 從記錄表中刪除請(qǐng)求對(duì)象。
其中第 4 步,無論是成功回調(diào)還是失敗回調(diào),都會(huì)依次調(diào)用代理對(duì)象實(shí)現(xiàn)的 requestFinished: 或 requestFailed,以及請(qǐng)求對(duì)象的 successCompletionBlock 或 failureCompletionBlock。
下載任務(wù)與緩存
關(guān)于下載任務(wù),我們先來看一下上述 sessionTaskForRequest:error: 方法中,當(dāng)請(qǐng)求對(duì)象的請(qǐng)求類型是 YTKRequestMethodGET 且設(shè)置了請(qǐng)求對(duì)象的 resumableDownloadPath 屬性時(shí),會(huì)調(diào)用 downloadTaskWithDownloadPath:requestSerializer:URLString:parameters:progress:error: 方法。該方法的具體實(shí)現(xiàn)如下:
- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
error:(NSError * _Nullable __autoreleasing *)error {
// 使用請(qǐng)求參數(shù)、請(qǐng)求URL、請(qǐng)求類型,初始化 URLRequest 對(duì)象
NSMutableURLRequest *urlRequest = [requestSerializer requestWithMethod:@"GET" URLString:URLString parameters:parameters error:error];
NSString *downloadTargetPath;
// 檢查 resumableDownloadPath 指定的下載存儲(chǔ)路徑是否是目錄
BOOL isDirectory;
if(![[NSFileManager defaultManager] fileExistsAtPath:downloadPath isDirectory:&isDirectory]) {
isDirectory = NO;
}
// 預(yù)處理下載存儲(chǔ)路徑,確保不是目錄,而是文件
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];
}
// 檢查未完成下載暫存路徑是否有數(shù)據(jù) 并 讀取此路徑暫存的數(shù)據(jù)
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) {
// 對(duì)于可恢復(fù)的下載請(qǐng)求,使用已下載的數(shù)據(jù)初始化一個(gè)下載任務(wù),繼續(xù)發(fā)起下載請(qǐng)求。
@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) {
// 如果嘗試?yán)^續(xù)下載失敗,則創(chuàng)建一個(gè)下載任務(wù),重新開始發(fā)起下載請(qǐng)求。
downloadTask = [_manager downloadTaskWithRequest:urlRequest progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
// 指定下載的存儲(chǔ)路徑
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;
}
下載任務(wù)的創(chuàng)建過程中,有三個(gè)關(guān)鍵步驟:
- 確保下載存儲(chǔ)路徑是文件路徑,而非目錄路徑。
- 讀取 未完成下載暫存路徑 的數(shù)據(jù),并判斷是否可繼續(xù)下載。
- 如果可以繼續(xù)下載,則創(chuàng)建請(qǐng)求繼續(xù)下載;否則,創(chuàng)建請(qǐng)求重新下載。
從上面代碼中,我們可以知道下載存儲(chǔ)路徑有兩種可能:
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];
}
從上述代碼,可以看出未完成下載暫存路徑其實(shí)就是:
-
NSTemporaryDirectory()+ 下載存儲(chǔ)路徑目錄的 md5 值
注意,NSTemporaryDirectory() 目錄就是 UNIX 中的 /tmp 目錄,該目錄下的文件會(huì)在系統(tǒng)重啟后被清空。
YTKNetwork 鏈?zhǔn)秸?qǐng)求

鏈?zhǔn)秸?qǐng)求主要是通過 YTKNetwork 提供的兩個(gè)類,并結(jié)合 YTKNetwork 核心功能實(shí)現(xiàn)的。這兩類分別是:
YTKChainRequestYTKChainRequestAgent
下面,我們分別介紹一下 YTKChainRequest 和 YTKChainRequestAgent。
YTKChainRequest
YTKChainRequest 繼承自 NSObject,主要包含一下這些屬性。
/// 公開屬性
@interface YTKChainRequest : NSObject
/// 代理對(duì)象
@property (nonatomic, weak, nullable) id<YTKChainRequestDelegate> delegate;
/// YTKRequestAccessory 是一個(gè)協(xié)議,聲明了三個(gè)方法,允許開發(fā)者分別在請(qǐng)求執(zhí)行的三個(gè)階段(start、willStop、didStop)調(diào)用。
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;
@end
/// ------------------------------------------
/// 私有屬性
@interface YTKChainRequest()<YTKRequestDelegate>
/// 鏈?zhǔn)秸?qǐng)求隊(duì)列
@property (strong, nonatomic) NSMutableArray<YTKBaseRequest *> *requestArray;
/// 鏈?zhǔn)秸?qǐng)求回調(diào)隊(duì)列
@property (strong, nonatomic) NSMutableArray<YTKChainCallback> *requestCallbackArray;
///
@property (assign, nonatomic) NSUInteger nextRequestIndex;
@property (strong, nonatomic) YTKChainCallback emptyCallback;
@end
YTKChainRequest 提供了 4 個(gè)方法。
/// 獲取鏈?zhǔn)秸?qǐng)求隊(duì)列
- (NSArray<YTKBaseRequest *> *)requestArray;
/// 添加實(shí)現(xiàn)了 YTKRequestAccessory 協(xié)議的對(duì)象
- (void)addAccessory:(id<YTKRequestAccessory>)accessory;
/// 開始執(zhí)行鏈?zhǔn)秸?qǐng)求
- (void)start;
/// 停止執(zhí)行鏈?zhǔn)秸?qǐng)求
- (void)stop;
/// 向鏈?zhǔn)秸?qǐng)求隊(duì)列中添加請(qǐng)求
- (void)addRequest:(YTKBaseRequest *)request callback:(nullable YTKChainCallback)callback;
我們通過源代碼來看一下其中比較關(guān)鍵的 start 方法。
- (void)start {
// 判斷鏈?zhǔn)秸?qǐng)求是否已經(jīng)啟動(dòng)
if (_nextRequestIndex > 0) {
YTKLog(@"Error! Chain request has already started.");
return;
}
// 鏈?zhǔn)秸?qǐng)求隊(duì)列非空,則開始執(zhí)行請(qǐng)求
if ([_requestArray count] > 0) {
[self toggleAccessoriesWillStartCallBack];
[self startNextRequest];
[[YTKChainRequestAgent sharedAgent] addChainRequest:self];
} else {
YTKLog(@"Error! Chain request array is empty.");
}
}
start 方法內(nèi)部首先判斷鏈?zhǔn)秸?qǐng)求是否已經(jīng)啟動(dòng),這是通過請(qǐng)求索引 _nextRequestIndex 來判斷的。如果鏈?zhǔn)秸?qǐng)求未啟動(dòng),則開始執(zhí)行鏈?zhǔn)秸?qǐng)求,這里調(diào)用了一個(gè)關(guān)鍵的方法 startNextRequest。
- (BOOL)startNextRequest {
if (_nextRequestIndex < [_requestArray count]) {
YTKBaseRequest *request = _requestArray[_nextRequestIndex];
_nextRequestIndex++;
request.delegate = self;
[request clearCompletionBlock];
[request start];
return YES;
} else {
return NO;
}
}
每調(diào)用一次 startNextRequest,會(huì)移動(dòng)請(qǐng)求索引、設(shè)置請(qǐng)求代理并執(zhí)行。
鏈?zhǔn)秸?qǐng)求中的每一個(gè)請(qǐng)求 YTKBaseRequest 的代理都是鏈?zhǔn)秸?qǐng)求 YTKChainRequest。YTKChainRequest 實(shí)現(xiàn)了 YTKRequestDelegate 協(xié)議。每一個(gè)請(qǐng)求執(zhí)行完成后,開始執(zhí)行下一個(gè)請(qǐng)求。如果有一個(gè)請(qǐng)求失敗,即整個(gè)鏈?zhǔn)秸?qǐng)求失敗。
- (void)requestFinished:(YTKBaseRequest *)request {
NSUInteger currentRequestIndex = _nextRequestIndex - 1;
YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
callback(self, request);
// 執(zhí)行下一個(gè)請(qǐng)求
if (![self startNextRequest]) {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
// 所有請(qǐng)求執(zhí)行完畢,調(diào)用代理方法 chainRequestFinished:
[_delegate chainRequestFinished:self];
[[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
}
[self toggleAccessoriesDidStopCallBack];
}
}
- (void)requestFailed:(YTKBaseRequest *)request {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
// 有一個(gè)請(qǐng)求失敗,即調(diào)用 chainRequestFailed:
[_delegate chainRequestFailed:self failedBaseRequest:request];
[[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
}
[self toggleAccessoriesDidStopCallBack];
}
YTKChainRequestAgent
YTKChainRequestAgent 的作用非常簡(jiǎn)單,就是作為一個(gè)單例,持有多個(gè)鏈?zhǔn)秸?qǐng)求。YTKChainRequestAgent 提供的方法如下:
+ (YTKChainRequestAgent *)sharedAgent;
/// 添加鏈?zhǔn)秸?qǐng)求
- (void)addChainRequest:(YTKChainRequest *)request;
/// 移除鏈?zhǔn)秸?qǐng)求
- (void)removeChainRequest:(YTKChainRequest *)request;
YTKNetwork 批量請(qǐng)求

YTKNetwork 批量請(qǐng)求的實(shí)現(xiàn)原理其實(shí)與鏈?zhǔn)秸?qǐng)求的實(shí)現(xiàn)原理是一樣的,也提供了兩個(gè)類:
YTKBatchRequestYTKBatchRequestAgent
不同之處在于,YTKBatchRequest 中的單個(gè)請(qǐng)求并不是 YTKBaseRequest 請(qǐng)求,而是它的子類 YTKRequest。
我們來看看 YTKRequest 在父類 YTKBaseRequest 的基礎(chǔ)上做了些什么。
YTKRequest
首先,我們來看一下 YTKRequest 所提供的外部屬性和方法。
@interface YTKRequest : YTKBaseRequest
// 是否忽略緩存
@property (nonatomic) BOOL ignoreCache;
/// 請(qǐng)求響應(yīng)數(shù)據(jù)是否來自本地緩存
- (BOOL)loadCacheWithError:(NSError * __autoreleasing *)error;
/// 請(qǐng)求不使用緩存數(shù)據(jù)
- (void)startWithoutCache;
/// 將響應(yīng)數(shù)據(jù)保存至緩存
- (void)saveResponseDataToCacheFile:(NSData *)data;
#pragma mark - Subclass Override
/// 緩存時(shí)間
- (NSInteger)cacheTimeInSeconds;
/// 緩存版本
- (long long)cacheVersion;
/// 緩存敏感數(shù)據(jù),用于驗(yàn)證緩存是否失效
- (nullable id)cacheSensitiveData;
/// 是否異步寫入緩存
- (BOOL)writeCacheAsynchronously;
@end
很明顯,YTKRequest 在父類的基礎(chǔ)上支持了本地緩存功能。
緩存目錄
我們來重點(diǎn)看一下 YTKRequest 中相關(guān)的緩存目錄。首先來看以下幾個(gè)方法:
- (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;
}
默認(rèn)情況下,cacheBasePath 方法返回的基本路徑是:/Library/LazyRequestCache。
cacheFileName 方法則根據(jù)請(qǐng)求的基本信息生成緩存的文件名:Method:xxx Host:xxx Url:xxx Argument:xxx,并使用 md5 進(jìn)行編碼。
cacheFilePath 則是請(qǐng)求數(shù)據(jù)的完整存儲(chǔ)路徑:/Library/LazyRequestCache/ + md5(Method:xxx Host:xxx Url:xxx Argument:xxx)。
cacheMetadataFilePath 則存儲(chǔ)了緩存元數(shù)據(jù),其路徑是:cacheFilePath + .medata。
緩存元數(shù)據(jù)使用 YTKCacheMetaData 對(duì)象表示,其定義如下:
@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 主要用戶驗(yàn)證緩存是否有效。驗(yàn)證方法如下:
- (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;
}
總結(jié)
YTKNetwork 設(shè)計(jì)原理非常簡(jiǎn)單,僅僅是對(duì) AFNetworking 做了一個(gè)簡(jiǎn)單的封裝,提供了面向?qū)ο蟮氖褂梅椒?,使用起來也是非常?jiǎn)單。不過也存在缺點(diǎn),就是每一個(gè)請(qǐng)求都需要定義一個(gè)類。
參考
(完)