美文网首页iOS Kit网络层
iOS 网络(3)——YTKNetwork

iOS 网络(3)——YTKNetwork

作者: baochuquan | 来源:发表于2019-09-28 22:02 被阅读0次

    【原文链接】

    注意:在阅读本文之前建议先阅读《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 的内部结构如下图所示。下面我们将以该图为指导进行介绍。

    image

    初始化

    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: 方法内部会做一下几个步骤的工作:

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

    我们重点看一下第 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];
        });
    }
    

    在这个回调中,主要做了一下几个工作:

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

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

    下载任务与缓存

    关于下载任务,我们先来看一下上述 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;
    }
    

    下载任务的创建过程中,有三个关键步骤:

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

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

    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];
    }
    

    从上述代码,可以看出未完成下载暂存路径其实就是:

    • NSTemporaryDirectory() + 下载存储路径目录的 md5 值

    注意,NSTemporaryDirectory() 目录就是 UNIX 中的 /tmp 目录,该目录下的文件会在系统重启后被清空。

    YTKNetwork 链式请求

    image

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

    • YTKChainRequest
    • YTKChainRequestAgent

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

    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 的代理都是链式请求 YTKChainRequestYTKChainRequest 实现了 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 批量请求

    image

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

    • 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 做了一个简单的封装,提供了面向对象的使用方法,使用起来也是非常简单。不过也存在缺点,就是每一个请求都需要定义一个类。

    参考

    1. YTKNetwork

    (完)

    相关文章

      网友评论

        本文标题:iOS 网络(3)——YTKNetwork

        本文链接:https://www.haomeiwen.com/subject/zxpaectx.html