YTKNetwork 到底做了什么

背景

在 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。

表1

表1

安装

可在 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所示:
图1

图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 类中,有着关于缓存的方法可供子类覆写,包括:cacheTimeInSecondscacheVersioncacheSensitiveDatawriteCacheAsynchronously。支持按时间、版本号缓存网络请求内容。
YTKBatchRequest 类支持批量的网络请求发送、YTKChainRequest 类支持方便地设置有相互依赖的网络请求的发送,协议 YTKUrlFilterProtocol 的存在,使得对网络请求的 URL 统一加参数、修改路径成为可能,这些这里就不再一一细说了。
最后附上 YTKNetwork 的github 下载链接:YTKNetwork

参考文章:

YTKNetwork 的 README.md
Objective C–命令模式
苹果开发者文档 CocoaDesignPatterns 之 Command
ObjC 中国:从 NSURLConnection 到 NSURLSession

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值