专栏名称: baochuquan
iOS开发攻城狮
今天看啥  ›  专栏  ›  baochuquan

iOS 网络(3)——YTKNetwork

baochuquan  · 掘金  ·  · 2019-11-26 07:51

文章预览

阅读 97

iOS 网络(3)——YTKNetwork

原文链接

注意:在阅读本文之前建议先阅读《iOS 网络——NSURLSession》《iOS 网络——AFNetworking》

《iOS 网络——AFNetworking》一文中我们介绍了基于 NSURLSession 进行封装的 AFNetworking 的核心功能原理。本文,我们进一步介绍基于 AFNetworking 进行封装的 YTKNetwork 开源框架。本文,我们通过阅读 YTKNetwork 源代码(版本号:2.0.4)。

YTKNetwork 概述

YTKNetwork 是猿题库技术团队开源的一个网络请求框架,内部封装了 AFNetworking。YTKNetwork 实现了一套高层级的 API,提供更高层次的网络访问抽象。目前,猿题库公司的所有产品的 iOS 客户端都使用了 YTKNetwork,包括:猿题库、小猿搜题、猿辅导、小猿口算、斑马系列等。

YTKNetwork 架构

YTKNetwork 开源框架主要包含 3 个部分:

  • YTKNetwork 核心功能
  • YTKNetwork 链式请求
  • YTKNetwork 批量请求

其中,链式请求和批量请求都是基于 YTKNetwork 的核心功能实现的。下面我们分别进行介绍。

YTKNetwork 核心功能

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

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

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

  • YTKBaseRequest
  • YTKNetworkConfig
  • YTKNetworkAgent

下面我们分别进行介绍。

YTKBaseRequest

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

@interface YTKBaseRequest : NSObject
/// 请求相关属性
@property (nonatomic, strong, readonly) NSURLSessionTask *requestTask;
@property (nonatomic, strong, readonly) NSURLRequest *currentRequest;
@property (nonatomic, strong, readonly) NSURLRequest *originalRequest;
@property (nonatomic, strong, readonly) NSHTTPURLResponse *response;

/// 响应相关属性
@property (nonatomic, readonly) NSInteger responseStatusCode;
@property (nonatomic, strong, readonly, nullable) NSDictionary *responseHeaders;
@property (nonatomic, strong, readonly, nullable) NSData *responseData;
@property (nonatomic, strong, readonly, nullable) NSString *responseString;
@property (nonatomic, strong, readonly, nullable) id responseObject;
@property (nonatomic, strong, readonly, nullable) id responseJSONObject;

/// 异常
@property (nonatomic, strong, readonly, nullable) NSError *error;

/// 状态
@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled;
@property (nonatomic, readonly, getter=isExecuting) BOOL executing;

/// 标识符,默认是 0
@property (nonatomic) NSInteger tag;

/// 附加信息,默认是 nil
@property (nonatomic, strong, nullable) NSDictionary *userInfo;

/// 代理
@property (nonatomic, weak, nullable) id<YTKRequestDelegate> delegate;

/// 成功/失败回调
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock successCompletionBlock;
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock failureCompletionBlock;

/// 用于在 POST 请求时构建 HTTP 主体。默认是 nil
@property (nonatomic, copy, nullable) AFConstructingBlock constructingBodyBlock;

/// 用于下载任务时指定本地下载路径
@property (nonatomic, strong, nullable) NSString *resumableDownloadPath;

/// 用于跟踪下载进度的回调
@property (nonatomic, copy, nullable) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;

/// 请求优先级
@property (nonatomic) YTKRequestPriority requestPriority;

/// YTKRequestAccessory 是一个协议,声明了三个方法,允许开发者分别在请求执行的三个阶段(start、willStop、didStop)调用。
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;

@end
复制代码

事实上,YTKBaseRequest 类就是围绕 NSURLSessionTask 类进行封装的, requestTask 是它最重要的属性。YTKBaseRequest 的其他多个属性都源自于 requestTask 的属性。如:

  • currentRequest:即 requestTask.currentRequest
  • originalRequest:即 requestTask.originalRequest
  • response:即 requestTask.response
  • responseHeaders:即 requestTask.allHeaderFields
  • responseStatusCode:即 requestTask.statusCode

YTKBaseRequest 提供了高层级的网络抽象,体现在提供了一些高层级的配置方法,并允许用户通过覆写这些方法来进行自定义配置。一些常用的配置方法包括如下:

/// Base URL,因为一个应用程序中的网络请求的 BaseURL 几乎都是相同的。
- (NSString *)baseUrl {
    return @"";
}

/// 请求的 URL 路径
- (NSString *)requestUrl {
    return @"";
}

/// 网络请求的超时间隔。默认 60 秒
- (NSTimeInterval)requestTimeoutInterval {
    return 60;
}

/// HTTP 请求方法。默认是 GET
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodGET;
}

/// 请求序列化器类型。默认是 HTTP
- (YTKRequestSerializerType)requestSerializerType {
    return YTKRequestSerializerTypeHTTP;
}

/// 响应序列化器类型。默认是 JSON
- (YTKResponseSerializerType)responseSerializerType {
    return YTKResponseSerializerTypeJSON;
}

/// 请求参数对象,会根据配置的请求序列化器进行编码。
- (id)requestArgument {
    return nil;
}

/// 是否允许蜂窝网络。默认 YES
- (BOOL)allowsCellularAccess {
    return YES;
}

/// 是否使用 CDN。默认 NO
- (BOOL)useCDN {
    return NO;
}

/// CDN URL。根据 useCDN 决定是否使用。
- (NSString *)cdnUrl {
    return @"";
}
...
复制代码

关于 YTKBaseRequest 对象的执行,它也提供了几个简单的方法以供开发者使用,如下所示。通过 start 方法,我们可以发现 YTKBaseRequest 被加入到了 YTKNetworkAgent 单例中。可见 YTKNetworkAgent 管理了多个 YTKBaseRequest 对象。

/// YTKBaseRequest 开始执行
- (void)start {
    // 执行 YTKRequestAccessory 协议定义的 requestWillStart: 方法。
    [self toggleAccessoriesWillStartCallBack];
    // 将请求对象加入 YTKNetworkAgent 单例
    [[YTKNetworkAgent sharedAgent] addRequest:self];
}

/// YTKBaseRequest 停止执行
- (void)stop {
    // 执行 YTKRequestAccessory 协议定义的 requestWillStop: 方法。
    [self toggleAccessoriesWillStopCallBack];
    self.delegate = nil;
    [[YTKNetworkAgent sharedAgent] cancelRequest:self];
    // 执行 YTKRequestAccessory 协议定义的 requestDidStop: 方法。
    [self toggleAccessoriesDidStopCallBack];
}

/// 一个便利方法。执行 YTKBaseRequest。
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                                    failure:(YTKRequestCompletionBlock)failure {
    [self setCompletionBlockWithSuccess:success failure:failure];
    [self start];
}
复制代码

YTKNetworkConfig

YTKNetworkConfig 是用于 YTKNetworkAgent 初始化的配置对象,是一个 单例

YTKNetworkConfig 主要包含以下属性:

@interface YTKNetworkConfig : NSObject

/// 请求的 Base URL。默认是 ""
@property (nonatomic, strong) NSString *baseUrl;

///  CDN URL. 默认是 ""
@property (nonatomic, strong) NSString *cdnUrl;

/// URL 过滤器。YTKUrlFilterProtocol 声明的 filterUrl:withRequest: 方法会返回最终被使用的 URL
@property (nonatomic, strong, readonly) NSArray<id<YTKUrlFilterProtocol>> *urlFilters;

/// 缓存路径过滤器。YTKCacheDirPathFilterProtocol 声明的 filterCacheDirPath:withRequest: 方法会返回最终被使用的缓存路径。
@property (nonatomic, strong, readonly) NSArray<id<YTKCacheDirPathFilterProtocol>> *cacheDirPathFilters;

/// 安全策略。
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;

/// 是否打印调试日志信息。默认是 NO
@property (nonatomic) BOOL debugLogEnabled;

/// 会话配置对象
@property (nonatomic, strong) NSURLSessionConfiguration* sessionConfiguration;

@end
复制代码

YTKNetworkConfig 持有了一个 NSURLSessionConfiguration 类型的属性 sessionConfiguration,用于 YTKNetworkAgent 中初始化 AFHTTPSessionManager(本质上是用于初始化 NSURLSession)。

YTKNetworkAgent

YTKNetworkAgent 的内部结构如下图所示。下面我们将以该图为指导进行介绍。

初始化

YTKNetworkAgent 初始化过程会使用 YTKNetworkConfig 单例对象(配置对象)。使用配置对象的会话配置对象 sessionConfiguration 初始化会话管理器 AFHTTPSessionManager

YTKNetwork 框架默认只能使用 YTKNetworkAgent 单例对象。

添加并执行请求

YTKNetworkAgent 提供了 addRequest: 方法来添加并执行请求对象。我们可以来看一下其内部实现。

- (void)addRequest:(YTKBaseRequest *)request {
    NSParameterAssert(request != nil);

    NSError * __autoreleasing requestSerializationError = nil;

    // 初始化请求对象的关键属性 requestTask,即任务对象
    NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
    if (customUrlRequest) {
        __block NSURLSessionDataTask *dataTask = nil;
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            // 完成回调
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;
    } else {
        // 默认方式
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
    }

    // 请求序列化异常处理
    if (requestSerializationError) {
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

    NSAssert(request.requestTask != nil, @"requestTask should not be nil");

    // 设置请求优先级
    // !!Available on iOS 8 +
    if ([request.requestTask respondsToSelector:@selector(priority)]) {
        switch (request.requestPriority) {
            case YTKRequestPriorityHigh:
                request.requestTask.priority = NSURLSessionTaskPriorityHigh;
                break;
            case YTKRequestPriorityLow:
                request.requestTask.priority = NSURLSessionTaskPriorityLow;
                break;
            case YTKRequestPriorityDefault:
                /*!!fall through*/
            default:
                request.requestTask.priority = NSURLSessionTaskPriorityDefault;
                break;
        }
    }

    YTKLog(@"Add request: %@", NSStringFromClass([request class]));
    // 将 请求对象 加入记录表
    [self addRequestToRecord:request];
    // 执行请求,即执行任务对象
    [request.requestTask resume];
}
复制代码

addRequest: 方法内部会做一下几个步骤的工作:

  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 链式请求

链式请求主要是通过 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 批量请求

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
………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览