iOS网络编程(YTKNetwork)

作者:J_Knight_
链接:https://www.jianshu.com/p/89dd444399ce
來源:

一.概述

基于 AFNetworking 进行再一次封装,以及缓存处理。采用了离散式替代传统 AFNetworking 的集约式编程。通过给每一个请求创建一个对象的高度定制性,使 url 以及响应方式等内容不暴露出来,以增加了一些代码工作量为代价,更好的做到业务层与网络层的分离。

二.架构

iOS网络编程(YTKNetwork)_第1张图片
架构图.png

说明:

  1. YTKNetwork 框架将每一个请求实例化,YTKBaseRequest 是所有请求类的基类,YTKRequest 是它的子类。所以如果我们想要发送一个请求,则需要创建并实例化一个继承于YTKRequest的自定义的请求类(CustomRequest)并发送请求。
  2. YTKNetworkAgent 是一个单例,负责管理所有的请求类(例如 CustomRequest )。当 CustomRequest 发送请求以后,会把自己放在 YTKNetworkAgent 持有的一个字典里,让其管理自己。
  3. 我们说 YTKNetwork 封装了 AFNetworking ,实际上是 YTKNetworkAgent 封装了 AFNetworking ,由它负责 AFNetworking 请求的发送和 AFNetworking 的回调处理。所以如果我们想更换一个第三方网络请求库,就可以在这里更换一下。而 YTKRequest 更多的是只是负责缓存的处理。

三.设计模式

采用命令模式(Command Pattern)设计模式。
命令模式的简单介绍:

  • Client:创建具体命令
  • Invoker:命令调用者
  • Receiver:命令接受者
  • ConcreteCommand:具体命令
  • Command:抽象命令

在 YTKNetwork 中的对应应用如下表:

对应表.png

命令模式的本质是对命令的封装,将发出命令的责任和执行命令的责任分割开。
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。

四.YTKNetwork 各类介绍

类名 职责
YTKBaseRequest 所有请求类的基类。持有 NSURLSessionTask 实例,responseData,responseObject,error 等重要数据,提供一些需要子类实现的与网络请求相关的方法,处理回调的代理和 block,命令 YTKNetworkAgent 发起网络请求。
YTKRequest YTKBaseRequest 的子类。负责缓存的处理:请求前查询缓存;请求后写入缓存。
YTKNetworkConfig 被 YTKRequest 和 YTKNetworkAgent 访问。负责所有请求的全局配置,例如 baseUrl 和 CDNUrl 等等。
YTKNetworkPrivate 提供 JSON 验证,appVersion 等辅助性的方法;给 YTKBaseRequest 增加一些分类。
YTKNetworkAgent 真正发起请求的类。负责发起请求,结束请求,并持有一个字典来存储正在执行的请求。
YTKBatchRequest 负责管理多个 YTKBatchRequest 实例,持有一个数组来保存 YTKBatchRequest 。支持添加和删除 YTKBatchRequest 实例。
YTKChainRequest 可以发起链式请求,持有一个数组来保存所有的请求类。当某个请求结束后才能发起下一个请求,如果其中有一个请求返回失败,则认定本请求链失败。
YTKChainRequestAgent 负责管理多个 YTKChainRequestAgent 实例,持有一个数组来保存 YTKChainRequest。支持添加和删除 YTKChainRequest 实例。

五.基本使用

单个请求配置

官方的教程建议我们将请求的全局配置是在 AppDelegate.m 文件里,设定 baseUrl 以及 cdnUrl 等参数。

- (BOOL)application:(UIApplication *)application 
   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
   config.baseUrl = @"http://yuantiku.com";
   config.cdnUrl = @"http://fen.bi";
}

如果我们需要新建一个注册的请求,则需要创建一个继承于YTKRequest的注册接口的类RegisterApi,并将针对该请求参数配置好:

// RegisterApi.h
#import "YTKRequest.h"

@interface RegisterApi : YTKRequest

- (id)initWithUsername:(NSString *)username password:(NSString *)password;

@end


// RegisterApi.m
#import "RegisterApi.h"

@implementation RegisterApi {
    NSString *_username;
    NSString *_password;
}

//初始化的时候将两个参数值传入
- (id)initWithUsername:(NSString *)username password:(NSString *)password {
    self = [super init];
    if (self) {
        _username = username;
        _password = password;
    }
    return self;
}

//需要和baseUrl拼接的地址
- (NSString *)requestUrl {
    // “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息
    return @"/iphone/register";
}

//请求方法,某人是GET
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
}

//请求体
- (id)requestArgument {
    return @{
        @"username": _username,
        @"password": _password
    };
}

@end
单个请求的发起

还是刚才的注册 API,在实例化以后,直接调用startWithCompletionBlockWithSuccess:failure 方法(或start方法)就可以发起它:

RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"succeed");
} failure:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"failed");
}];

六.源码解析

一.封装请求,调用 start 方法

startWithCompletionBlockWithSuccess:failure

来看一下YTKNetwork做了什么:

//YTKBaseRequest.m
//传入成功和失败的block,并保存起来
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                                    failure:(YTKRequestCompletionBlock)failure {
    //保存成功和失败的回调block,便于将来调用
    [self setCompletionBlockWithSuccess:success failure:failure];
    //发起请求
    [self start];
}

//保存成功和失败的block
- (void)setCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                              failure:(YTKRequestCompletionBlock)failure {
    self.successCompletionBlock = success;
    self.failureCompletionBlock = failure;
}

当保存完成功和失败的 bloc k以后,调用 start 方法,于是来到了 YTKRequest 类(注意,虽然 YTKBaseRequest 也实现了 start 方法,但是由于 YTKRequest 类是它的子类并也实现了 start 方法,所以这里最先走的是 YTKRequest 类的 start 方法):

- (void)start {
    //如果忽略缓存
    //ignoreCache属性是用户手动设置的,如果用户强制忽略缓存,则无论是否缓存是否存在,直接发送请求。
    if (self.ignoreCache) {
        //调用无缓存请求方法
        [self startWithoutCache];
        return;
    }

    //如果存在下载未完成的文件(不缓存下载请求)
    //resumableDownloadPath是断点下载路径,如果该路径不为空,说明有未完成的下载任务,则直接发送请求继续下载。
    if (self.resumableDownloadPath) {
        //调用无缓存请求方法
        [self startWithoutCache];
        return;
    }

    //获取缓存失败
    //loadCacheWithError:方法验证了加载缓存是否成功的方法(返回值为YES,说明可以加载缓存;反之亦然
    if (![self loadCacheWithError:nil]) {
        //调用无缓存请求方法
        [self startWithoutCache];
        return;
    }

    //到这里一定是有缓存
    _dataFromCache = YES;

    //开启主线程异步方法
    dispatch_async(dispatch_get_main_queue(), ^{
        //缓存处理
        [self requestCompletePreprocessor];
        //用户可以在这里进行真正的回调前操作
        [self requestCompleteFilter];
        
        //执行回调
        YTKRequest *strongSelf = self;
        //请求完成后的代理回调
        [strongSelf.delegate requestFinished:strongSelf];
        //请求成功的block
        if (strongSelf.successCompletionBlock) {
            strongSelf.successCompletionBlock(strongSelf);
        }
        //手动把成功和失败block都置为nil,避免循环引用
        [strongSelf clearCompletionBlock];
    });
}

二.判断有缓存情况还是无缓存情况

loadCacheWithError

验证加载缓存是否成功的方法(返回值为YES,说明可以加载缓存;反之亦然),看一下具体实现:

- (BOOL)loadCacheWithError:(NSError * _Nullable __autoreleasing *)error {
    //缓存时间小于0,缓存不可用,返回NO(缓存时间默认为-1,需要用户手动设置,单位是秒)
    if ([self cacheTimeInSeconds] < 0) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}];
        }
        return NO;
    }

    //是否有缓存的元数据,如果没有,返回错误
    //元数据是指数据的数据,在这里描述了缓存数据本身的一些特征:包括版本号,缓存时间,敏感信息等等
    if (![self loadCacheMetadata]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}];
        }
        return NO;
    }

    //检查缓存是否可用,如果不可用,返回NO
    if (![self validateCacheWithError:error]) {
        return NO;
    }

    //检查缓存是否可以加载,如果不能加载,返回NO
    if (![self loadCacheData]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}];
        }
        return NO;
    }

    //其他情况表示能够加载缓存
    return YES;
}
loadCacheMetadata

我们来看一下上面关于缓存的元数据的获取方法。

- (BOOL)loadCacheMetadata {
    //获取缓存元数据路径
    NSString *path = [self cacheMetadataFilePath];
    NSFileManager * fileManager = [NSFileManager defaultManager];
    //使用try catch捕获异常避免崩溃
    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        @try {
            _cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
            return YES;
        } @catch (NSException *exception) {
            YTKLog(@"Load cache metadata failed, reason = %@", exception.reason);
            return NO;
        }
    }
    return NO;
}

其中 [self cacheMetadataFilePath] 缓存元数据路径获取。
缓存元数据路径由根路径加缓存元数据子路径构成。

  • 根路径:默认为 library 路径拼接 "LazyRequestCache" 。如果遵循 YTKCacheDirPathFilterProtocol 协议实现 - (NSString *)filterCacheDirPath:(NSString *)originPath withRequest:(YTKBaseRequest *)request; 方法可更改跟路径。根据路径查询目录,如果目录不存在则创建。
  • 缓存元数据子路径: 获取 requestMethodbaseUrlrequestUrlargument 组成requestInfo,加密后形成缓存文件名。再加上.metadata形成缓存元数据子路径。
cacheMetadata(YTKCacheMetadata)

它描述的是缓存的版本号,敏感信息,创建时间,app版本等信息,并支持序列化处理,可以保存在磁盘里。
因此,loadCacheMetadata 方法的目的是将之前被序列化保存的缓存元数据信息反序列化,赋给自身的cacheMetadata 属性上。

//YTKRequest.m
@interface YTKCacheMetadata : NSObject

@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;

@end
validateCacheWithError

逐一验证元数据里的各项信息是否符合要求。

- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
    // 是否大于过期时间
    NSDate *creationDate = self.cacheMetadata.creationDate;
    NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
    //[self cacheTimeInSeconds] 设定缓存有效时间
    if (duration < 0 || duration > [self cacheTimeInSeconds]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
        }
        return NO;
    }
    // 缓存的版本号是否符合
    long long cacheVersionFileContent = self.cacheMetadata.version;
    if (cacheVersionFileContent != [self cacheVersion]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
        }
        return NO;
    }
    // 敏感信息是否符合 (元数据中的敏感信息 与 请求中设定的敏感信息 比对)
    NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
    NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
    if (sensitiveDataString || currentSensitiveDataString) {
        // If one of the strings is nil, short-circuit evaluation will trigger
        if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
            }
            return NO;
        }
    }
    // app的版本是否符合
    NSString *appVersionString = self.cacheMetadata.appVersionString;
    NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
    if (appVersionString || currentAppVersionString) {
        if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
            }
            return NO;
        }
    }
    return YES;
}
loadCacheData
- (BOOL)loadCacheData {
    // 获取缓存路径
    NSString *path = [self cacheFilePath];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;

    // 如果缓存目录存在
    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        NSData *data = [NSData dataWithContentsOfFile:path];
        _cacheData = data;
        _cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
        // 根据数据类型对缓存数据进行处理
        switch (self.responseSerializerType) {
            case YTKResponseSerializerTypeHTTP:
                // Do nothing.
                return YES;
            case YTKResponseSerializerTypeJSON:
                _cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
                return error == nil;
            case YTKResponseSerializerTypeXMLParser:
                _cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
                return YES;
        }
    }
    return NO;
}
dataFromCache

当确认缓存可以成功取出后,手动设置 dataFromCache 属性为 YES,说明当前的请求结果是来自于缓存,而没有通过网络请求。

requestCompletePreprocessor
- (void)requestCompletePreprocessor {
    [super requestCompletePreprocessor];

    // 是否异步将responseData写入缓存(写入缓存的任务放在专门的队列ytkrequest_cache_writing_queue进行)
    if (self.writeCacheAsynchronously) {
        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            // 保存响应数据到缓存
            [self saveResponseDataToCacheFile:[super responseData]];
        });
    } else {
        // 保存响应数据到缓存
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}
- (void)saveResponseDataToCacheFile:(NSData *)data {
    // 设置缓存时间大于0 且 此数据之前不是缓存数据
    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
        // 数据额非空
        if (data != nil) {
            @try {
                // 总是覆盖旧数据
                [data writeToFile:[self cacheFilePath] atomically:YES];

                // 设置缓存元数据
                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                // 储存缓存元数据
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}
requestCompleteFilter
- (void)requestCompleteFilter {
}

可以看到此方法内容为空。用户可以在这里做一些回调前的处理。

三.缓存情况下

    //到这里一定是有缓存
    _dataFromCache = YES;

    //开启主线程异步方法
    dispatch_async(dispatch_get_main_queue(), ^{
        //缓存处理
        [self requestCompletePreprocessor];
        //用户可以在这里进行真正的回调前操作
        [self requestCompleteFilter];
        
        //执行回调
        YTKRequest *strongSelf = self;
        //请求完成后的代理回调
        [strongSelf.delegate requestFinished:strongSelf];
        //请求成功的block
        if (strongSelf.successCompletionBlock) {
            strongSelf.successCompletionBlock(strongSelf);
        }
        //手动把成功和失败block都置为nil,避免循环引用
        [strongSelf clearCompletionBlock];
    });

在有缓存的情况下,直接利用缓存执行代理和 block 的回调

四.无缓存情况下

执行无缓存请求。

 [self startWithoutCache];
startWithoutCache
- (void)startWithoutCache {
    // 清除本类中全局变量
    [self clearCacheVariables];
    //调用父类的 start 方法
    [super start];
}
- (void)clearCacheVariables {
    _cacheData = nil;
    _cacheXML = nil;
    _cacheJSON = nil;
    _cacheString = nil;
    _cacheMetadata = nil;
    _dataFromCache = NO;
}
YTKBaseRequest start 方法
- (void)start {
    // 告诉Accessories即将回调了(其实是即将发起请求)
    [self toggleAccessoriesWillStartCallBack];
    // Agent添加请求
    [[YTKNetworkAgent sharedAgent] addRequest:self];
}

- (void)toggleAccessoriesWillStartCallBack {
    // 遵循YTKRequestAccessory代理的类执行start方法
    for (id accessory in self.requestAccessories) {
        if ([accessory respondsToSelector:@selector(requestWillStart:)]) {
            [accessory requestWillStart:self];
        }
    }
}
addRequest
- (void)addRequest:(YTKBaseRequest *)request {
    
    // 断言request不为空
    NSParameterAssert(request != nil);

    // 定义error默认为空
    NSError * __autoreleasing requestSerializationError = nil;

    // 获取自定义request,默认为空 如果自定义子类中中实现buildCustomUrlRequest方法则获取方法中的request
    NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
    // 判断自定义request是否存在
    if (customUrlRequest) {
        __block NSURLSessionDataTask *dataTask = nil;
        // 调用afn中网络请求方法
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            // 使用handleRequestResult处理afn返回结果
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;
    } else {
        // 如果自定义request不存在,使用传入的request(即baseRequest类型的具体request)
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
    }

    // 如果请求序列化错误
    if (requestSerializationError) {
        // 处理错误
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

    // 断言
    NSAssert(request.requestTask != nil, @"requestTask should not be nil");

    // ios8 以后可以设定请求优先级
    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;
        }
    }

    YTKLog(@"Add request: %@", NSStringFromClass([request class]));
    //请求备份
    [self addRequestToRecord:request];
    // 开始请求
    [request.requestTask resume];
}

此方法主要分三个部分:

  • 第一部分是获取当前请求对应的task并赋给request的requestTask属性
  • 第二部分是把request放入专门用来保存请求的字典中
  • 第三部分是启动task
sessionTaskForRequest: error :
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
    // 获取请求方法类型
    YTKRequestMethod method = [request requestMethod];
    // 获取url
    NSString *url = [self buildRequestUrl:request];
    // 获取请求参数
    id param = request.requestArgument;
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];
    // 获取request serializer
    AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

    // 根据不同请求方法类型返回task
    switch (method) {
        case YTKRequestMethodGET:
            // 如果断点下载
            if (request.resumableDownloadPath) {
                // 返回下载任务
                return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];
            } else {
                // 返回普通get任务
                return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            }
        case YTKRequestMethodPOST:
            //返回post任务
            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];
    }
}

五. NSURLSessionTask 参数配置

URL处理
//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

  ...
  NSString *url = [self buildRequestUrl:request];
  ...

}

- (NSString *)buildRequestUrl:(YTKBaseRequest *)request {
    // 断言
    NSParameterAssert(request != nil);

    //获取子类中自定义requesturl
    NSString *detailUrl = [request requestUrl];
    NSURL *temp = [NSURL URLWithString:detailUrl];
    
    // 存在host和scheme的url立即返回正确
    if (temp && temp.host && temp.scheme) {
        return detailUrl;
    }
    
    // 如果遵循YTKUrlFilterProtocol 重写filterUrl方法 过滤url
    NSArray *filters = [_config urlFilters];
    for (id f in filters) {
        detailUrl = [f filterUrl:detailUrl withRequest:request];
    }

    
    NSString *baseUrl;
    if ([request useCDN]) {
        // 如果使用CDN,在当前请求没有配置CDN地址的情况下,返回全局配置的CDN
        if ([request cdnUrl].length > 0) {
            baseUrl = [request cdnUrl];
        } else {
            baseUrl = [_config cdnUrl];
        }
    } else {
        // 如果使用baseUrl,在当前请求没有配置baseUrl,返回全局配置的baseUrl
        if ([request baseUrl].length > 0) {
            baseUrl = [request baseUrl];
        } else {
            baseUrl = [_config baseUrl];
        }
    }
    // 如果末尾没有/,则在末尾添加一个/
    NSURL *url = [NSURL URLWithString:baseUrl];

    if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/"]) {
        url = [url URLByAppendingPathComponent:@""];
    }

    // 拼接baseUrl和detailUrl并返回完整url
    return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString;
}
requestSerializerForRequest
- (AFHTTPRequestSerializer *)requestSerializerForRequest:(YTKBaseRequest *)request {
    AFHTTPRequestSerializer *requestSerializer = nil;
    // 判断请求的序列化类型是HTTP还是JSON (可通过requestSerializerType手动设定)
    if (request.requestSerializerType == YTKRequestSerializerTypeHTTP) {
        // 创建HTTP类型
        requestSerializer = [AFHTTPRequestSerializer serializer];
    } else if (request.requestSerializerType == YTKRequestSerializerTypeJSON) {
        // 创建JSON类型
        requestSerializer = [AFJSONRequestSerializer serializer];
    }

    // 超时时间 默认60 可手动设置
    requestSerializer.timeoutInterval = [request requestTimeoutInterval];
    // 是否允许使用蜂窝移动数据(4g)默认可以 可手动设置
    requestSerializer.allowsCellularAccess = [request allowsCellularAccess];

    // 如果当前请求需要服务器账号和密码
    NSArray *authorizationHeaderFieldArray = [request requestAuthorizationHeaderFieldArray];
    if (authorizationHeaderFieldArray != nil) {
        [requestSerializer setAuthorizationHeaderFieldWithUsername:authorizationHeaderFieldArray.firstObject
                                                          password:authorizationHeaderFieldArray.lastObject];
    }

    // 如果当前请求需要自定义 HTTPHeaderField
    NSDictionary *headerFieldValueDictionary = [request requestHeaderFieldValueDictionary];
    if (headerFieldValueDictionary != nil) {
        for (NSString *httpHeaderField in headerFieldValueDictionary.allKeys) {
            NSString *value = headerFieldValueDictionary[httpHeaderField];
            [requestSerializer setValue:value forHTTPHeaderField:httpHeaderField];
        }
    }
    return requestSerializer;
}

六.开始请求

获取 task
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                               requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                       constructingBodyWithBlock:(nullable void (^)(id  formData))block
                                           error:(NSError * _Nullable __autoreleasing *)error {
    NSMutableURLRequest *request = nil;

    // 根据有无Block使用AFN中的AFHTTPRequestSerializer创建request
    if (block) {
        request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
    } else {
        request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
    }

    // 根据request创建dataTask
    __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;
}
启动task
[request.requestTask resume];

七.处理回调

handleRequestResult

响应的统一处理内部操作

- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
    // 根据task获取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对应的response
    request.responseObject = responseObject;
    if ([request.responseObject isKindOfClass:[NSData class]]) {
        // 获取 responseData
        request.responseData = responseObject;
        // 获取responseString
        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;
        }
    }
    
    // 判断是否有错误,将错误对象赋值给requestError,改变succeed的布尔值。目的是根据succeed的值来判断到底是进行成功的回调还是失败的回调
    if (error) {
        // 如果该方法传入的error不为nil
        succeed = NO;
        requestError = error;
    } else if (serializationError) {
        // 如果序列化失败了
        succeed = NO;
        requestError = serializationError;
    } else {
        // 即使没有error而且序列化通过,也要验证request是否有效
        succeed = [self validateResult:request error:&validationError];
        requestError = validationError;
    }

    // 根据succeed的布尔值来调用相应的处理
    if (succeed) {
        // 请求成功的处理
        [self requestDidSucceedWithRequest:request];
    } else {
        // 请求失败的处理
        [self requestDidFailWithRequest:request error:requestError];
    }

    // 回调完成的处理
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在字典里移除当前request
        [self removeRequestFromRecord:request];
        // 清除所有block
        [request clearCompletionBlock];
    });
}

简单讲解一下上面的代码:

  • 前面以 task 的 identifier 为 key,将 request 存入字典,这里根据 task 取出 request。
  • 然后将获得的responseObject进行处理,将处理后获得的responseObject,responseData和responseString赋值给当前的请求实例request。
  • 再根据这些值的获取情况来判断最终回调的成败(改变succeed的值)。
  • 最后根据succeed的值来进行成功和失败的回调。
validateResult

验证返回的json数据是否有效

- (BOOL)validateResult:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
    // 判断statusCode是否在200~299之间,如果是则返回状态有效
    BOOL result = [request statusCodeValidator];
    if (!result) {
        // 如果状态码无效,传入error地址存在
        if (error) {
            // 创建NSError对象赋值给error响应地址
            *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidStatusCode userInfo:@{NSLocalizedDescriptionKey:@"Invalid status code"}];
        }
        // 返回result
        return result;
    }
    
    // 状态码有效的情况下判断json是否有效
    // 去返回json
    id json = [request responseJSONObject];
    // 取设定的json数据格式,可手动设置
    id validator = [request jsonValidator];
    // 如果同时存在
    if (json && validator) {
        // 判断json是否符合设定的jsonValidator
        result = [YTKNetworkUtils validateJSON:json withValidator:validator];
        if (!result) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidJSONFormat userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON format"}];
            }
            return result;
        }
    }
    return YES;
}

在这里,首先,用statusCodeValidator方法判断响应的code是否在正确的范围,然后再判断json的有效性。

validateJSON

判断目标字典对象是否满足于设置字典模型的key一致时,value类型与自定义类型格式相同

+ (BOOL)validateJSON:(id)json withValidator:(id)jsonValidator {
    // 判断是否同为字典类型
    if ([json isKindOfClass:[NSDictionary class]] &&
        [jsonValidator isKindOfClass:[NSDictionary class]]) {
        // 转为字典类型
        NSDictionary * dict = json;
        NSDictionary * validator = jsonValidator;
        BOOL result = YES;
        // 实例遍历类型对象
        NSEnumerator * enumerator = [validator keyEnumerator];
        NSString * key;
        // 使用while遍历validator的key数组,直到为空
        while ((key = [enumerator nextObject]) != nil) {
            // 根据key取dict字典value,和validator字典中定义类型
            id value = dict[key];
            id format = validator[key];
            // 如果vlue仍为字典或者数组,递归
            if ([value isKindOfClass:[NSDictionary class]]
                || [value isKindOfClass:[NSArray class]]) {
                result = [self validateJSON:value withValidator:format];
                if (!result) {
                    break;
                }
            } else {
                // 如果value不为定义类型 且 value不为空类型
                if ([value isKindOfClass:format] == NO &&
                    [value isKindOfClass:[NSNull class]] == NO) {
                    // 结果为NO
                    result = NO;
                    // 有一个结果为NO则全部为NO, 不需要再循环,跳出
                    break;
                }
            }
        }
        // 返回结果
        return result;
    // 如果json和jsonValidator同为数组类型
    } else if ([json isKindOfClass:[NSArray class]] &&
               [jsonValidator isKindOfClass:[NSArray class]]) {
        NSArray * validatorArray = (NSArray *)jsonValidator;
        if (validatorArray.count > 0) {
            // 目标对象转为数组
            NSArray * array = json;
            // 验证对象数组取第一个字典为模型即可
            NSDictionary * validator = jsonValidator[0];
            // 目标对象中的每一个格式 和 字典模型一致即可
            for (id item in array) {
                // 递归
                BOOL result = [self validateJSON:item withValidator:validator];
                if (!result) {
                    return NO;
                }
            }
        }
        return YES;
    // 类型直接相同返回yes
    } else if ([json isKindOfClass:jsonValidator]) {
        return YES;
    // 类型不同返回no
    } else {
        return NO;
    }
}
requestDidSucceedWithRequest

请求成功的处理。

- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
    // 加入手动创建的自动释放池中,执行完池中方法后相应内存直接释放
    // 响应数据写入文件缓存后相应内存直接释放,最大限度降低内存
    @autoreleasepool {
        // 写入缓存
        [request requestCompletePreprocessor];
    }
    
    // 异步回到主队列
    dispatch_async(dispatch_get_main_queue(), ^{
        // 告诉Accessories请求就要结束了
        [request toggleAccessoriesWillStopCallBack];
        // 在最后的回调之前可以做的处理,用户可以自定义
        [request requestCompleteFilter];

        // 如果有代理,则调用成功的代理
        if (request.delegate != nil) {
            [request.delegate requestFinished:request];
        }
        
        // 如果传入了成功的block,则调用
        if (request.successCompletionBlock) {
            request.successCompletionBlock(request);
        }
        
        // 告诉Accessories请求已经结束了
        [request toggleAccessoriesDidStopCallBack];
    });
}
requestDidFailWithRequest

请求失败处理。

- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error {
    // 赋值error
    request.error = error;
    YTKLog(@"Request %@ failed, status code = %ld, error = %@",
           NSStringFromClass([request class]), (long)request.responseStatusCode, error.localizedDescription);

    // 储存未完成的下载数据,存入resumableDownloadPath,等待断点续传
    NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData];
    if (incompleteDownloadData) {
        [incompleteDownloadData writeToURL:[self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] atomically:YES];
    }

    // 下载失败清除文件目录中的文件并清空响应
    if ([request.responseObject isKindOfClass:[NSURL class]]) {
        NSURL *url = request.responseObject;
        if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
            request.responseData = [NSData dataWithContentsOfURL:url];
            request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

            [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
        }
        request.responseObject = nil;
    }

    // 失败预处理默认是不做处理的,如需要需自定义
    @autoreleasepool {
        [request requestFailedPreprocessor];
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        // 告诉Accessories请求就要结束了
        [request toggleAccessoriesWillStopCallBack];
        // 在真正的回调之前做的处理,可自定义
        [request requestFailedFilter];

        // 如果有代理,就调用代理
        if (request.delegate != nil) {
            [request.delegate requestFailed:request];
        }
        // 如果传入了失败回调的block代码,就调用block
        if (request.failureCompletionBlock) {
            request.failureCompletionBlock(request);
        }
        // 告诉Accessories请求已经停止了
        [request toggleAccessoriesDidStopCallBack];
    });
}

在这个方法里,首先判断了当前任务是否为下载任务,如果是,则储存当前已经下载好的data到resumableDownloadPath里面。而如果下载任务失败,则将其对应的在本地保存的路径上的文件清空。


iOS网络编程(YTKNetwork)_第2张图片
流程图.png

八.取消请求

两个取消方法:

//YTKNetworkAgent.h
///  取消某个request
- (void)cancelRequest:(YTKBaseRequest *)request;

///  取消所有添加的request
- (void)cancelAllRequests;
cancelRequest

取消某个request。

- (void)cancelRequest:(YTKBaseRequest *)request {
    // 断言request不为空
    NSParameterAssert(request != nil);

    // 如果下载过程中取消
    if (request.resumableDownloadPath) {
        NSURLSessionDownloadTask *requestTask = (NSURLSessionDownloadTask *)request.requestTask;
        // 写入resumableDownloadPath等待断点续传。方法内部会调用cancel方法
        [requestTask cancelByProducingResumeData:^(NSData *resumeData) {
            NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath];
            [resumeData writeToURL:localUrl atomically:YES];
        }];
    } else {
        // 获取request的task,并取消
        [request.requestTask cancel];
    }

    // 从字典里移除当前request
    [self removeRequestFromRecord:request];
    // 清理所有block
    [request clearCompletionBlock];
}

- (void)removeRequestFromRecord:(YTKBaseRequest *)request {
    // 加锁
    Lock();
    // 移除字典中这组数据
    [_requestsRecord removeObjectForKey:@(request.requestTask.taskIdentifier)];
    YTKLog(@"Request queue size = %zd", [_requestsRecord count]);
    Unlock();
}
cancelAllRequests

取消所有request。

- (void)cancelAllRequests {
    // 加锁。多个线程对同一个字典进行操作的,都需要加锁
    Lock();
    NSArray *allKeys = [_requestsRecord allKeys];
    Unlock();
    if (allKeys && allKeys.count > 0) {
        // copy
        NSArray *copiedKeys = [allKeys copy];
        for (NSNumber *key in copiedKeys) {
            Lock();
            YTKBaseRequest *request = _requestsRecord[key];
            Unlock();
            // We are using non-recursive lock.
            // Do not lock `stop`, otherwise deadlock may occur.
            // stop每个请求
            [request stop];
        }
    }
}
stop
- (void)stop {
    // 告诉Accessories将要停止回调了
    [self toggleAccessoriesWillStopCallBack];
    // 清空代理
    self.delegate = nil;
    // 调用agent的取消某个request的方法
    [[YTKNetworkAgent sharedAgent] cancelRequest:self];
    // 告诉Accessories已经停止回调完成了
    [self toggleAccessoriesDidStopCallBack];
}

九.批量请求

初始化
- (instancetype)initWithRequestArray:(NSArray *)requestArray {
    self = [super init];
    if (self) {
        // 保存为属性,使用copy即使requestArray改变_requestArray也不会变
        _requestArray = [requestArray copy];
        //批量请求完成的数量初始化为0
        _finishedCount = 0;
        //类型检查,所有元素都必须为YTKRequest或的它的子类,否则强制初始化失败
        for (YTKRequest * req in _requestArray) {
            if (![req isKindOfClass:[YTKRequest class]]) {
                YTKLog(@"Error, request item must be YTKRequest instance.");
                return nil;
            }
        }
    }
    return self;
}

初始化以后,我们就可以调用start方法来发起当前YTKBatchRequest实例所管理的所有请求了。

- (void)start {
    //如果batch里第一个请求已经成功结束,则不能再start
    if (_finishedCount > 0) {
        YTKLog(@"Error! Batch request has already started.");
        return;
    }
    //最开始设定失败的request为nil
    _failedRequest = nil;
    //使用YTKBatchRequestAgent来管理当前的批量请求
    [[YTKBatchRequestAgent sharedAgent] addBatchRequest:self];
    [self toggleAccessoriesWillStartCallBack];
    
    //遍历所有request,并开始请求
    for (YTKRequest * req in _requestArray) {
        req.delegate = self;
        [req clearCompletionBlock];
        [req start];
    }
}
requestFinished

单个请求完成,如果全部完成则批量请求完成。

- (void)requestFinished:(YTKRequest *)request {
    //某个request成功后,首先让_finishedCount + 1
    _finishedCount++;
    //如果_finishedCount等于_requestArray的个数,则判定当前batch请求成功
    if (_finishedCount == _requestArray.count) {
        //调用即将结束的代理
        [self toggleAccessoriesWillStopCallBack];
        //调用请求成功的代理
        if ([_delegate respondsToSelector:@selector(batchRequestFinished:)]) {
            [_delegate batchRequestFinished:self];
        }
        //调用批量请求成功的block
        if (_successCompletionBlock) {
            _successCompletionBlock(self);
        }
        //清空成功和失败的block
        [self clearCompletionBlock];
        //调用请求结束的代理
        [self toggleAccessoriesDidStopCallBack];
         //从YTKBatchRequestAgent里移除当前的batch
        [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
    }
}
requestFailed

单个请求失败即批量请求失败,将失败的请求保存到_failedRequest中

- (void)requestFailed:(YTKRequest *)request {
    // 失败请求保存在_failedRequest中
    _failedRequest = request;
    // 调用即将结束的代理
    [self toggleAccessoriesWillStopCallBack];
    // 停止batch里所有的请求
    for (YTKRequest *req in _requestArray) {
        [req stop];
    }
    // 调用批量请求失败的代理
    if ([_delegate respondsToSelector:@selector(batchRequestFailed:)]) {
        [_delegate batchRequestFailed:self];
    }
    // 调用请求失败的block
    if (_failureCompletionBlock) {
        _failureCompletionBlock(self);
    }
    // 清空成功和失败的block
    [self clearCompletionBlock];
    // 调用请求结束的代理
    [self toggleAccessoriesDidStopCallBack];
    // 从YTKBatchRequestAgent里移除当前的batch
    [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
}

十.链式请求

和批量请求类似,处理链式请求的类是YTKChainRequest,并且用YTKChainRequestAgent单例来管理YTKChainRequest的实例

init

初始化方法。

- (instancetype)init {
    self = [super init];
    if (self) {
        // 下一个请求的index
        _nextRequestIndex = 0;
        // 保存链式请求的数组
        _requestArray = [NSMutableArray array];
        // 保存回调的数组
        _requestCallbackArray = [NSMutableArray array];
        // 空回调,用来填充用户没有定义的回调block
        _emptyCallback = ^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) {
            // do nothing
        };
    }
    return self;
}
addRequest

添加request的接口。

- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback {
    // 添加当前请求进入请求数组
    [_requestArray addObject:request];
    // 传入回调Block时
    if (callback != nil) {
        // 回调block存入回调数组
        [_requestCallbackArray addObject:callback];
    } else {
        // 之前之所以创建一个空的block回调是为了在用户不传入回调时对应传入创建的空回调,保持添加的请求和回调个数一致一一对应。
        [_requestCallbackArray addObject:_emptyCallback];
    }
}
start

请求的发起。

- (void)start {
    // 如果第1个请求已经结束,就不再重复start了。nextRequestIndex为0,请求重头开始。
    if (_nextRequestIndex > 0) {
        YTKLog(@"Error! Chain request has already started.");
        return;
    }

    // //如果请求队列数组里面还有request,则取出并start
    if ([_requestArray count] > 0) {
        // 将要开始回调
        [self toggleAccessoriesWillStartCallBack];
        // 开始下一个请求
        [self startNextRequest];
        // 在YTKChainRequestAgent请求数组中加入当前链式请求(即可以有多个链式请求同时存在)
        [[YTKChainRequestAgent sharedAgent] addChainRequest:self];
    } else {
        YTKLog(@"Error! Chain request array is empty.");
    }
}

- (BOOL)startNextRequest {
    // 判断不超出数组
    if (_nextRequestIndex < [_requestArray count]) {
        // 取出request
        YTKBaseRequest *request = _requestArray[_nextRequestIndex];
        // index+1
        _nextRequestIndex++;
        // 指定代理
        request.delegate = self;
        // 清除block
        [request clearCompletionBlock];
        // 开始请求
        [request start];
        return YES;
    } else {
        return NO;
    }
}

所以和批量请求不同的是,链式请求的请求队列是可以变动的,用户可以无限制地添加请求。只要请求队列里面有请求存在,则YTKChainRequest就会继续发送它们。

成功回调
- (void)requestFinished:(YTKBaseRequest *)request {
    // 获取当前回调
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
    // 当前请求回调
    callback(self, request);
    //如果没有下一个请求了
    if (![self startNextRequest]) {
        // 链式请求将要结束
        [self toggleAccessoriesWillStopCallBack];
        // 链式请求代理回调
        if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
            [_delegate chainRequestFinished:self];
            // 从YTKChainRequestAgent列表中移除当前链式请求
            [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
        }
        // 链式请求结束
        [self toggleAccessoriesDidStopCallBack];
    }
}
失败回调
- (void)requestFailed:(YTKBaseRequest *)request {
    //如果当前 chain里的某个request失败了,则判定当前chain失败。调用当前chain失败的回调
    [self toggleAccessoriesWillStopCallBack];
    if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
        [_delegate chainRequestFailed:self failedBaseRequest:request];
        [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
    }
    [self toggleAccessoriesDidStopCallBack];
}

stop
- (void)stop {
    //首先调用即将停止的callback
    [self toggleAccessoriesWillStopCallBack];
    //然后stop当前的请求,再清空chain里所有的请求和回掉block
    [self clearRequest];
    //在YTKChainRequestAgent里移除当前的chain
    [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
    //最后调用已经结束的callback
    [self toggleAccessoriesDidStopCallBack];
}

- (void)clearRequest {
    //获取当前请求的index
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    if (currentRequestIndex < [_requestArray count]) {
        YTKBaseRequest *request = _requestArray[currentRequestIndex];
        // 停止当前请求
        [request stop];
    }
    //清空请求数组
    [_requestArray removeAllObjects];
    //请求回调数组
    [_requestCallbackArray removeAllObjects];
}

七.封装使用

你可能感兴趣的:(iOS网络编程(YTKNetwork))