前言

一.網(wǎng)絡(luò)請(qǐng)求的構(gòu)建
網(wǎng)絡(luò)請(qǐng)求的構(gòu)建很簡(jiǎn)單, 根據(jù)一個(gè)請(qǐng)求需要的條件如URL, 請(qǐng)求方式, 請(qǐng)求參數(shù), 請(qǐng)求頭等定義請(qǐng)求生成的接口即可. 定義如下:
@interface HHURLRequestGenerator : NSObject
+ (instancetype)sharedInstance;
- (void)switchService;
- (void)switchToService:(HHServiceType)serviceType;
- (NSMutableURLRequest *)generateRequestWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
method:(NSString *)method
params:(NSDictionary *)params
header:(NSDictionary *)header;
- (NSMutableURLRequest *)generateUploadRequestUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
params:(NSDictionary *)params
contents:(NSArray<HHUploadFile *> *)contents
header:(NSDictionary *)header;
@end
可以看到方法參數(shù)都是生成請(qǐng)求基本組成部分, 當(dāng)然, 這里的參數(shù)比較少, 因?yàn)樵谖业捻?xiàng)目中像請(qǐng)求超時(shí)時(shí)間都是一樣的, 類似這些公用的設(shè)置我都偷懶直接寫在請(qǐng)求配置文件里面了. 我們看看請(qǐng)求接口的具體實(shí)現(xiàn), 以數(shù)據(jù)請(qǐng)求為例:
- (NSMutableURLRequest *)generateRequestWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps method:(NSString *)method params:(NSDictionary *)params header:(NSDictionary *)header {
NSString *urlString = [self urlStringWithPath:urlPath useHttps:useHttps];
NSMutableURLRequest *request = [self.requestSerialize requestWithMethod:method URLString:urlString parameters:params error:nil];
request.timeoutInterval = RequestTimeoutInterval;
[self setCookies];//設(shè)置cookie
[self setCommonRequestHeaderForRequest:request];// 在這里做公用請(qǐng)求頭的設(shè)置
[header enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
[request setValue:value forHTTPHeaderField:key];
}];
return request;
}
- (NSString *)urlStringWithPath:(NSString *)path useHttps:(BOOL)useHttps {
if ([path hasPrefix:@"http"]) {
return path;
} else {
NSString *baseUrlString = [HHService currentService].baseUrl;
if (useHttps && baseUrlString.length > 4) {
NSMutableString *mString = [NSMutableString stringWithString:baseUrlString];
[mString insertString:@"s" atIndex:4];
baseUrlString = [mString copy];
}
return [NSString stringWithFormat:@"%@%@", baseUrlString, path];
}
}
代碼很簡(jiǎn)單, 接口根據(jù)參數(shù)調(diào)用urlStringWithPath:useHttps:通過(guò)BaseURL和URLPath拼裝出完整的URL, 然后用這個(gè)URL和其他參數(shù)生成一個(gè)URLRequest, 然后調(diào)用setCommonRequestHeaderForRequest:設(shè)置公用請(qǐng)求, 最后返回這個(gè)URLRequest.
BaseURL來(lái)自HHService, HHService對(duì)外暴露各個(gè)環(huán)境(測(cè)試/開(kāi)發(fā)/發(fā)布)下的baseURL和切換服務(wù)器的接口, 內(nèi)部走工廠生成當(dāng)前的服務(wù)器, 我的設(shè)置是默認(rèn)連接第一個(gè)服務(wù)器且APP關(guān)閉后恢復(fù)此設(shè)置, APP運(yùn)行中可根據(jù)需要調(diào)用switchService切換服務(wù)器.
HHService定義如下:
@protocol HHService <NSObject>
@optional
- (NSString *)testEnvironmentBaseUrl;
- (NSString *)developEnvironmentBaseUrl;
- (NSString *)releaseEnvironmentBaseUrl;
@end
@interface HHService : NSObject<HHService>
+ (HHService *)currentService;
+ (void)switchService;
+ (void)switchToService:(HHServiceType)serviceType;
- (NSString *)baseUrl;
- (HHServiceEnvironment)environment;
@end
#import "HHService.h"
@interface HHService ()
@property (assign, nonatomic) HHServiceType type;
@property (assign, nonatomic) HHServiceEnvironment environment;
@end
@interface HHServiceX : HHService
@end
@interface HHServiceY : HHService
@end
@interface HHServiceZ : HHService
@end
@implementation HHService
#pragma mark - Interface
static HHService *currentService;
static dispatch_semaphore_t lock;
+ (HHService *)currentService {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = dispatch_semaphore_create(1);
currentService = [HHService serviceWithType:HHService0];
});
return currentService;
}
+ (void)switchService {
[self switchToService:self.currentService.type + 1];
}
+ (void)switchToService:(HHServiceType)serviceType {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
currentService = [HHService serviceWithType:(serviceType % ServiceCount)];
dispatch_semaphore_signal(lock);
}
+ (HHService *)serviceWithType:(HHServiceType)type {
HHService *service;
switch (type) {
case HHService0: service = [HHServiceX new]; break;
case HHService1: service = [HHServiceY new]; break;
case HHService2: service = [HHServiceZ new]; break;
}
service.type = type;
service.environment = BulidServiceEnvironment;
return service;
}
- (NSString *)baseUrl {
switch (self.environment) {
case HHServiceEnvironmentTest: return [self testEnvironmentBaseUrl];
case HHServiceEnvironmentDevelop: return [self developEnvironmentBaseUrl];
case HHServiceEnvironmentRelease: return [self releaseEnvironmentBaseUrl];
}
}
@end
2.網(wǎng)絡(luò)請(qǐng)求的派發(fā)
請(qǐng)求的派發(fā)是通過(guò)一個(gè)單例HHNetworkClient來(lái)實(shí)現(xiàn)的, 如果把請(qǐng)求比作炮彈的話, 那么這個(gè)單例就是發(fā)射炮彈的炮臺(tái), 使用炮臺(tái)的人只需要告訴炮臺(tái)需要發(fā)射什么樣的炮彈和炮彈的打擊目標(biāo)便可發(fā)射了. 另外, 應(yīng)該提供取消打擊的功能以處理不必要的打擊的情況, 那么, 根據(jù)炮臺(tái)的作用.
HHNetworkClient定義如下:
@interface HHNetworkClient : NSObject
+ (instancetype)sharedInstance;
- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
requestType:(HHNetworkRequestType)requestType
params:(NSDictionary *)params
header:(NSDictionary *)header
completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
requestType:(HHNetworkRequestType)requestType
params:(NSDictionary *)params
header:(NSDictionary *)header
completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;
- (NSNumber *)dispatchTask:(NSURLSessionTask *)task;
- (NSNumber *)uploadDataWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
params:(NSDictionary *)params
contents:(NSArray<HHUploadFile *> *)contents
header:(NSDictionary *)header
progressHandler:(void(^)(NSProgress *))progressHandler
completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;
- (void)cancelAllTask;
- (void)cancelTaskWithTaskIdentifier:(NSNumber *)taskIdentifier;
@end
@interface HHNetworkClient ()
@property (strong, nonatomic) AFHTTPSessionManager *sessionManager;
@property (strong, nonatomic) NSMutableDictionary<NSNumber *, NSURLSessionTask *> *dispathTable;
@property (assign, nonatomic) CGFloat totalTaskCount;
@property (assign, nonatomic) CGFloat errorTaskCount;
@end
1.請(qǐng)求的派發(fā)與取消
外部暴露數(shù)據(jù)請(qǐng)求和文件上傳的接口, 參數(shù)為構(gòu)建請(qǐng)求所需的必要參數(shù), 返回值為此次請(qǐng)求任務(wù)的taskIdentifier, 調(diào)用方可以通過(guò)taskIdentifier取消正在執(zhí)行的請(qǐng)求任務(wù).
內(nèi)部聲明一個(gè)dispathTable保持著此時(shí)正在執(zhí)行的任務(wù), 并在任務(wù)執(zhí)行完成或者任務(wù)取消時(shí)移除任務(wù)的引用, 以數(shù)據(jù)請(qǐng)求為例, 具體實(shí)現(xiàn)如下:
- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
NSString *method = (requestType == HHNetworkRequestTypeGet ? @"GET" : @"POST");
NSMutableURLRequest *request = [[HHURLRequestGenerator sharedInstance] generateRequestWithUrlPath:urlPath useHttps:useHttps method:method params:params header:header];
NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self checkSeriveWithTaskError:error];
[self.dispathTable removeObjectForKey:taskIdentifier.firstObject];
dispatch_semaphore_signal(lock);
completionHandler ? completionHandler(response, responseObject, error) : nil;
}];
taskIdentifier[0] = @(task.taskIdentifier);
return task;
}
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
return [self dispatchTask:[self dataTaskWithUrlPath:urlPath useHttps:useHttps requestType:requestType params:params header:header completionHandler:completionHandler]];
}
- (NSNumber *)dispatchTask:(NSURLSessionDataTask *)task {
if (task == nil) { return @-1; }
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
self.totalTaskCount += 1;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
dispatch_semaphore_signal(lock);
[task resume];
return @(task.taskIdentifier);
}
代碼很簡(jiǎn)單, 通過(guò)參數(shù)生成URLRequest, 然后通過(guò)AFHTTPSessionManager執(zhí)行任務(wù), 在任務(wù)執(zhí)行前我們以task.taskIdentifier為key保持一下執(zhí)行的任務(wù), 然后在任務(wù)執(zhí)行后我們移除這個(gè)任務(wù), 當(dāng)然, 外部也可以在必要的時(shí)候通過(guò)我們返回的task.taskIdentifier手動(dòng)移除任務(wù).
注意我們先聲明一個(gè)NSMutableArray來(lái)標(biāo)志taskIdentifier, 然后在任務(wù)生成后設(shè)置taskIdentifier[0]為task. taskIdentifier, 最后在任務(wù)完成的回調(diào)block中使用taskIdentifier[0]來(lái)移除這個(gè)已經(jīng)完成的任務(wù).
可能有人會(huì)有疑問(wèn)為什么不直接使用task.taskIdentifier, block不是可以捕獲task嗎? 下面解釋一下為什么這樣寫:
我們知道block之于函數(shù)最大的區(qū)別就在于它可以捕獲自身作用域外的對(duì)象, 并在block執(zhí)行的時(shí)候訪問(wèn)被捕獲的對(duì)象, 具體的, 對(duì)于值類型對(duì)象block會(huì)生成一份此對(duì)象的拷貝, 對(duì)于引用類型對(duì)象block會(huì)生成一個(gè)此對(duì)象的引用并使該對(duì)象的引用計(jì)數(shù)+1(這里我們只描述非__block修飾的情況). 那么代入到上面的代碼, 我們來(lái)一步一步分析:
- 直接捕獲task的寫法
NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
...略
[self.dispathTable removeObjectForKey:@(task.taskIdentifier)];
...略
}];
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
我們把它拆開(kāi)來(lái)看:
NSURLSessionDataTask *task;
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
...略
[self.dispathTable removeObjectForKey:@(task.taskIdentifier)];
...略
}];
task = returnTask;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
可以看到returnTask是我們實(shí)際存儲(chǔ)的任務(wù), 而task只是一個(gè)臨時(shí)變量, 此時(shí)task指向nil, 那我們生成returnTask的block此時(shí)捕獲到的task也就是nil, 所以在任務(wù)完成的時(shí)候我們的task.taskIdentifier一定是0, 這樣寫的結(jié)果就是dispathTable只會(huì)添加不會(huì)刪除(系統(tǒng)的taskIdentifier是從0開(kāi)始依次遞增的), 當(dāng)然, 因?yàn)檫M(jìn)行中的returnTask我們是做了存儲(chǔ)的, 所以在任務(wù)未完成的時(shí)候我們還是可以做取消的.
- 如果一開(kāi)始給task一個(gè)占位對(duì)象呢不讓它為nil可以嗎?
NSURLSessionDataTask *task = [NSObject new]; //1.suspend
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
...略
[self.dispathTable removeObjectForKey:@(task.taskIdentifier)];//3.completed
...略
}];//2.alloc
task = returnTask;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
這樣其實(shí)就是一個(gè)簡(jiǎn)單的引用變換題了, 我們來(lái)看看各個(gè)指針的指向情況:
suspend: pTask->NSObject block.pTask->nil pReturnTask->nil
alloc: pTask-> NSObject block.pTask->NSObject pReturnTask->returnTask
completed: pTask->returnTask block.pTask->NSObject pReturnTask->returnTask
可以看到在任務(wù)執(zhí)行完成時(shí)我們?cè)L問(wèn)block.pTask時(shí)也不過(guò)是我們一開(kāi)始的占位對(duì)象, 所以這個(gè)方案也不行, 當(dāng)然, 取消任務(wù)依然可用
事實(shí)上block.pTask確實(shí)是捕獲了占位對(duì)象, 只是我們?cè)谀侵鬀](méi)有替換block.pTask指向到returnTask, 然而block.pTask我們是訪問(wèn)不了的, 所以這個(gè)方案行不通.
- 如果我們的占位對(duì)象是一個(gè)容器呢?
NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
...略
[self.dispathTable removeObjectForKey:@(taskIdentifier.firstObject)];
...略
}];
taskIdentifier[0] = @(returnTask.taskIdentifier);
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
既然我們?cè)L問(wèn)不了block.pTask那就訪問(wèn)block.pTask指向的對(duì)象嘛, 更改這個(gè)對(duì)象的內(nèi)容不就相當(dāng)于更改了block.pTask么, 大家照著2的思路走一下應(yīng)該很容易就能想通, 我就不多說(shuō)了.
2.多服務(wù)器的切換
關(guān)于多服務(wù)器其實(shí)我也沒(méi)有實(shí)際的經(jīng)驗(yàn), 公司正在部署第二臺(tái)服務(wù)器, 具體需求是如果訪問(wèn)第一臺(tái)服務(wù)器總是超時(shí)或者出錯(cuò), 那就切換到第二臺(tái)服務(wù)器, 基于此需求我簡(jiǎn)單的實(shí)現(xiàn)一下:
- (NSNumber *)dispatchTask:(NSURLSessionDataTask *)task {
...略
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
self.totalTaskCount += 1;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
dispatch_semaphore_signal(lock);
...略
}
- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
NSString *method = (requestType == HHNetworkRequestTypeGet ? @"GET" : @"POST");
...略
NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
...略
[self checkSeriveWithTaskError:error];
...略
}];
...略
}
- (void)checkSeriveWithTaskError:(NSError *)error {
if ([HHAppContext sharedInstance].isReachable) {
switch (error.code) {
case NSURLErrorUnknown:
case NSURLErrorTimedOut:
case NSURLErrorCannotConnectToHost: {
self.errorTaskCount += 1;
}
default:break;
}
if (self.totalTaskCount >= 40 && (self.errorTaskCount / self.totalTaskCount) == 0.1) {
self.totalTaskCount = self.errorTaskCount = 0;
[[HHURLRequestGenerator sharedInstance] switchService];
}
}
}
- (void)didReceivedSwitchSeriveNotification:(NSNotification *)notif {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
self.totalTaskCount = self.errorTaskCount = 0;
dispatch_semaphore_signal(lock);
[[HHURLRequestGenerator sharedInstance] switchToService:[notif.userInfo[@"service"] integerValue]];
}
假設(shè)認(rèn)為APP在此次使用過(guò)程中網(wǎng)絡(luò)任務(wù)的錯(cuò)誤率達(dá)到10%那就應(yīng)該切換一下服務(wù)器, 我們?cè)谌蝿?wù)派發(fā)前將任務(wù)總數(shù)+1, 然后在任務(wù)結(jié)束后判斷任務(wù)是否成功, 失敗的話將任務(wù)失敗總數(shù)+1再判斷是否到達(dá)最大錯(cuò)誤率, 進(jìn)而切換到另一臺(tái)服務(wù)器.
另外還有一種情況是大部分服務(wù)器都掛了, 后臺(tái)直接走APNS推送可用的服務(wù)器序號(hào)過(guò)來(lái), 就不用挨個(gè)挨個(gè)切換了.
三.合理的使用請(qǐng)求派發(fā)器
OK, 炮彈有了, 炮臺(tái)也就緒了, 接下來(lái)看看如何使用這個(gè)炮臺(tái).
#pragma mark - HHAPIConfiguration
typedef void(^HHNetworkTaskProgressHandler)(CGFloat progress);
typedef void(^HHNetworkTaskCompletionHander)(NSError *error, id result);
@interface HHAPIConfiguration : NSObject
@property (copy, nonatomic) NSString *urlPath;
@property (strong, nonatomic) NSDictionary *requestParameters;
@property (assign, nonatomic) BOOL useHttps;
@property (strong, nonatomic) NSDictionary *requestHeader;
@property (assign, nonatomic) HHNetworkRequestType requestType;
@end
@interface HHDataAPIConfiguration : HHAPIConfiguration
@property (assign, nonatomic) NSTimeInterval cacheValidTimeInterval;
@end
@interface HHUploadAPIConfiguration : HHAPIConfiguration
@property (strong, nonatomic) NSArray<HHUploadFile *> * uploadContents;
@end
#pragma mark - HHAPIManager
@interface HHAPIManager : NSObject
- (void)cancelAllTask;
- (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier;
+ (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier;
+ (void)cancelTasksWithtaskIdentifiers:(NSArray *)taskIdentifiers;
- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchUploadTaskWithConfiguration:(HHUploadAPIConfiguration *)config progressHandler:(HHNetworkTaskProgressHandler)progressHandler completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
@end
- (void)cancelAllTask {
for (NSNumber *taskIdentifier in self.loadingTaskIdentifies) {
[[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
}
[self.loadingTaskIdentifies removeAllObjects];
}
- (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier {
[[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
[self.loadingTaskIdentifies removeObject:taskIdentifier];
}
+ (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier {
[[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
}
+ (void)cancelTasksWithtaskIdentifiers:(NSArray *)taskIdentifiers {
for (NSNumber *taskIdentifier in taskIdentifiers) {
[[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
}
}
- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
return [[HHNetworkClient sharedInstance] dataTaskWithUrlPath:config.urlPath useHttps:config.useHttps requestType:config.requestType params:config.requestParameters header:config.requestHeader completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
completionHandler ? completionHandler([self formatError:error], responseObject) : nil;
}];
}
HHAPIManager對(duì)外提供數(shù)據(jù)請(qǐng)求和取消的接口, 內(nèi)部調(diào)用HHNetworkClient進(jìn)行實(shí)際的請(qǐng)求操作.
1.協(xié)議還是配置對(duì)象?
HHAPIManager的接口我們并沒(méi)有像之前一樣提供多個(gè)參數(shù), 而是將多個(gè)參數(shù)組合為一個(gè)配置對(duì)象, 下面說(shuō)一下為什么這樣做:
- 為什么多個(gè)參數(shù)的接口方式不好?
一個(gè)APP中調(diào)用的API通常都是數(shù)以百計(jì)甚至千計(jì), 如果有一天需要對(duì)已成型的所有的API都追加一個(gè)參數(shù), 此時(shí)的改動(dòng)之多, 足使男程序員沉默, 女程序員流淚.
舉個(gè)例子: APP1.0已經(jīng)上線, 1.1版本總監(jiān)突然要求對(duì)數(shù)據(jù)請(qǐng)求加上緩存, 操作請(qǐng)求不用加緩存, 如果是參數(shù)接口的形式一般就是這樣寫:
//老接口
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
method:(NSString *)method
params:(NSDictionary *)params
header:(NSDictionary *)header;
//新接口
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
useHttps:(BOOL)useHttps
method:(NSString *)method
params:(NSDictionary *)params
header:(NSDictionary *)header
shouldCache:(BOOL)shouldCache;
然后原來(lái)的老接口全都調(diào)用新接口shouldCache默認(rèn)傳NO, 不需要緩存的API不用做改動(dòng), 而需要緩存的API都得改調(diào)用新接口然后shouldCache傳YES.
這樣能暫時(shí)解決問(wèn)題, 工作量也會(huì)小一些, 然后過(guò)了兩天總監(jiān)過(guò)來(lái)說(shuō), 為什么沒(méi)有對(duì)API區(qū)分緩存時(shí)間? 還有, 我們又有新需求了. 呵呵!
- 使用協(xié)議提升拓展性
@protocol HHAPIManager <NSObject>
@required
- (BOOL)useHttps;
- (NSString *)urlPath;
- (NSDictionary *)parameters;
- (OTSNetworkRequestType)requestType;
@optional
- (BOOL)checkParametersIsValid;
- (NSTimeInterval)cacheValidTimeInterval;
- (NSArray<OTSUploadFile *> *)uploadContents;
@end
@interface HHAPIManager : NSObject<HHAPIManager>
...略
- (NSNumber *)dispatchTaskWithCompletionHandler:(OTSNetworkTaskCompletionHander)completionHandler;
...略
@end
其實(shí)最初的設(shè)計(jì)是走協(xié)議的, HHAPIManager遵守這個(gè)協(xié)議, 內(nèi)部給上默認(rèn)參數(shù), dispatchTaskWithCompletionHandler:會(huì)去挨個(gè)獲取這些參數(shù), 各個(gè)子類自行實(shí)現(xiàn)自己自定義的部分, 這樣以后就算有任何拓展, 只需要在協(xié)議里面加個(gè)方法基類給上默認(rèn)值, 有需要的子類API重寫一下就行了.
- 替換協(xié)議為配置對(duì)象
- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchUploadTaskWithConfiguration:(HHUploadAPIConfiguration *)config progressHandler:(HHNetworkTaskProgressHandler)progressHandler completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
協(xié)議的方案其實(shí)很好, 也是我想要的設(shè)計(jì). 但是協(xié)議是針對(duì)類而言的, 這意味著今后的每添加一個(gè)API就需要新建一個(gè)HHAPIManager的子類, 很容易就有了幾百個(gè)API類文件, 維護(hù)起來(lái)很麻煩, 找起來(lái)很麻煩(以上是同事要求替換協(xié)議的理由, 我仍然支持協(xié)議, 但是他們?nèi)硕?. 所以將協(xié)議替換為配置對(duì)象, 然后API以模塊功能劃分, 每個(gè)模塊一個(gè)類文件給出多個(gè)API接口 ,內(nèi)部每個(gè)API搭上合適的配置對(duì)象, 這樣一來(lái)只需要十幾個(gè)類文件.
總之, 考慮到配置對(duì)象既可以實(shí)現(xiàn)單個(gè)API單個(gè)類的設(shè)計(jì), 也可以滿足同事的需求, 協(xié)議被換成了配置對(duì)象.
另外, 所有的block參數(shù)都不寫在配置對(duì)象里, 而是直接在接口處聲明, 看著別扭寫著方便(block做參數(shù)和做屬性哪個(gè)寫起來(lái)簡(jiǎn)單大家都懂的).
2.簡(jiǎn)單的請(qǐng)求結(jié)果緩存器
上面簡(jiǎn)單提到了請(qǐng)求緩存, 其實(shí)我們是沒(méi)有做緩存的, 因?yàn)槲宜綡TTP的API現(xiàn)在基本上都被廢棄了, 全是走TCP, 然而TCP的緩存又是另一個(gè)故事了.但是還是簡(jiǎn)單實(shí)現(xiàn)一下吧:
#define HHCacheManager [HHNetworkCacheManager sharedManager]
@interface HHNetworkCache : NSObject
+ (instancetype)cacheWithData:(id)data;
+ (instancetype)cacheWithData:(id)data validTimeInterval:(NSUInteger)interterval;
- (id)data;
- (BOOL)isValid;
@end
@interface HHNetworkCacheManager : NSObject
+ (instancetype)sharedManager;
- (void)removeObejectForKey:(id)key;
- (void)setObjcet:(HHNetworkCache *)object forKey:(id)key;
- (HHNetworkCache *)objcetForKey:(id)key;
@end
#define ValidTimeInterval 60
@implementation HHNetworkCache
+ (instancetype)cacheWithData:(id)data {
return [self cacheWithData:data validTimeInterval:ValidTimeInterval];
}
+ (instancetype)cacheWithData:(id)data validTimeInterval:(NSUInteger)interterval {
HHNetworkCache *cache = [HHNetworkCache new];
cache.data = data;
cache.cacheTime = [[NSDate date] timeIntervalSince1970];
cache.validTimeInterval = interterval > 0 ? interterval : ValidTimeInterval;
return cache;
}
- (BOOL)isValid {
if (self.data) {
return [[NSDate date] timeIntervalSince1970] - self.cacheTime < self.validTimeInterval;
}
return NO;
}
@end
#pragma mark - HHNetworkCacheManager
@interface HHNetworkCacheManager ()
@property (strong, nonatomic) NSCache *cache;
@end
@implementation HHNetworkCacheManager
+ (instancetype)sharedManager {
static HHNetworkCacheManager *sharedManager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[super allocWithZone:NULL] init];
[sharedManager configuration];
});
return sharedManager;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedManager];
}
- (void)configuration {
self.cache = [NSCache new];
self.cache.totalCostLimit = 1024 * 1024 * 20;
}
#pragma mark - Interface
- (void)setObjcet:(HHNetworkCache *)object forKey:(id)key {
[self.cache setObject:object forKey:key];
}
- (void)removeObejectForKey:(id)key {
[self.cache removeObjectForKey:key];
}
- (HHNetworkCache *)objcetForKey:(id)key {
return [self.cache objectForKey:key];
}
@end
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler{
NSString *cacheKey;
if (config.cacheValidTimeInterval > 0) {
NSMutableString *mString = [NSMutableString stringWithString:config.urlPath];
NSMutableArray *requestParameterKeys = [config.requestParameters.allKeys mutableCopy];
if (requestParameterKeys.count > 1) {
[requestParameterKeys sortedArrayUsingComparator:^NSComparisonResult(NSString * _Nonnull obj1, NSString * _Nonnull obj2) {
return [obj1 compare:obj2];
}];
}
[requestParameterKeys enumerateObjectsUsingBlock:^(NSString * _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) {
[mString appendFormat:@"&%@=%@",key, config.requestParameters[key]];
}];
cacheKey = [self md5WithString:[mString copy]];
HHNetworkCache *cache = [HHCacheManager objcetForKey:cacheKey];
if (!cache.isValid) {
[HHCacheManager removeObejectForKey:cacheKey];
} else {
completionHandler ? completionHandler(nil, cache.data) : nil;
return @-1;
}
}
NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
taskIdentifier[0] = [[HHNetworkClient sharedInstance] dispatchTaskWithUrlPath:config.urlPath useHttps:config.useHttps requestType:config.requestType params:config.requestParameters header:config.requestHeader completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (!error && config.cacheValidTimeInterval > 0) {
HHNetworkCache *cache = [HHNetworkCache cacheWithData:responseObject validTimeInterval:config.cacheValidTimeInterval];
[HHCacheManager setObjcet:cache forKey:cacheKey];
}
[self.loadingTaskIdentifies removeObject:taskIdentifier.firstObject];
completionHandler ? completionHandler([self formatError:error], responseObject) : nil;
}];
[self.loadingTaskIdentifies addObject:taskIdentifier.firstObject];
return taskIdentifier.firstObject;
簡(jiǎn)單定義一個(gè)HHCache對(duì)象, 存放緩存數(shù)據(jù), 緩存時(shí)間, 緩存時(shí)效, 然后HHNetworkCacheManager單例對(duì)象內(nèi)部用NSCache存儲(chǔ)緩存對(duì)象, 因?yàn)镹SCache自帶線程安全特效, 連鎖都不用.
在任務(wù)發(fā)起之前我們檢查一下是否有可用緩存, 有可用緩存直接返回, 沒(méi)有就走網(wǎng)絡(luò), 網(wǎng)絡(luò)任務(wù)成功后存一下請(qǐng)求數(shù)據(jù)即可.
3.請(qǐng)求結(jié)果的格式化
網(wǎng)絡(luò)任務(wù)完成后帶回的數(shù)據(jù)以什么樣的形式返回給調(diào)用方, 分兩種情況: 任務(wù)成功和任務(wù)失敗.這里我們定義一下任務(wù)成功和失敗, 成功表示網(wǎng)絡(luò)請(qǐng)求成功且?guī)Щ亓丝捎脭?shù)據(jù), 失敗表示未獲取到可用數(shù)據(jù).
舉個(gè)例子: 獲取一個(gè)話題列表, 用戶希望看到的看到是一排排彩色頭像, 如果你調(diào)用API拿不到這一堆數(shù)據(jù)那對(duì)于用戶來(lái)說(shuō)就是失敗的. 那么沒(méi)拿到數(shù)據(jù)可能是網(wǎng)絡(luò)出錯(cuò)了, 或者網(wǎng)絡(luò)沒(méi)有問(wèn)題只是用戶沒(méi)有關(guān)注過(guò)任何話題, 那么相應(yīng)的展示網(wǎng)絡(luò)錯(cuò)誤提示或者推薦話題提示.
任務(wù)成功的話很簡(jiǎn)單, 直接做相應(yīng)JSON解析正常返回就行, 如果某個(gè)XXXAPI有特殊需求那就新加一個(gè)XXXAPIConfig繼承APIConfig基類, 在里面添加屬性或者方法描述一下你有什么特殊需求, XXXAPI負(fù)責(zé)格式好返回就行了(所以還是一個(gè)API一個(gè)類好, 干凈).
任務(wù)失敗的話就麻煩一點(diǎn), 我希望任何API都能友好的返回錯(cuò)誤提示, 具體的, 如果有錯(cuò)誤發(fā)生了, 那么返回給調(diào)用方的error.code一定是可讀的枚舉而不是301之類的需要比對(duì)文檔的錯(cuò)誤碼(必須), error.domain通常就是錯(cuò)誤提示語(yǔ)(可選), 這就要求程序員寫每個(gè)API時(shí)都定義好錯(cuò)誤枚舉(所以還是一個(gè)API一個(gè)類好, 干凈)和相應(yīng)的錯(cuò)誤提示.大概是這樣子:
//HHNetworkTaskError.h 通用錯(cuò)誤
typedef enum : NSUInteger {
HHNetworkTaskErrorTimeOut = 101,
HHNetworkTaskErrorCannotConnectedToInternet = 102,
HHNetworkTaskErrorCanceled = 103,
HHNetworkTaskErrorDefault = 104,
HHNetworkTaskErrorNoData = 105,
HHNetworkTaskErrorNoMoreData = 106
} HHNetworkTaskError;
static NSError *HHError(NSString *domain, int code) {
return [NSError errorWithDomain:domain code:code userInfo:nil];
}
static NSString *HHNoDataErrorNotice = @"這里什么也沒(méi)有~";
static NSString *HHNetworkErrorNotice = @"當(dāng)前網(wǎng)絡(luò)差, 請(qǐng)檢查網(wǎng)絡(luò)設(shè)置~";
static NSString *HHTimeoutErrorNotice = @"請(qǐng)求超時(shí)了~";
static NSString *HHDefaultErrorNotice = @"請(qǐng)求失敗了~";
static NSString *HHNoMoreDataErrorNotice = @"沒(méi)有更多了~";
- (NSError *)formatError:(NSError *)error {
if (error != nil) {
switch (error.code) {
case NSURLErrorCancelled: {
error = HHError(HHDefaultErrorNotice, HHNetworkTaskErrorCanceled);
} break;
case NSURLErrorTimedOut: {
error = HHError(HHTimeoutErrorNotice, HHNetworkTaskErrorTimeOut);
} break;
case NSURLErrorCannotFindHost:
case NSURLErrorCannotConnectToHost:
case NSURLErrorNotConnectedToInternet: {//應(yīng)產(chǎn)品要求, 所有連不上服務(wù)器都是用戶網(wǎng)絡(luò)的問(wèn)題
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
} break;
default: {
error = HHError(HHNoDataErrorNotice, HHNetworkTaskErrorDefault);
} break;
}
}
return error;
}
通用的錯(cuò)誤枚舉和提示語(yǔ)定義在一個(gè).h中, 以后有新增通用描述都在這里添加, 便于管理. HHAPIManager基類會(huì)先格式好某些通用錯(cuò)誤, 然后各個(gè)子類定義自己特有的錯(cuò)誤枚舉(不可和通用描述沖突)和錯(cuò)誤描述, 像這樣:
//HHTopicAPIManager.h
typedef enum : NSUInteger {
HHUserInfoTaskErrorNotExistUserId = 1001,//用戶不存在
HHUserInfoTaskError1,//瞎寫的, 意思到就行
HHUserInfoTaskError2
} HHUserInfoTaskError;
typedef enum : NSUInteger {
HHUserFriendListTaskError0 = 1001,
HHUserFriendListTaskError1,
HHUserFriendListTaskError2,
} HHTopicListTaskError;
//HHTopicAPIManager.m
- (NSNumber *)fetchUserInfoWithUserId:(NSUInteger)userId completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
HHDataAPIConfiguration *config = [HHDataAPIConfiguration new];
config.urlPath = @"fetchUserInfoWithUserIdPath";
config.requestParameters = nil;
return [super dispatchDataTaskWithConfiguration:config completionHandler:^(NSError *error, id result) {
if (!error) {//通用錯(cuò)誤基類已經(jīng)處理好, 做好自己的數(shù)據(jù)格式就行
switch ([result[@"code"] integerValue]) {
case 200: {
// 請(qǐng)求數(shù)據(jù)無(wú)誤做相應(yīng)解析
// result = [HHUser objectWithKeyValues:result[@"data"]];
} break;
case 301: {
error = HHError(@"用戶不存在", HHUserInfoTaskErrorNotExistUserId);
} break;
case 302: {
error = HHError(@"xxx錯(cuò)誤", HHUserInfoTaskError1);
} break;
case 303: {
error = HHError(@"yyy錯(cuò)誤", HHUserInfoTaskError2);
} break;
default:break;
}
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
然后調(diào)用方一般情況下只需要這樣:
[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
}];
當(dāng)然, 情況復(fù)雜的話只能這樣, 代碼多一點(diǎn), 但是有枚舉讀起來(lái)也不麻煩:
[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
error ? [self showErrorViewWithError:error] : [self reloadTableViewWithNames:result];
}];
- (void)showErrorViewWithError:(NSError *)error {
switch (error.code) {//如果情況復(fù)雜就自己switch
case HHNetworkTaskErrorTimeOut: {
// 展示請(qǐng)求超時(shí)錯(cuò)誤頁(yè)面
} break;
case HHNetworkTaskErrorCannotConnectedToInternet: {
// 展示網(wǎng)絡(luò)錯(cuò)誤頁(yè)面
}
case HHUserInfoTaskErrorNotExistUserId: {
// ...
}
// ...
default:break;
}
}
這里多扯兩句, 請(qǐng)求的回調(diào)我是以(error, id)的形式返回的, 而不是像AFN那樣分別給出successBlock和failBlock. 其實(shí)我本身是很支持AFN的做法的, 區(qū)分成功和錯(cuò)誤強(qiáng)行讓兩種業(yè)務(wù)的代碼出現(xiàn)在兩個(gè)不同的部分, 這很好, 不同的業(yè)務(wù)處理就該在不同函數(shù)/方法里面. 但是實(shí)際開(kāi)發(fā)中有很多成功和失敗都會(huì)執(zhí)行的操作, 典型的例子就是HUD, 兩個(gè)block的話我需要在兩個(gè)地方都加上[HUD hide], 這樣的代碼寫的多了就會(huì)很煩, 而我又懶, 所以就成功失敗都在一個(gè)回調(diào)返回了.
但是! 你也應(yīng)該區(qū)分不同的業(yè)務(wù)寫出兩個(gè)不同方法(像上面那樣做), 至于公用的部分就只寫一次就夠了.像這樣:
[hud show:YES];
[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
[hud hide:YES];
error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
}];
再說(shuō)一句, 即使你比我還懶, 不聲明兩個(gè)方法那也應(yīng)該將較短的邏輯寫在前面, 較長(zhǎng)的寫在后面, 易讀, 像這樣:
if (!error) {
...短
...短
} else {
switch (error.code) {//如果情況復(fù)雜就自己switch
case HHNetworkTaskErrorTimeOut: {
// 展示請(qǐng)求超時(shí)錯(cuò)誤頁(yè)面
} break;
case HHNetworkTaskErrorCannotConnectedToInternet: {
// 展示網(wǎng)絡(luò)錯(cuò)誤頁(yè)面
}
case HHUserInfoTaskErrorNotExistUserId: {
// ...長(zhǎng)
}
// ...長(zhǎng)
default:break;
}
}
}
4.兩個(gè)小玩意兒
文章到這基本上這個(gè)網(wǎng)絡(luò)層該說(shuō)的都說(shuō)的差不多了, 各位可以根據(jù)自己的需求改動(dòng)改動(dòng)就能用了, 最后簡(jiǎn)單介紹下兩個(gè)和它相關(guān)的小玩意兒就結(jié)尾吧:
- HHNetworkTaskGroup
@protocol HHNetworkTask <NSObject>
- (void)cancel;
- (void)resume;
@end
@interface HHNetworkTaskGroup : NSObject
- (void)addTaskWithMessgeType:(NSInteger)type message:(id)message completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (void)addTask:(id<HHNetworkTask>)task;
- (void)cancel;
- (void)dispatchWithNotifHandler:(void(^)(void))notifHandler;
@end
@interface HHNetworkTaskGroup ()
@property (copy, nonatomic) void(^notifHandler)(void);
@property (assign, nonatomic) NSInteger signal;
@property (strong, nonatomic) NSMutableSet *tasks;
@property (strong, nonatomic) dispatch_semaphore_t lock;
@property (strong, nonatomic) id keeper;
@end
@implementation HHNetworkTaskGroup
//- (void)addTaskWithMessgeType:(HHSocketMessageType)type message:(PBGeneratedMessage *)message completionHandler:(HHNetworkCompletionHandler)completionHandler {
//
// HHSocketTask *task = [[HHSocketManager sharedManager] taskWithMessgeType:type message:message completionHandler:completionHandler];
// [self addTask:task];
//}
- (void)addTask:(id<HHNetworkTask>)task {
if ([task respondsToSelector:@selector(cancel)] &&
[task respondsToSelector:@selector(resume)] &&
![self.tasks containsObject:task]) {
[self.tasks addObject:task];
[(id)task addObserver:self forKeyPath:NSStringFromSelector(@selector(state)) options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
}
- (void)dispatchWithNotifHandler:(void (^)(void))notifHandler {
if (self.tasks.count == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
notifHandler ? notifHandler() : nil;
});
return;
}
self.lock = dispatch_semaphore_create(1);
self.keeper = self;
self.signal = self.tasks.count;
self.notifHandler = notifHandler;
for (id<HHNetworkTask> task in self.tasks.allObjects) {
[task resume];
}
}
- (void)cancel {
for (id<HHNetworkTask> task in self.tasks.allObjects) {
if ([(id)task state] < NSURLSessionTaskStateCanceling) {
[(id)task removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];
[task cancel];
}
}
[self.tasks removeAllObjects];
self.keeper = nil;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
NSURLSessionTaskState oldState = [change[NSKeyValueChangeOldKey] integerValue];
NSURLSessionTaskState newState = [change[NSKeyValueChangeNewKey] integerValue];
if (oldState != newState && newState >= NSURLSessionTaskStateCanceling) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
self.signal--;
dispatch_semaphore_signal(self.lock);
if (self.signal == 0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.notifHandler ? self.notifHandler() : nil;
[self.tasks removeAllObjects];
self.keeper = nil;
});
}
}
}
}
#pragma mark - Getter
- (NSMutableSet *)tasks {
if (!_tasks) {
_tasks = [NSMutableSet set];
}
return _tasks;
}
@end
看名字應(yīng)該就知道這個(gè)是和dispatch_group_notif差不多的東西, 不過(guò)是派發(fā)的對(duì)象不是dispatch_block_t而是id<HHNetworkTask>. 代碼很簡(jiǎn)單, 說(shuō)說(shuō)思路就行了.
keeper
系統(tǒng)大部分帶有Block的API都有一個(gè)特性就是只需要生成不需要持有, 也不用擔(dān)心Block持有我們的對(duì)象而造成循環(huán)引用, 例如:dispatch_async, dataTaskWithURL:completionHandler:等等, 其實(shí)具體的實(shí)現(xiàn)就是先循環(huán)引用再破除循環(huán)引用, 比如dispatch_async的queue和block會(huì)循環(huán)引用, 這樣在block執(zhí)行期間雙方都不會(huì)釋放, 然后等到block執(zhí)行完成后再將queue.block置nil破除循環(huán)引用, block沒(méi)了, 那它捕獲的queue和其他對(duì)象計(jì)數(shù)都能-1,也就都能正常釋放了.代碼里面的keeper就是來(lái)制造這個(gè)循環(huán)引用的.signal和tasks
signal其實(shí)就是tasks.count, 為什么我們不直接在task完成后直接tasks.remove然后判斷tasks.count == 0而是要間接給一個(gè)signal來(lái)做這事兒?
原因很簡(jiǎn)單: forin過(guò)程中是不能改變?nèi)萜鲗?duì)象的. 當(dāng)我們forin派發(fā)task的時(shí)候, task是異步執(zhí)行的, 有可能在task執(zhí)行完成觸發(fā)KVO的時(shí)候我們的forin還在遍歷, 此時(shí)直接remove就會(huì)crash. 如果不用forin, 而是用while或者for(;;)就會(huì)漏發(fā). 所以就聲明一個(gè)signal來(lái)做計(jì)數(shù)了. 另外addObserve和removeObserve必須成對(duì)出現(xiàn), 控制好就行.dispatch_after
在所有任務(wù)執(zhí)行完成后并沒(méi)有馬上執(zhí)行notif(), 而是等待0.1秒以后再執(zhí)行notif(), 這是因?yàn)閠ask.state的設(shè)置會(huì)在task.completionHandler之前執(zhí)行, 所以我們需要等一下, 確認(rèn)completionHandler執(zhí)行后在走我們的notif().如何使用
HHNetworkTaskGroup *group = [HHNetworkTaskGroup new];
HHTopicAPIManager *manager = [HHTopicAPIManager new];
for (int i = 1; i < 6; i++) {
NSURLSessionDataTask *task = [manager topicListDataTaskWithPage:i pageSize:20 completionHandler:^(NSError *error, id result) {
//...completionHandler... i
}];
[group addTask:(id)task];
}
[group dispatchWithNotifHandler:^{
//notifHandler
}];
強(qiáng)調(diào)一下, 絕對(duì)不應(yīng)該直接調(diào)用HHNetworkClient或者HHAPIManger的dataTaskxxx...這些通用接口來(lái)生成task, 應(yīng)該在該task所屬的API暴露接口生成task, 簡(jiǎn)單說(shuō)就是不要跨層訪問(wèn). 每個(gè)API的參數(shù)甚至簽名規(guī)則都是不一樣的, API的調(diào)用方應(yīng)該只提供生成task的相應(yīng)參數(shù)而不應(yīng)該也不需要知道這些參數(shù)具體的拼裝邏輯.
- HHNetworkAPIRecorder
@interface HHNetworkAPIRecorder : NSObject
@property (strong, nonatomic) id rawValue;
@property (assign, nonatomic) int pageSize;
@property (assign, nonatomic) int currentPage;
@property (assign, nonatomic) NSInteger itemsCount;
@property (assign, nonatomic) NSInteger lastRequestTime;
- (void)reset;
- (BOOL)hasMoreData;
- (NSInteger)maxPage;
@end
日常請(qǐng)求中有很多接口涉及到分頁(yè), 然而毫無(wú)疑問(wèn)分頁(yè)的邏輯在每個(gè)頁(yè)面都是一模一樣的, 但是卻需要每個(gè)調(diào)用頁(yè)面都保持一下currentPage然后調(diào)用邏輯都寫一次, 其實(shí)直接在API內(nèi)部實(shí)現(xiàn)一下分頁(yè)的邏輯, 然后對(duì)外暴露第一頁(yè)和下一頁(yè)的接口就不用聲明currentPage和重復(fù)這些無(wú)聊的邏輯了. 像這樣:
//XXXAPI.h
- (NSNumber *)refreshTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//第一頁(yè)
- (NSNumber *)loadmoreTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//當(dāng)前頁(yè)的下一頁(yè)
- (NSNumber *)fetchTopicListWithPage:(NSInteger)page completionHandler:(HHNetworkTaskCompletionHander)completionHandler;//指定頁(yè)(一般外部用不到, 看情況暴露)
//XXXAPI.m
- (NSNumber *)refreshTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler {
[self.topicListAPIRecorder reset];
return [self fetchTopicListWithPage:self.topicListAPIRecorder.currentPage completionHandler:completionHandler];
}
- (NSNumber *)loadmoreTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler {
self.topicListAPIRecorder.currentPage++;
return [self fetchTopicListWithPage:self.topicListAPIRecorder.currentPage completionHandler:completionHandler];
}
//SomeViewController
self.topicAPIManager = [HHTopicAPIManager new];
...
self.tableView.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{//下拉刷新
[weakSelf.topicAPIManager refreshTopicListWithCompletionHandler:^(NSError *error, id result) {
...
}];
}];
self.tableView.footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{//上拉加載
[weakSelf.topicAPIManager loadmoreTopicListWithCompletionHandler:^(NSError *error, id result) {
...
}];
}];
總結(jié)
HHURLRequestGenerator: 網(wǎng)絡(luò)請(qǐng)求的生成器, 公用的請(qǐng)求頭, cookie都在此設(shè)置.
HHNetworkClient: 網(wǎng)絡(luò)請(qǐng)求的派發(fā)器, 這里會(huì)記錄每一個(gè)服役中的請(qǐng)求, 并在必要的時(shí)候切換服務(wù)器.
HHAPIManager: 網(wǎng)絡(luò)請(qǐng)求派發(fā)器的調(diào)用者, 這里對(duì)請(qǐng)求的結(jié)果做相應(yīng)的數(shù)據(jù)格式化后返回給API調(diào)用方, 提供請(qǐng)求模塊的拓展性支持, 并提供合理的Task供TaskGroup派發(fā).