iOS-HTTP網(wǎng)絡(luò)框架

前言

一.網(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ā).

轉(zhuǎn)載 http://m.itdecent.cn/u/14431e509ae8

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容