YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 iOS 网络库,其实现了一套 High Level 的 API,提供了更高层次的网络访问抽象。
YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承
YTKRequest
类,通过覆盖父类的一些方法来构造指定的网络请求。
YTKBaseRequest
YTKRequest继承自YTKBaseRequest,首先来看下YTKBaseRequest
这个类,下面是请求网络的代码
#pragma mark - Request Action
- (void)start {
[self toggleAccessoriesWillStartCallBack];
[[YTKNetworkAgent sharedAgent] addRequest:self];
}
- (void)stop {
[self toggleAccessoriesWillStopCallBack];
self.delegate = nil;
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
[self toggleAccessoriesDidStopCallBack];
}
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
failure:(YTKRequestCompletionBlock)failure {
[self setCompletionBlockWithSuccess:success failure:failure];
[self start];
}
Accessory是附属物的意思,类似hook的意思,可以在每个流程点调用对应方法。
除去Accessory相关的代码,就只剩下YTKNetworkAgent
的方法调用了,有addRequest:
和cancelRequest:
两个实例方法。我们进入这里去看看。
- (void)addRequest:(YTKBaseRequest *)request {
NSParameterAssert(request != nil);
NSError * __autoreleasing requestSerializationError = nil;
// 构建requestTask(NSURLSessionTask)
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");
// 优先级设置,只在iOS 8以上有用,所以使用respondsToSelector做判断
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];
}
这个方法主要分为三个部分:
- 构造NSURLSessionTask,这也是iOS自带的网络请求库。
- 设置task的优先级,这个看注释是只在iOS8以上才有。
- 缓存请求,调用
[task resume]
,发起网络请求。
我们再进入第一个部分详细看下
// 构建requestTask(NSURLSessionTask)
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];
}
如果自定义的request实现了buildCustomUrlRequest
方法,直接使用manager创建task。如果没有实现,使用Agent
的sessionTaskForRequest:error:
方法创建task
- (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];
}
}
根据请求的方法,如果是GET方法,而且实现了resumableDownloadPath
,那么创建一个NSURLSessionDownloadTask
,其他则最终会进入到下面这个方法
- (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;
}
这里参数中有个block,是用与上传文件的请求的。根据这个参数,会调用requestSerializer不同的方法,最后调用_manager
的方法创建最终的dataTask,这些都是AFNetworking提供的方法。回调方式是block,调用Agent中的handleRequestResult:responseObject:error:
方法。
开始请求之前,有一个请求缓存操作,addRequestToRecord:
,将request对象缓存到一个Map中,key是上面创建的task的taskIdentifier
。这里还是用了多线程锁对这个缓存进行上锁,使用的是pthread_mutex_lock
。这个缓存有什么用?我们还是继续往下看,后面会揭晓。
#define Lock() pthread_mutex_lock(&_lock)
#define Unlock() pthread_mutex_unlock(&_lock)
- (void)addRequestToRecord:(YTKBaseRequest *)request {
Lock();
_requestsRecord[@(request.requestTask.taskIdentifier)] = request;
Unlock();
}
我们再看下回调方法
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
Lock();
YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
Unlock();
// When the request is cancelled and removed from records, the underlying
// AFNetworking failure callback will still kicks in, resulting in a nil `request`.
//
// Here we choose to completely ignore cancelled tasks. Neither success or failure
// callback will be called.
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];
});
}
回调方法里面就用到了上面的缓存,如果请求已经取消,缓存中已经不存在这个request对象,那么回调函数就不用再往下执行了。如果没有取消,继续下面的处理
- 首先根据响应的类型
responseSerializerType
序列化数据,支持了Json和XML两种序列化,其他的直接使用responseObject
。 - 序列化如果也没有报错的话,进行结果的验证,首先是statuscode,从task的response拿到statusCode,如果在200~299之间,那么继续。结果内容的检查只支持Json数据的检查,调用
[YTKNetworkUtils validateJSON:json withValidator:validator];
进行检查。 - 检查通过后通知请求成功,并从Map缓存中移除请求。
从上面可以看出,主要的逻辑是在YTKNetworkAgent
这个类里面。
YTKRequest
上面介绍完了YTKBaseRequest,我们回到YTKRequest中来看下。这里主要处理的是网络请求结果的缓存。
重写了开始请求的方法
- (void)start {
if (self.ignoreCache) {
[self startWithoutCache];
return;
}
// Do not cache download request.
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];
});
}
对于指定忽略缓存的任务和下载任务
,直接进行网络请求,否则进行缓存加载loadCacheWithError:
,缓存命中,直接返回,没有的话进行网络请求。
取消请求
取消请求又要回到YTKBaseRequest
中来,调用stop
方法。最后会进入YTKNetworkAgent
的cancelRequest:
方法。
- (void)stop {
[self toggleAccessoriesWillStopCallBack];
self.delegate = nil;
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
[self toggleAccessoriesDidStopCallBack];
}
- (void)cancelRequest:(YTKBaseRequest *)request {
NSParameterAssert(request != nil);
if (request.resumableDownloadPath) {
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];
}
cancelRequest:
方法会对下载任务进行处理,将下载的部分数据进行本地保存,创建下载任务的时候也会根据这个本地数据决定是否进行断点续传。
- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
error:(NSError * _Nullable __autoreleasing *)error {
// add parameters to URL;
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 targetPath is a directory, use the file name we got from the urlRequest.
// Make sure downloadTargetPath is always a file, not directory.
if (isDirectory) {
NSString *fileName = [urlRequest.URL lastPathComponent];
downloadTargetPath = [NSString pathWithComponents:@[downloadPath, fileName]];
} else {
downloadTargetPath = downloadPath;
}
// AFN use `moveItemAtURL` to move downloaded file to target path,
// this method aborts the move attempt if a file already exist at the path.
// So we remove the exist file before we start the download task.
// https://github.com/AFNetworking/AFNetworking/issues/3775
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;
// Try to resume with resumeData.
// Even though we try to validate the resumeData, this may still fail and raise excecption.
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;
}
总结
按照YTKNetwork的思想,所有的请求都会继承自YTKRequest
,所以有以下几个特点
- 默认支持下载任务的断点续传。
-
YTKRequest
的子类只要重写cacheTimeInSeconds
方法就可以实现请求结果的缓存。其他的方法都有默认实现。 - 支持请求结果Json验证。
设计模式
上面的代码中有很多Accessory相关的,这属于代理模式。除此之外在YTKNetworkConfig
中还支持添加两种filter,YTKUrlFilterProtocol
和YTKCacheDirPathFilterProtocol
,这些从设计模式上讲都属于装饰者模式。
如果有哪里写的不清楚或者不对的地方,欢迎大家学习指正。
网友评论