YTKNetwork在GitHub的仓库中有一份高级教程,这篇我们就来看下高级教程中相关部分的源码。
YTKUrlFilterProtocol 接口
这个只是一个接口,需要实现下面的方法,可以为originUrl拼接额外的一些信息。
- (NSString *)filterUrl:(NSString *)originUrl withRequest:(YTKBaseRequest *)request;
Demo工程中提供了一种实现YTKUrlArgumentsFilter
,通过一个NSDictionary初始化,使用AFNetworking的方法AFQueryStringFromParameters
把NSDictionary转成NSString拼接到originUrlString后面。这个实现是用queryString的形式,也可以添加到header中,这就看具体的实现了。
YTKBatchRequest
批量请求,Demo中有例子:
- (void)sendBatchRequest {
GetImageApi *a = [[GetImageApi alloc] initWithImageId:@"1.jpg"];
GetImageApi *b = [[GetImageApi alloc] initWithImageId:@"2.jpg"];
GetImageApi *c = [[GetImageApi alloc] initWithImageId:@"3.jpg"];
GetUserInfoApi *d = [[GetUserInfoApi alloc] initWithUserId:@"123"];
YTKBatchRequest *batchRequest = [[YTKBatchRequest alloc] initWithRequestArray:@[a, b, c, d]];
[batchRequest startWithCompletionBlockWithSuccess:^(YTKBatchRequest *batchRequest) {
NSLog(@"succeed");
NSArray *requests = batchRequest.requestArray;
GetImageApi *a = (GetImageApi *)requests[0];
GetImageApi *b = (GetImageApi *)requests[1];
GetImageApi *c = (GetImageApi *)requests[2];
GetUserInfoApi *user = (GetUserInfoApi *)requests[3];
// deal with requests result ...
NSLog(@"%@, %@, %@, %@", a, b, c, user);
} failure:^(YTKBatchRequest *batchRequest) {
NSLog(@"failed");
}];
}
首先创建多个网络请求,把他们加到一个数组中,使用这个数组初始化一个YTKBatchRequest
批量请求,设置回调方式,并开始请求。
- (void)start {
if (_finishedCount > 0) {
YTKLog(@"Error! Batch request has already started.");
return;
}
_failedRequest = nil;
[[YTKBatchRequestAgent sharedAgent] addBatchRequest:self];
[self toggleAccessoriesWillStartCallBack];
// 直接遍历所有请求,调用start方法开始请求
for (YTKRequest * req in _requestArray) {
req.delegate = self;
[req clearCompletionBlock];
[req start];
}
}
本来我以为是用dispatch_group_t
实现的,结果一看源码,很简单,在请求回调的地方维护了一个count,等count等于初始化时请求的数量时,就说明请求全部成功了。
- (void)requestFinished:(YTKRequest *)request {
_finishedCount++;
if (_finishedCount == _requestArray.count) {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(batchRequestFinished:)]) {
[_delegate batchRequestFinished:self];
}
if (_successCompletionBlock) {
_successCompletionBlock(self);
}
[self clearCompletionBlock];
[self toggleAccessoriesDidStopCallBack];
[[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
}
}
疑问
这里其实我有两点疑问:
- 这里的
_finishedCount++
是否需要加锁,因为返回的队列是一个DISPATCH_QUEUE_CONCURRENT
队列,有可能会有多个请求同时返回,这里会不会有问题? - YTKBatchRequestAgent的作用是什么,我理解的只是为了防止这个batchRequest被release。不知道这样理解对不对?
如果有同学知道,还望留言指教。
YTKChainRequest
chain是链的意思,YTKChainRequest用于管理有相互依赖的网络请求。有些请求需要依赖前面的请求,这时候就需要用到YTKChainRequest,把请求加到chainReq中,并加上回调处理。
- (void)sendChainRequest {
RegisterApi *reg = [[RegisterApi alloc] initWithUsername:@"username" password:@"password"];
YTKChainRequest *chainReq = [[YTKChainRequest alloc] init];
[chainReq addRequest:reg callback:^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) {
RegisterApi *result = (RegisterApi *)baseRequest;
NSString *userId = [result userId];
GetUserInfoApi *api = [[GetUserInfoApi alloc] initWithUserId:userId];
[chainRequest addRequest:api callback:nil];
}];
chainReq.delegate = self;
// start to send request
[chainReq start];
}
在方法- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback;
中只是简单的把请求和回调加到数组中。调用- (void)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.");
}
}
- (BOOL)startNextRequest {
if (_nextRequestIndex < [_requestArray count]) {
YTKBaseRequest *request = _requestArray[_nextRequestIndex];
_nextRequestIndex++;
request.delegate = self;
[request clearCompletionBlock];
[request start];
return YES;
} else {
return NO;
}
}
上面添加请求的时候有两个数组,一个保存请求,一个保存回调block,这里使用了一个index
来管理,发出请求后,index➕1,delegate回调方法中执行回调数组对应index的block(如果添加的时候这个block是nil,使用一个空的block放到数组中占位)。有点绕哈,请求的时候会把回调方式设置为delegate方式,并清除block回调。
YTKBaseRequest *request = _requestArray[_nextRequestIndex];
_nextRequestIndex++;
request.delegate = self;
[request clearCompletionBlock];
[request start];
疑问:如果有多个请求,这里是否会有多个block嵌套?
加载缓存的请求
有一些请求,结果返回后我们会进行缓存,下次再进入某个页面时,我们先展示缓存数据,再进行网络请求,返回后更新缓存和界面。这种情况就可以用到下面这个例子了。
- (void)loadCacheData {
NSString *userId = @"1";
GetUserInfoApi *api = [[GetUserInfoApi alloc] initWithUserId:userId];
if ([api loadCacheWithError:nil]) {
NSDictionary *json = [api responseJSONObject];
NSLog(@"json = %@", json);
// 先显示缓存数据
}
[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
//Api中已经设置了缓存信息,所以缓存任务不用再这里处理,只需要更新UI
NSLog(@"update ui");
} failure:^(YTKBaseRequest *request) {
NSLog(@"failed");
}];
}
前面一篇已经说过,只要继承YTKRequest
,并且实现- (NSInteger)cacheTimeInSeconds;
方法,返回一个大于0的时间,就会把请求结果缓存到本地。loadCacheWithError:
会尝试从缓存中恢复数据,数据分为元数据(metaData)和结果数据。元数据中有创建时间,版本等信息,在网络请求返回时保存到NSKeyValueArchiver中,恢复的时候使用unarchiveObjectWithFile
方法进行恢复,验证时间,版本号等有效后恢复结果数据,并返回。
// 缓存请求结果
- (void)saveResponseDataToCacheFile:(NSData *)data {
if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
if (data != nil) {
@try {
// New data will always overwrite old data.
[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);
}
}
}
}
// 恢复请求结果
- (BOOL)loadCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// Make sure cache time in valid.
if ([self cacheTimeInSeconds] < 0) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}];
}
return NO;
}
// Try load metadata.
if (![self loadCacheMetadata]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}];
}
return NO;
}
// Check if cache is still valid.
if (![self validateCacheWithError:error]) {
return NO;
}
// Try load cache.
if (![self loadCacheData]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}];
}
return NO;
}
return YES;
}
- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// Date
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;
}
// Version
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;
}
// 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]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
}
return NO;
}
}
// App version
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;
}
- (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;
}
- (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:
// Do nothing.
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;
}
定制 buildCustomUrlRequest
调用YTKNetworkAgent的addRequest:
方法时,会判断自定义的请求是否实现了buildCustomUrlRequest
,如果返回不为nil,则直接使用返回的NSURLSessionTask进行请求。这里是为了有一些特殊的请求而做的定制需求。比如下面的例子,gzippingData上传数据:
- (NSURLRequest *)buildCustomUrlRequest {
NSData *rawData = [[_events jsonString] dataUsingEncoding:NSUTF8StringEncoding];
NSData *gzippingData = [NSData gtm_dataByGzippingData:rawData];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.requestUrl]];
[request setHTTPMethod:@"POST"];
[request addValue:@"application/json;charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
[request addValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
[request setHTTPBody:gzippingData];
return request;
}