开源代码信息 开源地址
YTKNetwork是基于AFNetworking封装的iOS网络库,它在AFNetworking的基础上提供了如下的功能:
支持按时间缓存网络请求内容
支持按版本号缓存网络请求内容
支持统一设置服务器和 CDN 的地址
支持检查返回 JSON 内容的合法性
支持文件的断点续传
支持 block 和 delegate 两种模式的回调方式
支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B 和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。
定义了一套插件机制,可以很方便地为 YTKNetwork 增加功能。猿题库官方现在提供了一个插件,可以在某些网络请求发起时,在界面上显示“正在加载”的 HUD。
源码分析 YTKNetWork 整个代码量不是很大,整个代码结构如下所示:
在整个请求过程中都是围绕着Request展开,这里需要注意的是YTKBatchRequest以及YTKChainRequest并不继承自YTKBaseRequest,它其实是通过请求数组requestArray来管理请求的形式,所以个人觉得这两个类的命名不是很合理,很容易让人以为它们和YTKRequest一样都是继承自YTKBaseRequest。另一个个人觉得不是很合理的地方是它将全部的数据都放到YTKRequest,会显得庞大且乱。
这篇博客将通过如下几个方面对YTKNetWork源码进行解析:
* YTKRequest && YTKBaseRequest 的组成* 基本请求过程* 批量请求* 链式请求* 缓存机制* 断点续传* URL filter* 插件机制
1. YTKRequest && YTKBaseRequest 的组成 YTKRequest
@interface YTKBaseRequest : NSObject #pragma mark - Request and Response Information @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;#pragma mark - Request Configuration @property (nonatomic ) NSInteger tag;@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;@property (nonatomic , strong , nullable ) NSMutableArray <id <YTKRequestAccessory>> *requestAccessories;@property (nonatomic , copy , nullable ) AFConstructingBlock constructingBodyBlock;@property (nonatomic , strong , nullable ) NSString *resumableDownloadPath;@property (nonatomic , copy , nullable ) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;@property (nonatomic ) YTKRequestPriority requestPriority;- (void )setCompletionBlockWithSuccess:(nullable YTKRequestCompletionBlock)success failure:(nullable YTKRequestCompletionBlock)failure; - (void )clearCompletionBlock; - (void )addAccessory:(id <YTKRequestAccessory>)accessory; #pragma mark - Request Action - (void )start; - (void )stop; - (void )startWithCompletionBlockWithSuccess:(nullable YTKRequestCompletionBlock)success failure:(nullable YTKRequestCompletionBlock)failure; #pragma mark - Subclass Override - (void )requestCompletePreprocessor; - (void )requestCompleteFilter; - (void )requestFailedPreprocessor; - (void )requestFailedFilter; - (NSString *)baseUrl; - (NSString *)requestUrl; - (NSString *)cdnUrl; - (NSTimeInterval )requestTimeoutInterval; - (nullable id )requestArgument; - (YTKRequestMethod)requestMethod; - (YTKRequestSerializerType)requestSerializerType; - (YTKResponseSerializerType)responseSerializerType; - (nullable NSArray <NSString *> *)requestAuthorizationHeaderFieldArray; - (nullable NSDictionary <NSString *, NSString *> *)requestHeaderFieldValueDictionary; - (nullable NSURLRequest *)buildCustomUrlRequest; - (BOOL )useCDN; - (BOOL )allowsCellularAccess; - (nullable id )jsonValidator; - (BOOL )statusCodeValidator; @end
YTKRequest
YTKRequest主要是在YTKBaseRequest添加了缓存的相关管理。
@interface YTKRequest : YTKBaseRequest @property (nonatomic ) BOOL ignoreCache;- (BOOL )isDataFromCache; - (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
2. 基本请求过程 基本请求这块代码都放在YTKBaseRequest以及YTKNetworkAgent中,YTKBaseRequest会将实际的处理封装在YTKNetworkAgent,我们就来看下这部分代码:
请求的发起
一般请求我们会通过startWithCompletionBlockWithSuccess来执行:
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success failure:(YTKRequestCompletionBlock)failure { [self setCompletionBlockWithSuccess:success failure:failure] ; [self start] ; } - (void)start { [self toggleAccessoriesWillStartCallBack] ; [[YTKNetworkAgent sharedAgent] addRequest:self]; }
请求过程中会先设置成功,失败结果返回的block,然后通知插件请求将要开始了,插件可以根据实际需求做对应处理,比如显示加载动画等,然后就将请求传递给YTKNetworkAgent。
- (void )addRequest:(YTKBaseRequest *)request { NSError * __autoreleasing requestSerializationError = nil ; 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 ; } 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: default : request.requestTask.priority = NSURLSessionTaskPriorityDefault ; break ; } } [self addRequestToRecord:request]; [request.requestTask resume]; }
在YTKNetworkAgent中我们首先会看下是否开发者在对应等request中重写了buildCustomUrlRequest 方法,如果是的话则通过这个返回的request 生成NSURLSessionDataTask,否则使用传入的request通过调用sessionTaskForRequest生成NSURLSessionDataTask,紧接着给请求设置优先级,并且保持请求引用。然后开启请求。
- (NSURLSessionTask * )sessionTaskForRequest:(YTKBaseRequest * )request error:(NSError * _Nullable __autoreleasing * )error { YTKRequestMethod method = [request requestMethod]; NSString * url = [self buildRequestUrl:request]; id param = request.requestArgument; 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方法会将YTKBaseRequest中的一系列配置拆开,通过在YTKBaseRequest中的配置来创建不同请求方法下的dataTask。首先看下请求url的构建策略:
请求url的构建是通过buildRequestUrl 来实现的
- (NSString *)buildRequestUrl:(YTKBaseRequest *)request { NSString *detailUrl = [request requestUrl]; NSURL *temp = [NSURL URLWithString:detailUrl]; if (temp && temp.host && temp.scheme) { return detailUrl; } NSArray *filters = [_config urlFilters]; for (id <YTKUrlFilterProtocol> f in filters) { detailUrl = [f filterUrl:detailUrl withRequest:request]; } NSString *baseUrl; if ([request useCDN]) { if ([request cdnUrl].length > 0 ) { baseUrl = [request cdnUrl]; } else { baseUrl = [_config cdnUrl]; } } else { if ([request baseUrl].length > 0 ) { baseUrl = [request baseUrl]; } else { baseUrl = [_config baseUrl]; } } NSURL *url = [NSURL URLWithString:baseUrl]; if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/" ]) { url = [url URLByAppendingPathComponent:@"" ]; } return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString; }
首先会先取requestUrl看下是不是一个完整的URL请求,如果是的话就直接将这个作为请求返回,紧接着会从YTKNetWorkConfig中获取预设的urlFilters,一一对它进行处理,比如添加公共字段等。最终形成一个处理后的不包括scheme,host的URL。
接下来就是构建baseUrl的过程,这个顺序如下:
request --> cdnUrl > _config ---> cdnUrl > request---> baseUrl > _config ---> baseUrl
总结来说就是能使用cdn的优先使用cdn的地址,不能使用的时候再选用baseUrl中的地址,能使用request的优先使用request的,没有的话使用config中的。最后将detailUrl和baseUrl进行拼接形成最终的地址。所以整个地址包含三个部分,一个是baseUrl,一个是requestUrl,然后是各个filter添加的信息。
接下来看下dataTask的创建这里分成三类,一个是GET方式,一个是POST方式,一个是download方式:
GET
[self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error :error ];
POST
[self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error :error ];
DOWNLOAD
[self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer: requestSerializer URLString: url parameters:param progress: request.resumableDownloadProgressBlock error: error];
上面GET POST 最终都走到下面的方法中,无非就是constructingBodyWithBlock空与非空的区别,
- (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 ; 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]; } __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; }
constructingBodyWithBlock有值的时候我们会使用请求序列化中的multipartFormRequestWithMethod进行处理,如果无值的话就会调用请求序列化器中的requestWithMethod进行处理。对请求处理后就可以使用该请求对象生成dataTask了,这部分没什么特殊的地方,我们关键来看下下载的部分:
- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath requestSerializer:(AFHTTPRequestSerializer *)requestSerializer URLString:(NSString *)URLString parameters:(id )parameters progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock error:(NSError * _Nullable __autoreleasing *)error { NSMutableURLRequest *urlRequest = [requestSerializer requestWithMethod:@"GET" URLString:URLString parameters:parameters error:error]; NSString *downloadTargetPath; 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 resumeSucceeded = NO ; __block NSURLSessionDownloadTask *downloadTask = nil ; NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:downloadPath]; if (localUrl != nil ) { BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:localUrl.path]; NSData *data = [NSData dataWithContentsOfURL:localUrl]; BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data]; BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid; 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; }
首先会先通过判断传进来的downloadPath是否是目录名称,如果是则将url最后的部分作为文件名添加到path最后,如果是文件名,则直接作为目标下载地址
紧接着如果要下载的文件已经存在那么在下载之前先删除这个文件
然后再去临时目录去看是否有之前下载一半没有下载结束的缓存文件,临时目录下的这些缓存文件命名规则大家可以看下这部分代码:
- (NSURL *)incompleteDownloadTempPathForDownloadPath:(NSString *)downloadPath { NSString *tempPath = nil ; NSString *md5URLString = [YTKNetworkUtils md5StringFromString:downloadPath]; tempPath = [[self incompleteDownloadTempCacheFolder] stringByAppendingPathComponent:md5URLString]; return tempPath == nil ? nil : [NSURL fileURLWithPath:tempPath]; }
如果有上一次下载一半的数据,就先加载到内存,验证数据的合法性,如果数据存在,并且合法,就通过downloadTaskWithResumeData创建dataTask,否则通过downloadTaskWithRequest来创建。
请求结束处理
请求结束统一调用handleRequestResult进行处理:
- (void )handleRequestResult:(NSURLSessionTask *)task responseObject:(id )responseObject error:(NSError *)error { Lock(); 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: 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]; }); }
根据上面加载的请求发起的流程来看,不难猜出下载结束后的工作应该是,将请求数据通过response 序列化器进行处理后存放到YTKBaseRequest中,然后通过对应的block以及delegte将数据交付到应用层,然后将delegate以及block清除,但是我们知道请求返回后AFNetWorking实际上返回的是task,responseOject,这时候并不知道具体是哪个YTKBaseRequest ,这个问题是怎么解决的呢?我们知道在addRequest的时候会调用****[self addRequestToRecord:request]****
- (void) addRequestToRecord:(YTKBaseRequest *) request { Lock() ; _requestsRecord[@(request.requestTask.taskIdentifier) ] = request; Unlock() ; }
它会以dataTask的taskIdentifier为key YTKBaseRequest为value建立一个映射表,当请求结束的时候我们得到的是一个dataTask,那么就可以通过它的taskIdentifier作为key,获得对应的YTKBaseRequest,然后将resonse序列化器处理后的结果塞到YTKBaseRequest中。这一切都结束后再将映射关系从映射表中删除。
我们再继续看下请求成功和失败后会怎么处理接下来的流程:
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request { @autoreleasepool { [request requestCompletePreprocessor] ; } dispatch_async (dispatch_get_main_queue(), ^{ [request toggleAccessoriesWillStopCallBack] ; [request requestCompleteFilter] ; if (request.delegate != nil) { [request.delegate requestFinished:request] ; } if (request.successCompletionBlock) { request.successCompletionBlock (request); } [request toggleAccessoriesDidStopCallBack] ; }); }
请求成功后在还没切回主线程之前会先调用requestCompletePreprocessor,在这里相当于埋下一个勾子,外面可以在这里实现类似统计之类的功能,紧接着切换到主线程,通知外部插件请求将要结束了,然后再调用requestCompleteFilter,这个和requestCompletePreprocessor功能类似,区别是一个在后台线程一个在主线程,紧接着就是通过代理和block将数据交付给上层,最后通知插件该请求结束。
- (void )requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error { request.error = error; YTKLog(@"Request %@ failed, status code = %ld, error = %@" , NSStringFromClass ([request class ]), (long )request.responseStatusCode, error.localizedDescription); NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData ]; NSURL *localUrl = nil ; if (request.resumableDownloadPath) { localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath]; } if (incompleteDownloadData && localUrl != nil ) { [incompleteDownloadData writeToURL:localUrl atomically:YES ]; } if ([request.responseObject isKindOfClass:[NSURL class ]]) { NSURL *url = request.responseObject; if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) { request.responseData = [NSData dataWithContentsOfURL:url]; request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]]; [[NSFileManager defaultManager] removeItemAtURL:url error:nil ]; } request.responseObject = nil ; } @autoreleasepool { [request requestFailedPreprocessor]; } dispatch_async (dispatch_get_main_queue(), ^{ [request toggleAccessoriesWillStopCallBack]; [request requestFailedFilter]; if (request.delegate != nil ) { [request.delegate requestFailed:request]; } if (request.failureCompletionBlock) { request.failureCompletionBlock(request); } [request toggleAccessoriesDidStopCallBack]; }); }
请求失败的话大体上和请求成功在流程上是一致的,只不过会在失败的时候通过****error.userInfo[NSURLSessionDownloadTaskResumeData]****拿到已经获取的数据,这时候就可以将这数据存储起来供下一次继续请求的时候实现断点续传。
最后看下取消请求的流程:
- (void )cancelRequest:(YTKBaseRequest *)request { NSParameterAssert (request != nil ); if (request.resumableDownloadPath && [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] != nil ) { NSURLSessionDownloadTask *requestTask = (NSURLSessionDownloadTask *)request.requestTask; [requestTask cancelByProducingResumeData:^(NSData *resumeData) { NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath]; [resumeData writeToURL:localUrl atomically:YES ]; }]; } else { [request.requestTask cancel]; } [self removeRequestFromRecord:request]; [request clearCompletionBlock]; }
和请求失败处理一样可以做临时数据缓存,只不过需要通过NSURLSessionDownloadTask 的 cancelByProducingResumeData 方法拿到已经获取的数据。再将数据写到临时目录下。
3. 批量请求 批量请求没啥需要介绍的,就是用一个数组将YTKRequst存放起来,在start的时候一个个调用YTKRequest的start方法:
- (void )start { if (_finishedCount > 0 ) { YTKLog(@"Error! Batch request has already started." ); return ; } _failedRequest = nil; [[YTKBatchRequestAgent sharedAgent] addBatchRequest:self]; [self toggleAccessoriesWillStartCallBack ]; for (YTKRequest * req in _requestArray) { req.delegate = self; [req clearCompletionBlock ]; [req start ]; } }
4. 链式请求 链式请求是一系列有依赖关系的请求,它会在某个请求执行完毕后,进行下一个请求的启动。接下来我们以添加请求 ,启动请求 ,请求结束后启动下一个请求 三个部分进行简单展开:
1. 添加请求
- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback { [_requestArray addObject:request] ; if (callback != nil) { [_requestCallbackArray addObject:callback] ; } else { [_requestCallbackArray addObject:_emptyCallback] ; } }
添加请求是通过addRequest:callback方法来完成的,每个请求与callback是一一对应的,如果外面传进来一个nil callback,那么_requestCallbackArray就会添加一个空请求,作为占位。
2. 启动请求
- (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." ); } } - (BOOL )startNextRequest { if (_nextRequestIndex < [_requestArray count]) { YTKBaseRequest *request = _requestArray[_nextRequestIndex]; _nextRequestIndex++; request.delegate = self ; [request clearCompletionBlock]; [request start]; return YES ; } else { return NO ; } }
这个会按照请求数组的顺序一个一个取出来,调用start方法,这个就不做过多介绍了。
请求结束后启动下一个请求
- (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:)]) { [_delegate chainRequestFinished:self ]; [[YTKChainRequestAgent sharedAgent] removeChainRequest:self ]; } [self toggleAccessoriesDidStopCallBack]; } }
请求结束后会从_requestCallbackArray中获取对应的YTKChainCallback进行处理,然后调用startNextRequest开启下一个请求。
5. 缓存机制 前面已经提到YTKNetWork的缓存机制都保存在YTKRequest中,所以这部分我们来看下YTKReques:
依旧从启动请求开始:
- (void )start { if (self .ignoreCache) { [self startWithoutCache]; return ; } if (self .resumableDownloadPath) { [self startWithoutCache]; return ; } if (![self loadCacheWithError:nil ]) { [self startWithoutCache]; return ; } _dataFromCache = YES ; dispatch_async (dispatch_get_main_queue(), ^{ [self requestCompletePreprocessor]; [self requestCompleteFilter]; YTKRequest *strongSelf = self ; [strongSelf.delegate requestFinished:strongSelf]; if (strongSelf.successCompletionBlock) { strongSelf.successCompletionBlock(strongSelf); } [strongSelf clearCompletionBlock]; }); }
在启动的时候会首先根据当前情况选择是否使用缓存数据,如果我们在YTKRequest中配置了ignoreCache,或者在下载数据的情景中并且配置了resumableDownloadPath,再或者缓存加载失败都会启动无缓存请求模式, 除了上述情况会先通过*loadCacheWithError **将缓存再本地的数据加载到内存中:
- (BOOL )loadCacheWithError:(NSError * _Nullable __autoreleasing *)error { if ([self cacheTimeInSeconds] < 0 ) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey :@"Invalid cache time" }]; } return NO ; } if (![self loadCacheMetadata]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey :@"Invalid metadata. Cache may not exist" }]; } return NO ; } if (![self validateCacheWithError:error]) { return NO ; } if (![self loadCacheData]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey :@"Invalid cache data" }]; } return NO ; } return YES ; }
在加载数据之前会先检查下我们配置的缓存时间,如果cacheTimeInSeconds小于0的话就不再继续加载了,这时候会抛出错误。缓存数据加载失败走正常数据请求流程。否则再紧接着缓存加载的流程:
加载缓存信息数据loadCacheMetadata ,检查缓存数据的合法性validateCacheWithError ,加载数据loadCacheData ,下面就针对这些环节进行介绍:
loadCacheMetadata
在加载缓存数据之前会先加载缓存的信息文件
- (BOOL )loadCacheMetadata { NSString *path = [self cacheMetadataFilePath]; NSFileManager * fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path isDirectory:nil ]) { @try { _cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; return YES ; } @catch (NSException *exception) { YTKLog(@"Load cache metadata failed, reason = %@" , exception.reason); return NO ; } } return NO ; }
那么缓存信息文件存在哪里呢?我们怎么拿现有的配置取到缓存的信息文件?带着这个疑问继续看下面的代码:
- (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 *)cacheMetadataFilePath { NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata" , [self cacheFileName]]; NSString *path = [self cacheBasePath]; path = [path stringByAppendingPathComponent:cacheMetadataFileName]; return path; } - (NSString *)cacheBasePath { NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains (NSLibraryDirectory , NSUserDomainMask , YES ) objectAtIndex:0 ]; NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache" ]; 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; }
我们先来看下requestInfo 这个字符串是怎么来的:
[NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",(long)[self requestMethod], baseUrl, requestUrl, argument];
requestInfo包含着Method,URL(Host:Url),Argument,也就是说缓存文件信息文件名里面包含着一个请求的全部信息。在通过对这个字符串进行md5加密,生成的字符串作为缓存信息文件名。 最终文件存放在:
/[系统缓存路径]/ LazyRequestCache[这个名称会经过cacheDirPathFilters进行处理,默认是LazyRequestCache]/xxxxxxxxxxxx.metadata
可以通过cacheDirPathFilters添加版本信息等公共的处理。
加载出来的对象结构如下:
@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
包括缓存版本,关键字段,字符编码格式,创建时间,app版本信息。后面将会重点看下怎么利用这些信息来实现缓存策略。
validateCacheWithError
上面已经成功地加载了缓存信息文件,接下来就是对这些进行缓存信息进行校验了:
- (BOOL )validateCacheWithError:(NSError * _Nullable __autoreleasing *)error { NSDate *creationDate = self .cacheMetadata.creationDate; NSTimeInterval duration = -[creationDate timeIntervalSinceNow]; if (duration < 0 || duration > [self cacheTimeInSeconds]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey :@"Cache expired" }]; } return NO ; } long long cacheVersionFileContent = self .cacheMetadata.version; if (cacheVersionFileContent != [self cacheVersion]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey :@"Cache version mismatch" }]; } return NO ; } NSString *sensitiveDataString = self .cacheMetadata.sensitiveDataString; NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description; if (sensitiveDataString || currentSensitiveDataString) { if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey :@"Cache sensitive data mismatch" }]; } return NO ; } } NSString *appVersionString = self .cacheMetadata.appVersionString; NSString *currentAppVersionString = [YTKNetworkUtils appVersionString]; if (appVersionString || currentAppVersionString) { if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) { if (error) { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey :@"App version mismatch" }]; } return NO ; } } return YES ; }
首先是对缓存是否过期进行校验,这一步主要主要通过YTKCacheMetadata中的creationDate与当前时间计算差值,再与cacheTimeInSeconds对比判断缓存数据是否过期。 紧接着看缓存版本和我们request中的cacheVersion是否匹配。紧接着对比缓存的关键信息。然后再是应用版本的匹配,这些信息都匹配的时候才说明缓存是可用的,才会去加载缓存数据。
loadCacheData
紧接着会将本地缓存文件根据序列化形式加载到****_cacheData, _cacheString, _cacheXML****,
- (BOOL )loadCacheData { NSString *path = [self cacheFilePath]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil ; if ([fileManager fileExistsAtPath:path isDirectory:nil ]) { NSData *data = [NSData dataWithContentsOfFile:path]; _cacheData = data; _cacheString = [[NSString alloc] initWithData:_cacheData encoding:self .cacheMetadata.stringEncoding]; switch (self .responseSerializerType) { case YTKResponseSerializerTypeHTTP: return YES ; case YTKResponseSerializerTypeJSON: _cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions )0 error:&error]; return error == nil ; case YTKResponseSerializerTypeXMLParser: _cacheXML = [[NSXMLParser alloc] initWithData:_cacheData]; return YES ; } } return NO ; }
向应用层交付缓存数据
dispatch_async(dispatch_get_main_queue(), ^{ [self requestCompletePreprocessor] ; [self requestCompleteFilter] ; YTKRequest *strongSelf = self; [strongSelf.delegate requestFinished:strongSelf] ; if (strongSelf.successCompletionBlock) { strongSelf.successCompletionBlock(strongSelf); } [strongSelf clearCompletionBlock] ;});
这里照样会通过delegate以及block进行交付,但是刚开始会感到奇怪我们刚刚加载数据的时候是加载到****_cacheString这些地方,但是这里交付却是 strongSelf****,那么我们通过strongSelf的responseString这些对象访问的时候怎么转到_cacheString中的呢?
- (NSString *)responseString { if (_cacheString) { return _cacheString; } return [super responseString]; }
嗯,就是在getter方法中拦截,所以及时重置_cacheString非常关键。
缓存数据
还有最后一个问题?这些缓存数据是怎么来的呢?
- (void )requestCompletePreprocessor { [super requestCompletePreprocessor]; if (self .writeCacheAsynchronously) { dispatch_async (ytkrequest_cache_writing_queue(), ^{ [self saveResponseDataToCacheFile:[super responseData]]; }); } else { [self saveResponseDataToCacheFile:[super responseData]]; } }
我们知道请求结束的时候会调用requestCompletePreprocessor 这时候会调用 *saveResponseDataToCacheFile **。
- (void )saveResponseDataToCacheFile:(NSData *)data { if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) { if (data != nil ) { @try { [data writeToFile:[self cacheFilePath] atomically:YES ]; YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init]; metadata.version = [self cacheVersion]; metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description; metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self ]; metadata.creationDate = [NSDate date]; metadata.appVersionString = [YTKNetworkUtils appVersionString]; [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]]; } @catch (NSException *exception) { YTKLog(@"Save cache failed, reason = %@" , exception.reason); } } } }
在saveResponseDataToCacheFile方法中会将服务端返回的数据写到缓存目录下,然后再更新缓存信息。
6. 断点续传 断点续传其实上面已经介绍过了,既然这里已经将它拎出来了,就做个总结把:
这里最关键在于两个方面: 请求中断的时候怎么缓存数据,请求开始的时候怎么续传:
YTKNetWork中会在请求失败,以及取消请求的时候对数据进行缓存:
失败时候进行缓存
- (void )requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error { NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData ]; NSURL *localUrl = nil ; if (request.resumableDownloadPath) { localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath]; } if (incompleteDownloadData && localUrl != nil ) { [incompleteDownloadData writeToURL:localUrl atomically:YES ]; } }
取消请求后的缓存
- (void )cancelRequest:(YTKBaseRequest *)request { if (request.resumableDownloadPath && [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] != nil ) { NSURLSessionDownloadTask *requestTask = (NSURLSessionDownloadTask *)request.requestTask; [requestTask cancelByProducingResumeData:^(NSData *resumeData) { NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath]; [resumeData writeToURL:localUrl atomically:YES ]; }]; } else { [request.requestTask cancel]; } [self removeRequestFromRecord:request]; [request clearCompletionBlock]; }
启动请求之前会先查看下载缓存地址,如果有数据则通过downloadTaskWithResumeData 继续:
- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath requestSerializer:(AFHTTPRequestSerializer *)requestSerializer URLString:(NSString *)URLString parameters:(id )parameters progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock error:(NSError * _Nullable __autoreleasing *)error { if ([[NSFileManager defaultManager] fileExistsAtPath:downloadTargetPath]) { [[NSFileManager defaultManager] removeItemAtPath:downloadTargetPath error:nil ]; } BOOL resumeSucceeded = NO ; __block NSURLSessionDownloadTask *downloadTask = nil ; NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:downloadPath]; if (localUrl != nil ) { BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:localUrl.path]; NSData *data = [NSData dataWithContentsOfURL:localUrl]; BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data]; BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid; 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 ; } } } return downloadTask; }
7. Filter 在YTKNetworkConfig中有两个Filter,一个是_urlFilters,一个是_cacheDirPathFilters
@implementation YTKNetworkConfig { NSMutableArray <id<YTKUrlFilterProtocol >> * _urlFilters; NSMutableArray <id<YTKCacheDirPathFilterProtocol >> * _cacheDirPathFilters; }
url filter它会对所有发起请求的requestUrl进行处理:
- (NSString *)buildRequestUrl:(YTKBaseRequest *)request { // ....... // 对构建出来的URL通过Config中预设的urlFilters 一一对它进行处理,比如添加公共字段等 NSArray *filters = [_config urlFilters]; for (id<YTKUrlFilterProtocol> f in filters) { detailUrl = [f filterUrl:detailUrl withRequest:request]; } // ...... }
cacheDirPathFilters filter会在创建缓存路径的时候调用:
- (NSString *)cacheBasePath { NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains (NSLibraryDirectory , NSUserDomainMask , YES ) objectAtIndex:0 ]; NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache" ]; 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; }
8. 插件机制 插件机制,这个在很多场合都会使用,其实就是一种观察者模式,将观察者添加到要观察对象的通知列表中,一旦某个关键环节发生了,就便利这些观察者,调用他们的方法通知他们。这时候观察者就可以插入自己想要的操作: YTKNetWork中目前有三个类支持插件机制,分别是YTKBaseRequest ,YTKBatchRequest ,YTKChainRequest
@interface YTKBaseRequest (RequestAccessory )- (void )toggleAccessoriesWillStartCallBack; - (void )toggleAccessoriesWillStopCallBack; - (void )toggleAccessoriesDidStopCallBack; @end @interface YTKBatchRequest (RequestAccessory )- (void )toggleAccessoriesWillStartCallBack; - (void )toggleAccessoriesWillStopCallBack; - (void )toggleAccessoriesDidStopCallBack; @end @interface YTKChainRequest (RequestAccessory )- (void )toggleAccessoriesWillStartCallBack; - (void )toggleAccessoriesWillStopCallBack; - (void )toggleAccessoriesDidStopCallBack; @end
他们都持有requestAccessories:
@property (nonatomic , strong , nullable ) NSMutableArray <id <YTKRequestAccessory>> *requestAccessories;- (void )addAccessory:(id <YTKRequestAccessory>)accessory;
其实个人认为这里的requestAccessories权限应该限制为readonly比较合适,只是一个小建议。
触发时机,这里以YTKBaseRequest 为例子,嗯,不解释看代码。
- (void )start { [self toggleAccessoriesWillStartCallBack]; [[YTKNetworkAgent sharedAgent] addRequest:self ]; } - (void )stop { [self toggleAccessoriesWillStopCallBack]; self .delegate = nil ; [[YTKNetworkAgent sharedAgent] cancelRequest:self ]; [self toggleAccessoriesDidStopCallBack]; }
好了 YTKNetWork的解析就到这里,今天周六,祝大家周末愉快!