一步一步构建你的iOS网络层 - HTTP篇

从简书迁移到掘金...git

前言

本文参考casa先生的网络层架构设计从网络请求的构建到请求结果的处理为你概述如何构建一个方便易用的iOS网络层, 全文约五千字, 预计花费阅读时间20 - 30分钟.程序员

目录

  • 网络请求的构建github

  • 网络请求的派发数组

    1. 请求的派发与取消
    2. 多服务器的切换
  • 合理的使用请求派发器缓存

    1. 协议仍是配置对象?
    2. 简单的请求结果缓存器
    3. 请求结果的格式化
    4. 两个小玩意儿

一.网络请求的构建

网络请求的构建很简单, 根据一个请求须要的条件如URL, 请求方式, 请求参数, 请求头等定义请求生成的接口便可. 定义以下:安全

@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
复制代码

能够看到方法参数都是生成请求基本组成部分, 固然, 这里的参数比较少, 由于在个人项目中像请求超时时间都是同样的, 相似这些公用的设置我都偷懒直接写在请求配置文件里面了. 咱们看看请求接口的具体实现, 以数据请求为例:bash

- (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];//设置cookie
    [self setCommonRequestHeaderForRequest:request];// 在这里作公用请求头的设置
    [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];
    }
}
复制代码

代码很简单, 接口根据参数调用urlStringWithPath:useHttps:经过BaseURL和URLPath拼装出完整的URL, 而后用这个URL和其余参数生成一个URLRequest, 而后调用setCommonRequestHeaderForRequest:设置公用请求, 最后返回这个URLRequest.服务器

BaseURL来自HHService, HHService对外暴露各个环境(测试/开发/发布)下的baseURL和切换服务器的接口, 内部走工厂生成当前的服务器, 个人设置是默认链接第一个服务器且APP关闭后恢复此设置, APP运行中可根据须要调用switchService切换服务器. HHService定义以下:cookie

@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.网络请求的派发

请求的派发是经过一个单例HHNetworkClient来实现的, 若是把请求比做炮弹的话, 那么这个单例就是发射炮弹的炮台, 使用炮台的人只须要告诉炮台须要发射什么样的炮弹和炮弹的打击目标即可发射了. 另外, 应该提供取消打击的功能以处理没必要要的打击的状况, 那么, 根据炮台的做用. 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.请求的派发与取消

外部暴露数据请求和文件上传的接口, 参数为构建请求所需的必要参数, 返回值为这次请求任务的taskIdentifier, 调用方能够经过taskIdentifier取消正在执行的请求任务. 内部声明一个dispathTable保持着此时正在执行的任务, 并在任务执行完成或者任务取消时移除任务的引用, 以数据请求为例, 具体实现以下:

- (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);
}
复制代码

代码很简单, 经过参数生成URLRequest, 而后经过AFHTTPSessionManager执行任务, 在任务执行前咱们以task.taskIdentifier为key保持一下执行的任务, 而后在任务执行后咱们移除这个任务, 固然, 外部也能够在必要的时候经过咱们返回的task.taskIdentifier手动移除任务.

注意咱们先声明一个NSMutableArray来标志taskIdentifier, 而后在任务生成后设置taskIdentifier[0]为task. taskIdentifier, 最后在任务完成的回调block中使用taskIdentifier[0]来移除这个已经完成的任务. 可能有人会有疑问为何不直接使用task.taskIdentifier, block不是能够捕获task吗? 下面解释一下为何这样写:

咱们知道block之于函数最大的区别就在于它能够捕获自身做用域外的对象, 并在block执行的时候访问被捕获的对象, 具体的, 对于值类型对象block会生成一份此对象的拷贝, 对于引用类型对象block会生成一个此对象的引用并使该对象的引用计数+1(这里咱们只描述非__block修饰的状况). 那么代入到上面的代码, 咱们来一步一步分析:

  • 直接捕获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)];
复制代码

咱们把它拆开来看:

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是咱们实际存储的任务, 而task只是一个临时变量, 此时task指向nil, 那咱们生成returnTask的block此时捕获到的task也就是nil, 因此在任务完成的时候咱们的task.taskIdentifier必定是0, 这样写的结果就是dispathTable只会添加不会删除(系统的taskIdentifier是从0开始依次递增的), 固然, 由于进行中的returnTask咱们是作了存储的, 因此在任务未完成的时候咱们仍是能够作取消的.

  • 若是一开始给task一个占位对象呢不让它为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)];
复制代码

这样其实就是一个简单的引用变换题了, 咱们来看看各个指针的指向状况:

suspend: pTask->NSObject block.pTask->nil pReturnTask->nil

alloc: pTask-> NSObject block.pTask->NSObject pReturnTask->returnTask

completed: pTask->returnTask block.pTask->NSObject pReturnTask->returnTask

能够看到在任务执行完成时咱们访问block.pTask时也不过是咱们一开始的占位对象, 因此这个方案也不行, 固然, 取消任务依然可用

事实上block.pTask确实是捕获了占位对象, 只是咱们在那以后没有替换block.pTask指向到returnTask, 然而block.pTask咱们是访问不了的, 因此这个方案行不通.

  • 若是咱们的占位对象是一个容器呢?
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)];
复制代码

既然咱们访问不了block.pTask那就访问block.pTask指向的对象嘛, 更改这个对象的内容不就至关于更改了block.pTask么, 你们照着2的思路走一下应该很容易就能想通, 我就很少说了.

2.多服务器的切换

关于多服务器其实我也没有实际的经验, 公司正在部署第二台服务器, 具体需求是若是访问第一台服务器老是超时或者出错, 那就切换到第二台服务器, 基于此需求我简单的实现一下:

- (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]];
}
复制代码

假设认为APP在这次使用过程当中网络任务的错误率达到10%那就应该切换一下服务器, 咱们在任务派发前将任务总数+1, 而后在任务结束后判断任务是否成功, 失败的话将任务失败总数+1再判断是否到达最大错误率, 进而切换到另外一台服务器. 另外还有一种状况是大部分服务器都挂了, 后台直接走APNS推送可用的服务器序号过来, 就不用挨个挨个切换了.

三.合理的使用请求派发器

OK, 炮弹有了, 炮台也就绪了, 接下来看看如何使用这个炮台.

#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对外提供数据请求和取消的接口, 内部调用HHNetworkClient进行实际的请求操做.

1.协议仍是配置对象?

HHAPIManager的接口咱们并无像以前同样提供多个参数, 而是将多个参数组合为一个配置对象, 下面说一下为何这样作:

  • 为何多个参数的接口方式很差?

一个APP中调用的API一般都是数以百计甚至千计, 若是有一天须要对已成型的全部的API都追加一个参数, 此时的改动之多, 足使男程序员沉默, 女程序员流泪. 举个例子: APP1.0已经上线, 1.1版本总监忽然要求对数据请求加上缓存, 操做请求不用加缓存, 若是是参数接口的形式通常就是这样写:

//老接口
- (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;
复制代码

而后原来的老接口全都调用新接口shouldCache默认传NO, 不须要缓存的API不用作改动, 而须要缓存的API都得改调用新接口而后shouldCache传YES.

这样能暂时解决问题, 工做量也会小一些, 而后过了两天总监过来讲, 为何没有对API区分缓存时间? 还有, 咱们又有新需求了. 呵呵!

  • 使用协议提高拓展性
@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
复制代码

其实最初的设计是走协议的, HHAPIManager遵照这个协议, 内部给上默认参数, dispatchTaskWithCompletionHandler:会去挨个获取这些参数, 各个子类自行实现本身自定义的部分, 这样之后就算有任何拓展, 只须要在协议里面加个方法基类给上默认值, 有须要的子类API重写一下就好了.

  • 替换协议为配置对象
- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchUploadTaskWithConfiguration:(HHUploadAPIConfiguration *)config progressHandler:(HHNetworkTaskProgressHandler)progressHandler completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
复制代码

协议的方案其实很好, 也是我想要的设计. 可是协议是针对类而言的, 这意味着从此的每添加一个API就须要新建一个HHAPIManager的子类, 很容易就有了几百个API类文件, 维护起来很麻烦, 找起来很麻烦(以上是同事要求替换协议的理由, 我仍然支持协议, 可是他们人多). 因此将协议替换为配置对象, 而后API以模块功能划分, 每一个模块一个类文件给出多个API接口 ,内部每一个API搭上合适的配置对象, 这样一来只须要十几个类文件.

总之, 考虑到配置对象既能够实现单个API单个类的设计, 也能够知足同事的需求, 协议被换成了配置对象. 另外, 全部的block参数都不写在配置对象里, 而是直接在接口处声明, 看着别扭写着方便(block作参数和作属性哪一个写起来简单你们都懂的).

2.简单的请求结果缓存器

上面简单提到了请求缓存, 其实咱们是没有作缓存的, 由于我司HTTP的API如今基本上都被废弃了, 全是走TCP, 然而TCP的缓存又是另外一个故事了.可是仍是简单实现一下吧:

#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;
复制代码

简单定义一个HHCache对象, 存放缓存数据, 缓存时间, 缓存时效, 而后HHNetworkCacheManager单例对象内部用NSCache存储缓存对象, 由于NSCache自带线程安全特效, 连锁都不用.

在任务发起以前咱们检查一下是否有可用缓存, 有可用缓存直接返回, 没有就走网络, 网络任务成功后存一下请求数据便可.

3.请求结果的格式化

网络任务完成后带回的数据以什么样的形式返回给调用方, 分两种状况: 任务成功和任务失败.这里咱们定义一下任务成功和失败, 成功表示网络请求成功且带回了可用数据, 失败表示未获取到可用数据. 举个例子: 获取一个话题列表, 用户但愿看到的看到是一排排彩色头像, 若是你调用API拿不到这一堆数据那对于用户来讲就是失败的. 那么没拿到数据多是网络出错了, 或者网络没有问题只是用户没有关注过任何话题, 那么相应的展现网络错误提示或者推荐话题提示.

任务成功的话很简单, 直接作相应JSON解析正常返回就行, 若是某个XXXAPI有特殊需求那就新加一个XXXAPIConfig继承APIConfig基类, 在里面添加属性或者方法描述一下你有什么特殊需求, XXXAPI负责格式好返回就好了(因此仍是一个API一个类好, 干净).

任务失败的话就麻烦一点, 我但愿任何API都能友好的返回错误提示, 具体的, 若是有错误发生了, 那么返回给调用方的error.code必定是可读的枚举而不是301之类的须要比对文档的错误码(必须), error.domain一般就是错误提示语(可选), 这就要求程序员写每一个API时都定义好错误枚举(因此仍是一个API一个类好, 干净)和相应的错误提示.大概是这样子:

//HHNetworkTaskError.h 通用错误
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 = @"这里什么也没有~";
static NSString *HHNetworkErrorNotice = @"当前网络差, 请检查网络设置~";
static NSString *HHTimeoutErrorNotice = @"请求超时了~";
static NSString *HHDefaultErrorNotice = @"请求失败了~";
static NSString *HHNoMoreDataErrorNotice = @"没有更多了~";
复制代码
- (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: {//应产品要求, 全部连不上服务器都是用户网络的问题
                error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
            }   break;
                
            default: {
                error = HHError(HHNoDataErrorNotice, HHNetworkTaskErrorDefault);
            }   break;
        }
    }
    return error;
}
复制代码

通用的错误枚举和提示语定义在一个.h中, 之后有新增通用描述都在这里添加, 便于管理. HHAPIManager基类会先格式好某些通用错误, 而后各个子类定义本身特有的错误枚举(不可和通用描述冲突)和错误描述, 像这样:

//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) {//通用错误基类已经处理好, 作好本身的数据格式就行
            
            switch ([result[@"code"] integerValue]) {
                case 200: {
                    //                    请求数据无误作相应解析
                    //                    result = [HHUser objectWithKeyValues:result[@"data"]];
                }   break;
                    
                case 301: {
                    error = HHError(@"用户不存在", HHUserInfoTaskErrorNotExistUserId);
                }  break;
                    
                case 302: {
                    error = HHError(@"xxx错误", HHUserInfoTaskError1);
                }   break;
                    
                case 303: {
                    error = HHError(@"yyy错误", HHUserInfoTaskError2);
                }   break;
                default:break;
            }
        }
        completionHandler ? completionHandler(error, result) : nil;
    }];
}

复制代码

而后调用方通常状况下只须要这样:

[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
       error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
    }];
复制代码

固然, 状况复杂的话只能这样, 代码多一点, 可是有枚举读起来也不麻烦:

[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
        error ? [self showErrorViewWithError:error] : [self reloadTableViewWithNames:result];
    }];

- (void)showErrorViewWithError:(NSError *)error {

    switch (error.code) {//若是状况复杂就本身switch
                case HHNetworkTaskErrorTimeOut: {
                    //                    展现请求超时错误页面
                }   break;
                case HHNetworkTaskErrorCannotConnectedToInternet: {
                    //                    展现网络错误页面
                }
                case HHUserInfoTaskErrorNotExistUserId: {
                    //                    ...
                }
                    //                    ...
                default:break;
            }
}
复制代码

这里多扯两句, 请求的回调我是以(error, id)的形式返回的, 而不是像AFN那样分别给出successBlock和failBlock. 其实我自己是很支持AFN的作法的, 区分红功和错误强行让两种业务的代码出如今两个不一样的部分, 这很好, 不一样的业务处理就该在不一样函数/方法里面.

可是实际开发中有不少成功和失败都会执行的操做, 典型的例子就是HUD, 两个block的话我须要在两个地方都加上[HUD hide], 这样的代码写的多了就会很烦, 而我又懒, 因此就成功失败都在一个回调返回了.

可是! 你也应该区分不一样的业务写出两个不一样方法(像上面那样作), 至于公用的部分就只写一次就够了.像这样:

[hud show:YES];
[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
      [hud hide:YES];
       error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
    }];
复制代码

再说一句, 即便你比我还懒, 不声明两个方法那也应该将较短的逻辑写在前面, 较长的写在后面, 易读, 像这样:

if (!error) {
            ...短
            ...短
        } else {
            
            switch (error.code) {//若是状况复杂就本身switch
                case HHNetworkTaskErrorTimeOut: {
                    //                    展现请求超时错误页面
                }   break;
                case HHNetworkTaskErrorCannotConnectedToInternet: {
                    //                    展现网络错误页面
                }
                case HHUserInfoTaskErrorNotExistUserId: {
                    //                    ...长
                }
                    //                    ...长
                default:break;
            }
        }
    }
复制代码

4.两个小玩意儿

文章到这基本上这个网络层该说的都说的差很少了, 各位能够根据本身的需求改动改动就能用了, 最后简单介绍下两个和它相关的小玩意儿就结尾吧:

  • 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
复制代码

看名字应该就知道这个是和dispatch_group_notif差很少的东西, 不过是派发的对象不是dispatch_block_t而是id. 代码很简单, 说说思路就好了.

  • keeper 系统大部分带有Block的API都有一个特性就是只须要生成不须要持有, 也不用担忧Block持有咱们的对象而形成循环引用, 例如:dispatch_async, dataTaskWithURL:completionHandler:等等, 其实具体的实现就是先循环引用再破除循环引用, 好比dispatch_async的queue和block会循环引用, 这样在block执行期间双方都不会释放, 而后等到block执行完成后再将queue.block置nil破除循环引用, block没了, 那它捕获的queue和其余对象计数都能-1,也就都能正常释放了.代码里面的keeper就是来制造这个循环引用的.

  • signal和tasks signal其实就是tasks.count, 为何咱们不直接在task完成后直接tasks.remove而后判断tasks.count == 0而是要间接给一个signal来作这事儿? 缘由很简单: forin过程当中是不能改变容器对象的. 当咱们forin派发task的时候, task是异步执行的, 有可能在task执行完成触发KVO的时候咱们的forin还在遍历, 此时直接remove就会crash. 若是不用forin, 而是用while或者for(;;)就会漏发. 因此就声明一个signal来作计数了. 另外addObserve和removeObserve必须成对出现, 控制好就行.

  • dispatch_after 在全部任务执行完成后并无立刻执行notif(), 而是等待0.1秒之后再执行notif(), 这是由于task.state的设置会在task.completionHandler以前执行, 因此咱们须要等一下, 确认completionHandler执行后在走咱们的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
    }];
复制代码

强调一下, 绝对不该该直接调用HHNetworkClient或者HHAPIManger的dataTaskxxx...这些通用接口来生成task, 应该在该task所属的API暴露接口生成task, 简单说就是不要跨层访问. 每一个API的参数甚至签名规则都是不同的, API的调用方应该只提供生成task的相应参数而不该该也不须要知道这些参数具体的拼装逻辑.

  • 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
复制代码

平常请求中有不少接口涉及到分页, 然而毫无疑问分页的逻辑在每一个页面都是如出一辙的, 可是却须要每一个调用页面都保持一下currentPage而后调用逻辑都写一次, 其实直接在API内部实现一下分页的逻辑, 而后对外暴露第一页和下一页的接口就不用声明currentPage和重复这些无聊的逻辑了. 像这样:

//XXXAPI.h
- (NSNumber *)refreshTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//第一页
- (NSNumber *)loadmoreTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//当前页的下一页
- (NSNumber *)fetchTopicListWithPage:(NSInteger)page completionHandler:(HHNetworkTaskCompletionHander)completionHandler;//指定页(通常外部用不到, 看状况暴露)
复制代码
//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) {
                ...
        }];
    }];
复制代码

总结

HHURLRequestGenerator: 网络请求的生成器, 公用的请求头, cookie都在此设置.

HHNetworkClient: 网络请求的派发器, 这里会记录每个服役中的请求, 并在必要的时候切换服务器.

HHAPIManager: 网络请求派发器的调用者, 这里对请求的结果作相应的数据格式化后返回给API调用方, 提供请求模块的拓展性支持, 并提供合理的Task供TaskGroup派发.

本文附带的demo地址

一步一步构建你的iOS网络层 - TCP篇

相关文章
相关标签/搜索