背景
在 2013 的 WWDC 上,苹果推出了 NSURLSession,作为新的网络基础架构,代替 NSURLConnection。而在开发者中广受欢迎的第三方网络框架 ASIHTTPRequest,也于 2012年10月宣布停止更新维护。目前最受欢迎的第三方网络框架 AFNetworking,随着苹果官方网络层基础架构的重构,也在 2015年12月 升级到了 3.x 版本,将其底层从 NSURLConnection 迁移到 NSURLSession。
作为开发者,对网络层进行封装非常必要,一方面可使得项目的代码更好维护、更稳健;另一方面,当网络底层需要进行更换时,可将工作量降到最小,最大程度保证项目的稳定性。
简介
YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 iOS 网络库,从 2.x 开始,YTKNetwork 抛弃了旧有的以 AFHTTPRequestOperation 为核心的 API,采用新的基于 NSURLSession 的 API。
安装
可在 Podfile 中加入下面一行代码来使用 YTKNetwork。
$ pod ‘YTKNetwork'
功能
相比 AFNetworking,YTKNetwork 提供了以下更高级的功能:
- 支持按时间缓存网络请求内容
- 支持按版本号缓存网络请求内容
- 支持统一设置服务器和 CDN 的地址
- 支持检查返回 JSON 内容的合法性
- 支持文件的断点续传
- 支持 block 和 delegate 两种模式的回调方式
- 支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
- 支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B 和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
- 支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。
基本思想
YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。
把每一个网络请求封装成对象其实是使用了设计模式中的 Command 模式,它有以下好处:
- 将网络请求与具体的第三方库依赖隔离,方便以后更换底层的网络库
- 方便在基类中处理公共逻辑
- 方便在基类中处理缓存逻辑,以及其它一些公共逻辑。
- 方便做对象的持久化。
Command 模式
苹果的官方文档对 Command 模式的描述如下:
The Command design pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. The request object binds together one or more actions on a specific receiver. The Command pattern separates an object making a request from the objects that receive and execute that request.
翻译如下:
命令模式把一个请求封装为一个对象,因此你可以使用不同的请求来参数化你的客户、对请求进行排队或记录请求的日志,以及支持可撤销操作。请求对象把一个或多个操作绑定到一个特定的接收者上。命令模式把创建请求的对象,和接收、执行请求的对象分隔开来。
对应 YTKNetwork,Command 模式的结构如图1所示:
基础使用
由于官方已有 YTKNetwork 的基础使用教程和高级使用教程,此处只作一个简单的阐述,用以引出下文。
首先,在程序刚启动的回调中,使用 YTKNetworkConfig 类,设置好网络请求的服务器地址,如下所示:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
config.baseUrl = @“https://love.163.com";
}
设置好之后,所有的网络请求都会默认使用 YTKNetworkConfig 中 baseUrl 参数指定的地址。当我们需要切换服务器地址时,只需要修改 YTKNetworkConfig 中的 baseUrl 参数即可。
每一种请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。假设我们要向网址 https://love.163.com/login 发送一个 POST 请求,请求参数是 username 和 password。那么,这个类应该如下所示:
// LoginApi.h
#import "YTKRequest.h"
@interface LoginApi : YTKRequest
- (id)initWithUsername:(NSString *)username password:(NSString *)password;
@end
// LoginApi.m
#import "LoginApi.h"
@implementation LoginApi {
NSString *_username;
NSString *_password;
}
- (id)initWithUsername:(NSString *)username password:(NSString *)password {
self = [super init];
if (self) {
_username = username;
_password = password;
}
return self;
}
- (NSString *)requestUrl {
// “ https://love.163.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息
return @"/login";
}
- (YTKRequestMethod)requestMethod {
return YTKRequestMethodPOST;
}
- (id)requestArgument {
return @{
@"username": _username,
@"password": _password
};
}
@end
在上面这个示例中,我们可以看到:
- 通过覆盖 YTKRequest 类的 requestUrl 方法,指定了网址信息。
- 通过覆盖 YTKRequest 类的 requestMethod 方法,指定 POST 方法来传递参数。
- 通过覆盖 YTKRequest 类的 requestArgument 方法,提供了 POST 的信息。
在构造完成 LoginApi 之后,我们可以在登录的 ViewController 中,调用 LoginApi,并用 block 或 delegate 的方式来取得网络请求结果:
- (void)loginButtonPressed:(id)sender {
NSString *username = self.UserNameTextField.text;
NSString *password = self.PasswordTextField.text;
if (username.length > 0 && password.length > 0) {
LoginApi *api = [[LoginApi alloc] initWithUsername:username password:password];
api.delegate = self;
[api start];
/* 也可使用如下方法代替 start,此时则无需设置 api 的 delegate。
*[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
NSLog(@"succeed");
} failure:^(YTKBaseRequest *request) {
NSLog(@"failed");
}];
*/
}
}
- (void)requestFinished:(YTKBaseRequest *)request {
NSLog(@"succeed");
}
- (void)requestFailed:(YTKBaseRequest *)request {
NSLog(@"failed");
}
至此,一个简单的登录请求操作就完成了。
源码解析
基于上一节的内容,我们来看看 YTKNetworking 在方法 start、startWithCompletionBlockWithSuccess 背后到底做了什么。
// YTKBaseRequest.m
- (void)start {
[self toggleAccessoriesWillStartCallBack];
[[YTKNetworkAgent sharedAgent] addRequest:self];
}
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success failure:(YTKRequestCompletionBlock)failure {
// 设置了成功与失败的两个回调的 block
[self setCompletionBlockWithSuccess:success failure:failure];
[self start];
}
方法 toggleAccessoriesWillStartCallBack 代码如下,YTKNetworkPrivate 是 YTKNetworking 的一个私有类,其中包括了对 YTKBaseRequest 的扩展。
@protocol YTKRequestAccessory <NSObject>
@optional
- (void)requestWillStart:(id)request;
- (void)requestWillStop:(id)request;
- (void)requestDidStop:(id)request;
@end
// YTKNetworkPrivate.m
@implementation YTKBatchRequest (RequestAccessory)
- (void)toggleAccessoriesWillStartCallBack {
for (id<YTKRequestAccessory> accessory in self.requestAccessories) {
if ([accessory respondsToSelector:@selector(requestWillStart:)]) {
[accessory requestWillStart:self];
}
}
}
- (void)toggleAccessoriesWillStopCallBack {
for (id<YTKRequestAccessory> accessory in self.requestAccessories) {
if ([accessory respondsToSelector:@selector(requestWillStop:)]) {
[accessory requestWillStop:self];
}
}
}
- (void)toggleAccessoriesDidStopCallBack {
for (id<YTKRequestAccessory> accessory in self.requestAccessories) {
if ([accessory respondsToSelector:@selector(requestDidStop:)]) {
[accessory requestDidStop:self];
}
}
}
@end
requestAccessories 是 YTKBaseRequest 类中,遵循协议 YTKRequestAccessory 的一个数组属性。方法 toggleAccessoriesWillStartCallBack 使得数组 requestAccessories 中的回调函数 requestWillStart 被调用,这里用于在请求开始前、即将结束时、请求结束后进行某些操作,只要执行操作的对象遵循 YTKRequestAccessory,并放入继承于 YTKBaseRequest 类的实例对象的数组 requestAccessories 中。
我们再看看[[YTKNetworkAgent sharedAgent] addRequest:self]
到底做了什么。代码如下:
- (void)addRequest:(YTKBaseRequest *)request
{
NSParameterAssert(request != nil);
// 生成 NSURLSessionTask 的实例对象,并赋给 request 的属性 requestTask
NSError * __autoreleasing requestSerializationError = nil;
NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
if (customUrlRequest) {
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
[self handleRequestResult:dataTask responseObject:responseObject error:error];
}];
request.requestTask = dataTask;
} else {
request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
}
// 如果生成 NSURLSessionTask 实例对象失败,执行 requestDidFailWithRequest:error: 方法
if (requestSerializationError) {
[self requestDidFailWithRequest:request error:requestSerializationError];
return;
}
NSAssert(request.requestTask != nil, @"requestTask should not be nil");
// 设置 request 的优先级
// !!Available on iOS 8 +
if ([request.requestTask respondsToSelector:@selector(priority)]) {
switch (request.requestPriority) {
case YTKRequestPriorityHigh:
request.requestTask.priority = NSURLSessionTaskPriorityHigh;
break;
case YTKRequestPriorityLow:
request.requestTask.priority = NSURLSessionTaskPriorityLow;
break;
case YTKRequestPriorityDefault:
/*!!fall through*/
default:
request.requestTask.priority = NSURLSessionTaskPriorityDefault;
break;
}
}
/*
* 将 request 添加到 YTKNetworkAgent 的字典型私有变量 requestsRecord 中。
* _requestsRecord[@(request.requestTask.taskIdentifier)] = request;
* requestsRecord 可精确索引每一个 request
*/
YTKLog(@"Add request: %@", NSStringFromClass([request class]));
[self addRequestToRecord:request];
// 启动 request.requestTask
[request.requestTask resume];
}
方法 addRequest 的主要功能注释已经说得很清楚了,这里我们主要留意以下四行代码:
NSURLRequest *customUrlRequest= [request buildCustomUrlRequest]
覆盖 YTKBaseRequest 的 buildCustomUrlRequest 方法,可忽略 requestUrl,requestTimeoutInterval,requestArgument,allowsCellularAccess,requestMethod 和 requestSerializerType 方法,创建一个自定义的 NSURLRequest 对象。self requestDidFailWithRequest:request error:requestSerializationError]
请求失败时,若是下载任务,则对下载了一半的数据进行缓存,提供断点续传功能,并执行一系列请求失败的回调或 block 代码块,包括上文提到的 toggleAccessoriesWillStopCallBack 方法、toggleAccessoriesDidStopCallBack 方法。request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError]
使用 request 生成一个 NSURLSessionTask 的实例对象,作为 request 的 requestTask 属性。[self handleRequestResult:dataTask responseObject:responseObject error:error]
处理请求的结果。
方法 sessionTaskForRequest:error:
的代码如下:
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request
error:(NSError * _Nullable __autoreleasing *)error
{
// 为下面生成 dataTask 准备好参数
YTKRequestMethod method = [request requestMethod];
NSString *url = [self buildRequestUrl:request];
id param = request.requestArgument;
AFConstructingBlock constructingBlock = [request constructingBodyBlock];
AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];
switch (method) {
case YTKRequestMethodGET:
if (request.resumableDownloadPath) {
/*
* 若 request 的属性 resumableDownloadPath 不为空,则返回 NSURLSessionDownloadTask 的实例对象
* 以下方法 downloadTaskWithDownloadPath 中,首先判断是否存在上一次下载失败的数据,若存在则创建一个断点续传的 downloadTask。
*/
return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];
} else {
return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
}
case YTKRequestMethodPOST:
return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];
case YTKRequestMethodHEAD:
return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPUT:
return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodDELETE:
return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPATCH:
return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error];
}
}
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error
{
NSMutableURLRequest *request = nil;
if (block) {
// block 存在,则表明有文件需要 post 上服务器
request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
} else {
request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:request
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
[self handleRequestResult:dataTask responseObject:responseObject error:_error];
}];
return dataTask;
}
注意,直到目前为止,只有两处调用了 AFNetworking 的 API dataTaskWithRequest:completionHandler:
。一处是 addRequest:
中,用户覆写了 YTKBaseRequest 的方法 buildCustomUrlRequest
时;一处就是以上的代码的第 46 行了。而该 API 的回调 block 中,都调用了方法 handleRequestResult:responseObject:error:
,详细代码如下:
- (void)handleRequestResult:(NSURLSessionTask *)task
responseObject:(id)responseObject
error:(NSError *)error
{
// 从字典 _requestsRecord 中取出 request 对象
Lock();
YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
Unlock();
// When the request is cancelled and removed from records, the underlying
// AFNetworking failure callback will still kicks in, resulting in a nil `request`.
//
// Here we choose to completely ignore cancelled tasks. Neither success or failure
// callback will be called.
if (!request) {
return;
}
YTKLog(@"Finished Request: %@", NSStringFromClass([request class]));
NSError * __autoreleasing serializationError = nil;
NSError * __autoreleasing validationError = nil;
NSError *requestError = nil;
BOOL succeed = NO;
request.responseObject = responseObject;
if ([request.responseObject isKindOfClass:[NSData class]]) {
request.responseData = responseObject;
request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];
switch (request.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Default serializer. Do nothing.
break;
case YTKResponseSerializerTypeJSON:
request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
request.responseJSONObject = request.responseObject;
break;
case YTKResponseSerializerTypeXMLParser:
request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
break;
}
}
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}
if (succeed) {
[self requestDidSucceedWithRequest:request];
} else {
[self requestDidFailWithRequest:request error:requestError];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self removeRequestFromRecord:request];
[request clearCompletionBlock];
});
}
这个方法中,对请求的响应进行了一系列的处理,包括给 request 的属性 responseObject、responseData、responseString 赋值,对返回的结果进行有效性验证,然后在方法 requestDidSucceedWithRequest:
、requestDidFailWithRequest:
中执行一系列的成功或失败的回调、block 代码块,最后在主线程中,把字典 _requestsRecord 中的这个 request 给移除掉,并把 block 置 nil。
到这里为止,YTKNetwork 是如何执行一个请求操作的脉络就基本清晰了。
此外,在 YTKRequest 类中,有着关于缓存的方法可供子类覆写,包括:cacheTimeInSeconds
、cacheVersion
、cacheSensitiveData
、writeCacheAsynchronously
。支持按时间、版本号缓存网络请求内容。
YTKBatchRequest 类支持批量的网络请求发送、YTKChainRequest 类支持方便地设置有相互依赖的网络请求的发送,协议 YTKUrlFilterProtocol 的存在,使得对网络请求的 URL 统一加参数、修改路径成为可能,这些这里就不再一一细说了。
最后附上 YTKNetwork 的github 下载链接:YTKNetwork
参考文章:
YTKNetwork 的 README.md
Objective C–命令模式
苹果开发者文档 CocoaDesignPatterns 之 Command
ObjC 中国:从 NSURLConnection 到 NSURLSession