App架构升级之网络层优化(一)
一、背景
目前,公司里的App基本上采用了MVVM+ReactiveCocoa
的模式来开发。所依赖的网络层私有库是DDNetWork
。其内部是通过工程里的DDNetWorkManager
单例子来进行调用,并且4xx、5xx
类的错误直接在网络库内部弹出提示,导致视图层对这部分的错误信息不可控。并且每次在ViewModel
里涉及到网络请求的部分,都需要写如下的代码:
- (RACSignal *)signal_waitDoneOrders
{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
DDRequestInfo *request = [DDRequestInfo createDDStaffRequestTransporterWaitDoneOrderNum];
dispatch_async(dispatch_get_main_queue(), ^{
[[DDNetWorkManager defaultManger] sendGETRequest:request success:^(id response) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
if (response)
{
[subscriber sendNext:response];
[subscriber sendCompleted];
}
else
{
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:errorCode userInfo:userInfo];
[subscriber sendError:error];
}
});
} andFalilur:^(NSError *error, NSInteger errCode) {
[subscriber sendError:error];
}];
});
});
return [RACDisposable disposableWithBlock:^{
// 需要cancel网络请求。
}];
}];
return [[signal replayLazily] setNameWithFormat:@"-signalWaitDoneOrders:%@", @"red_point"];
}
- 一是网络请求比较多的模块,就会造成重复代码的急剧增加。
- 另外一个是每创建一个新的请求,工程师就要去写这么长的一段代码,心累。
- 重新封装一个网络库,费时费力,也没有必要。
于是,决定在原来网络库的基础上,参照octokit.objc
进行网络请求封装的优化,使其增加RAC的能力。
二、改造网络库
1、向外传递NSURLSessionDataTask。
这一步的目的是为了可以在发起网络请求的对象里面方便地取消请求。即将上面代码部分的:
return [RACDisposable disposableWithBlock:^{
// 需要cancel网络请求。
}];
修改为:
return [RACDisposable disposableWithBlock:^{
// 需要cancel网络请求。
if (task.state != NSURLSessionTaskStateCompleted) {
[task cancel];
}
}];
这样,外部就可以通过这个RACDisposable
对象来及时地取消对应的请求。
所以DDNetwork
的每一个消息由返回void
修改为返回NSURLSessionDataTask
,如GET
请求:
- (void)sendGETRequest:(DDRequestInfo *)request HTTPHeader:(NSDictionary *)headerDictionary success:(successHandler)success andFalilur:(failurHandler)failur;
修改为:
- (NSURLSessionDataTask *)sendGETRequest:(DDRequestInfo *)request HTTPHeader:(NSDictionary *)headerDictionary success:(successHandler)success andFalilur:(failurHandler)failur;
2、向外传递NSHTTPURLResponse
在请求结束、获取返回信息的时候。可通过NSHTTPURLResponse
的statusCode
来决定是否处理返回的数据。比如statusCode
为304
的时候,subscriber
可以直接调用sendCompleted
结束。
那么DDNetwork
消息的success
参数数据类型successHandler
:
typedef void (^successHandler)(id responseObject);
修改为:
typedef void (^successHandler)(NSHTTPURLResponse *response, id responseObject);
则GET
请求的返回:
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
if (success) {
success(responseObject);
}
}];
修改为:
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
if (success) {
success((NSHTTPURLResponse *)task.response,responseObject);
}
}];
3、向外传递 NSError
第一步、将错误分域。
因为API返回的错误code有可能也是4xx、5xx
等,但是所表示的错误信息和服务器的4xx、5xx
等是不一样的。为了区别对待。将所有API返回的错误划分为:DDAPIErrorDomain
。其他情况产生的域保持不变。
DDNetWorkModel.h
extern NSString * const DDAPIErrorDomain;
DDNetWorkModel.m
NSString * const DDAPIErrorDomain = @"DDAPIErrorDomain";
然后在构造NSError的时候:
- (NSError *)errorFromServerResponse:(id)responseObject
{
NSString *errMessage = stringFromObject(responseObject, @"errorMsg");
NSInteger errorCode = [stringFromObject(responseObject, @"errorCode") integerValue];
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:errMessage};
NSError *error = [NSError errorWithDomain:DDAPIErrorDomain code:errorCode userInfo:userInfo];
return error;
}
第二步、改造网络请求的failure:^(NSURLSessionDataTask *task, NSError *error) {}
__weak typeof(self) wself = self;
NSURLSessionDataTask *task = [manager POST:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// 处理请求成功。
} failure:^(NSURLSessionDataTask *task, NSError *error) {
__strong typeof(wself) sself = wself;
[sself changeTerminateRequestModelWhenErrorWihtAddress:request.actionAddress andError:error];
if ([sself isShowErrorProcessInfoWithAddress:request.actionAddress] == YES)
{
NSString *errorMsg = [sself cuteMessageWithErrorCode:[sself errorCodeWithError:error]];
NSString *errMessage = errorMsg.length > 0 ? errorMsg : (error.localizedFailureReason.length > 0 ? error.localizedFailureReason:@"");
// 直接toas展示errMessage
}
else
{
if (failur) {
failur(nil, error.code);
}
}
}];
修改为:
__weak typeof(self) wself = self;
NSURLSessionDataTask *task = [manager POST:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// 处理请求成功。
} failure:^(NSURLSessionDataTask *task, NSError *error) {
__strong typeof(wself) sself = wself;
[sself changeTerminateRequestModelWhenErrorWihtAddress:request.actionAddress andError:error];
if ([sself isShowErrorProcessInfoWithAddress:request.actionAddress] == YES)
{
NSString *errorMsg = [sself cuteMessageWithErrorCode:[sself errorCodeWithError:error]];
NSString *errMessage = errorMsg.length > 0 ? errorMsg : (error.localizedFailureReason.length > 0 ? error.localizedFailureReason:@"");
// 向外传递到外部视图展示。
NSError *tError = [NSError errorWithDomain:error.domain code:error.code userInfo:@{NSLocalizedFailureReasonErrorKey:errMessage}];
if (failur) {
failur(tError, tError.code);
}
}
else
{
if (failur) {
failur(nil, error.code);
}
}
}];
第三步、网络不可达,及时返回
如果网络本身不可达。那么需要在请求发出去之前就返回错误,通知上层发生网络连接错误。
首先、在DDNetWorkModel
初始化的时候,开启网络状态的嗅探。
- (instancetype)init
{
if (self = [super init])
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.httpSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
// 开始网络状态嗅探
[self.httpSessionManager.reachabilityManager startMonitoring];
// 其他处理
}
return self;
}
其次、在每个网络请求的第一步,判断网络是否可达。
- (NSURLSessionDataTask *)sendGETRequest:(DDRequestInfo *)request
HTTPHeader:(NSDictionary *)headerDictionary
success:(successHandler)success
andFalilur:(failurHandler)failur
{
// 先判断网络是否ok。
if (self.httpSessionManager.reachabilityManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable)
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"无法连接到网络", @"")];
// 构造连接失败的error。
NSError *error = [self connectErrorWithFailureReason:failureReason];
if (failur) {
failur(error, error.code);
}
return nil;
}
// 创建 AFHTTPSessionManager 对象,以及发起 GET 请求。
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// xxxx
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// xxxx
}];
return task;
}
因为这一步是客户端主动判断网络是否可达。定义其错误域为:DDClientErrorDomain
,错误码为:668
。
DDNetWorkModel.h
extern NSString * const DDClientErrorDomain;
DDNetWorkModel.m
NSString * const DDClientErrorDomain = @"DDClientErrorDomain";
经过以上的修改,成功地将网络库里的NSURLSessionDataTask、NSHTTPURLResponse以及错误信息传递到了外部。并且网络库只提供网络请求相关的功能,不再提供错误信息的toast提示。
三、DDNetWorkManager增加RAC扩展
@interface DDNetWorkManger (RAC)
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method resultClass:(Class)resultClass;
@end
这里只提供一个对外的接口。将请求加入队列中。
其中DDHTTPMethod
目前支持三种请求类型:
typedef NS_ENUM(NSUInteger, DDHTTPMethod) {
DDHTTPMethodGET,
DDHTTPMethodPOST,
DDHTTPMethodPUT
};
resultClass
参数用来支持将返回的json数据直接转换为指定的数据模型(resultClass)。
目前,只提供基础的result数据模型DDAPIResult
:
@interface DDAPIResult : DDObject
@property (nonatomic, copy) NSString *status;
// 请求结果
@property (nonatomic, copy) id content;
@property (nonatomic, copy) NSString *errorCode;
@property (nonatomic, copy) NSString *errorMsg;
// 用于检查JSON是否合法
- (id)jsonValidator;
@end
该接口的具体实现为:
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method resultClass:(Class)resultClass
{
@weakify(self);
// 封装请求
return [[[self enqueueRequest:request method:method] reduceEach:^id _Nullable(NSHTTPURLResponse *response, id responseObject){
@strongify(self);
// 解析请求结果
return [[[self parsedResponseOfClass:resultClass fromJSON:responseObject] map:^id(id parsedResult) {
// 将结果封装成DDResponse返回
DDResponse *parsedResponse = [[DDResponse alloc] initWithHTTPURLResponse:response parsedResult:parsedResult];
NSAssert(parsedResponse != nil, @"Could not create DDResponse with response %@ and parsedResult %@", response, parsedResult);
return parsedResponse;
}] doNext:^(DDResponse *parsedResponse) {
DDLOG(@"%@ => %li ", response.URL, (long)response.statusCode,);
}];
}] concat];
}
其中DDResponse
为:
@interface DDResponse : NSObject
@property (nonatomic, strong, readonly) id parsedResult;
@property (nonatomic, assign, readonly) NSInteger statusCode;
- (instancetype)initWithHTTPURLResponse:(NSHTTPURLResponse *)response parsedResult:(id)parsedResult;
@end
封装请求的实现如下:
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method
{
@weakify(self);
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
NSURLSessionDataTask *task = nil;
if (DDHTTPMethodGET == method)
{
task = [self sendGETRequest:request success:^(NSHTTPURLResponse *response, id responseObject) {
if (response.statusCode == DDAPINotModifiedStatusCode)
{
[subscriber sendCompleted];
return ;
}
[[RACSignal return:RACTuplePack(response, responseObject)] subscribe:subscriber];
} andFalilur:^(NSError *error, NSInteger errCode) {
// 将网络层的错误向外传递
[subscriber sendError:error];
}];
}
else if (DDHTTPMethodPOST == method)
{
task = [self sendPOSTRequest:request success:^(NSHTTPURLResponse *response, id responseObject) {
if (response.statusCode == DDAPINotModifiedStatusCode)
{
[subscriber sendCompleted];
return ;
}
RACSignal *nextPageSignal = [RACSignal empty];
[[RACSignal return:RACTuplePack(response, responseObject)] subscribe:subscriber];
} andFalilur:^(NSError *error, NSInteger errCode) {
[subscriber sendError:error];
}];
}
return [RACDisposable disposableWithBlock:^{
// 需要cancel网络请求。
if (task.state != NSURLSessionTaskStateCompleted) {
[task cancel];
}
}];
}];
return [[signal replayLazily] setNameWithFormat:@"-enqueueRequest:%@", request];
}
这里,第一、实现了将网络层的错误向外传递到视图层。第二、实现了支持cancel网络请求的功能。
解析请求结果的实现:
- (RACSignal *)parsedResponseOfClass:(Class)resultClass fromJSON:(id)responseObject
{
@weakify(self);
return [RACSignal createSignal:^ id (id<RACSubscriber> subscriber) {
@strongify(self);
void (^parseJSONDictionary)(NSDictionary *) = ^(NSDictionary *JSONDictionary) {
@strongify(self);
if (resultClass == nil)
{
DDAPIResult *parsedObject = [[DDAPIResult class] mj_objectWithKeyValues:JSONDictionary];
// 检查返回值的合法性
BOOL success = [self checkResult:JSONDictionary response:parsedObject];
if (success)
{
NSAssert([parsedObject isKindOfClass:DDObject.class], @"Parsed model object is not an DDObject: %@", parsedObject);
// 检查API返回的错误信息
if (parsedObject.errorCode > 0 || ![parsedObject.status isEqualToString:DDAPIResultStatusOK])
{
[subscriber sendError:[self errorFromServerResponse:JSONDictionary]];
return;
}
[subscriber sendNext:JSONDictionary];
}
else
{
NSString *failureReason = parsedObject.errorMsg.length > 0 ?parsedObject.errorMsg:[NSString stringWithFormat:NSLocalizedString(@"服务器返回的数据格式错误! (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
}
return;
}
DDAPIResult *parsedObject = [resultClass mj_objectWithKeyValues:JSONDictionary];
// 检查返回值的合法性
BOOL success = [self checkResult:JSONDictionary response:parsedObject];
if (success)
{
NSAssert([parsedObject isKindOfClass:DDObject.class], @"Parsed model object is not an DDObject: %@", parsedObject);
// 检查API返回的错误信息
if (parsedObject.errorCode > 0 || ![parsedObject.status isEqualToString:DDAPIResultStatusOK])
{
[subscriber sendError:[self errorFromServerResponse:JSONDictionary]];
return;
}
[subscriber sendNext:parsedObject];
}
else
{
NSString *failureReason = parsedObject.errorMsg.length > 0 ?parsedObject.errorMsg:[NSString stringWithFormat:NSLocalizedString(@"服务器返回的数据格式错误! (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
return;
}
};
if ([responseObject isKindOfClass:NSArray.class])
{
for (NSDictionary *JSONDictionary in responseObject)
{
if (![JSONDictionary isKindOfClass:NSDictionary.class])
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Invalid JSON array element: %@", @""), JSONDictionary];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
return nil;
}
parseJSONDictionary(JSONDictionary);
}
[subscriber sendCompleted];
}
else if ([responseObject isKindOfClass:NSDictionary.class])
{
parseJSONDictionary(responseObject);
[subscriber sendCompleted];
}
else if (responseObject == nil)
{
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
else
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Response wasn't an array or dictionary (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
}
return nil;
}];
}
其中,DDObject为DDAPIResult的父类。json自动转model需要作传入数据模型类型合法性检查。
以上、网络请求具备了RAC的能力。下一步就是在发起网络请求的ViewModel中订阅这些信号即可。