iOS 網(wǎng)絡(luò)(3)——YTKNetwork

【原文鏈接】

注意:在閱讀本文之前建議先閱讀《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 核心功能

image

上圖所示為 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è)類:

  • YTKBaseRequest
  • YTKNetworkConfig
  • YTKNetworkAgent

下面我們分別進(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)行介紹。

image

初始化

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è)步驟的工作:

  1. 初始化請(qǐng)求對(duì)象的關(guān)鍵屬性 requestTask,即任務(wù)對(duì)象。
  2. 設(shè)置請(qǐng)求優(yōu)先級(jí)
  3. 以任務(wù)對(duì)象的 taskIdentifier 為鍵,請(qǐng)求對(duì)象為值,建立映射關(guān)系,存入 記錄表(即上圖中的 _requestRecord,后文還會(huì)提到)。
  4. 執(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è)工作:

  1. 根據(jù)任務(wù)對(duì)象的 taskIdentifier 從記錄表 _requestRecord 中獲取請(qǐng)求對(duì)象。
  2. 對(duì)于獲取到的請(qǐng)求對(duì)象,根據(jù)不同的響應(yīng)序列化器,序列化響應(yīng)數(shù)據(jù)。
  3. 檢查請(qǐng)求是否成功,并獲取請(qǐng)求異常。
  4. 調(diào)用請(qǐng)求成功處理 或 調(diào)用請(qǐng)求失敗處理
  5. 從記錄表中刪除請(qǐng)求對(duì)象。

其中第 4 步,無論是成功回調(diào)還是失敗回調(diào),都會(huì)依次調(diào)用代理對(duì)象實(shí)現(xiàn)的 requestFinished:requestFailed,以及請(qǐng)求對(duì)象的 successCompletionBlockfailureCompletionBlock。

下載任務(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)鍵步驟:

  1. 確保下載存儲(chǔ)路徑是文件路徑,而非目錄路徑。
  2. 讀取 未完成下載暫存路徑 的數(shù)據(jù),并判斷是否可繼續(xù)下載。
  3. 如果可以繼續(xù)下載,則創(chuàng)建請(qǐng)求繼續(xù)下載;否則,創(chuàng)建請(qǐng)求重新下載。

從上面代碼中,我們可以知道下載存儲(chǔ)路徑有兩種可能:

  1. resumableDownloadPath
  2. 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)求

image

鏈?zhǔn)秸?qǐng)求主要是通過 YTKNetwork 提供的兩個(gè)類,并結(jié)合 YTKNetwork 核心功能實(shí)現(xiàn)的。這兩類分別是:

  • YTKChainRequest
  • YTKChainRequestAgent

下面,我們分別介紹一下 YTKChainRequestYTKChainRequestAgent。

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)求

image

YTKNetwork 批量請(qǐng)求的實(shí)現(xiàn)原理其實(shí)與鏈?zhǔn)秸?qǐng)求的實(shí)現(xiàn)原理是一樣的,也提供了兩個(gè)類:

  • YTKBatchRequest
  • YTKBatchRequestAgent

不同之處在于,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è)類。

參考

  1. YTKNetwork

(完)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容