1. AFNetworking (1)

本文详细解析了AFNetworking网络请求库的基本使用与内部原理,包括NSURLSession的基础操作、AFHTTPSessionManager的构造及使用、参数处理流程等关键内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

梳理

我们已经知道AF3.0是基于NSURLSession的那么首先在分析AF之前我们先看一下NSURLSession的基础操作

/*
sessionWithConfiguration:delegate:delegateQueue:
注意这里可以设置回调的delegate,但是这个delegate会被强引用。同时我们还可以设置回调在什么队列中进行。
*/
NSURLSession * urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com/"]];
NSURLSessionDataTask * dataTask = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
    NSLog(@"--%@",response);
}];
[dataTask resume];
复制代码

上面将返回我们请求的一段数据。但是在日常编程中这段代码包含了太多不合理的东西。

首先我们很多时候都要求有一个BaseURL存在
我们想尽可能的减少重复的初始化代码
同时有的时候我们更加喜欢灵活的block等方式
而且我们总是要去写[dataTask resume];这种代码。诸如此类,问题很多。

那么就一步步来解决这些问题,我一直认为不管是写还是看代码库都是一步一步来比较好,那么我们从第一个需求开始处理

解决需求

创建一个管理此类信息的类AFHTTPSessionManager创建一条只读属性
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
并创建初始化方法
- (instancetype)initWithBaseURL:(NSURL *)url
在其中只需要简单的进行赋值即可

在这里AF3.0选择了在内部再创建出一个rw属性,这样的好处是在内部可以使用self.赋值走set方法。当然如果像我这种懒癌患者的话,有的时候会选择_直接赋值当然如果有的时候要对set进行截取的时候就会很蛋疼。

但是iOS对于URL有个非常蛋疼的东西,如果是URL的话你不调用absoluteString,打印出来的东西有的时候并不是大多数人期望的东西。为了减少误差,AF在这里进行了处理

if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) 
{
    // 如果长度不为0,且并不是相对URL。
    // 那么就进行一次URL的空append,这样可以减少误差
    url = [url URLByAppendingPathComponent:@""];
}
复制代码

如此当我们再需要进行调用的时候我们就只需要提交后台提供的相对URL就可以通过保有后台baseURL的AFHTTPSessionManager进行访问了,该类就能通过下面的方法获取URLString

NSURLSession * urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString relativeToURL:self.baseURL]];
NSURLSessionDataTask * dataTask = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
    NSLog(@"--%@",response);
}];
[dataTask resume];
复制代码

baseURL问题就算是解决了
我们现在只需要提供一个相对URL然后调用使用baseURL创建出来的AFHTTPSessionManager来访问就行了。但这又在一定的程度上不够开放,所以我们开始用心的思考我们应该怎么设置对外接口。

AFHTTPSessionManager

我发现我们可能会存在自定义的NSURLSessionConfiguration所以我们在原先的- (instancetype)initWithBaseURL:(NSURL *)url基础上拓展出

- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration
复制代码

并遵循commonInit的原则新建一个initWithBaseURL并对新方法调用。如果不存在则制作一个默认的configuration信息,并保存。拿这个必定存在的configuration信息进行session创建,代码看起来就像是这样

if (!configuration) {
    configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
// 创建Configuration
self.sessionConfiguration = configuration;
// 创建一个delegate的队列,最大允许运行数量为1。
// 但是要注意这个东西和GCD的串行队列是不同的,这里要多一种叫依赖的概念。
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;

self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
复制代码

然后NSURLRequest和NSURLSessionDataTask的创建则可以封装到一个方法中返回一个NSURLSessionDataTask对象出来,而对于completionHandler我们进行一点点稍稍的处理。通过对NSError的判断得到一个成功回调和一个失败回调。同时最后返回一个NSURLSessionDataTask对象方便外部控制任务。这样前面的一大段代码就会变成这样

- (NSURLSessionDataTask *)netPot:(NSString *)URLString
                         success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                         failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
    NSURLSessionDataTask *dataTask = [self URLString:URLString success:success failure:failure];
    [dataTask resume];
    return dataTask;
}
复制代码

当然这样只是在我们一开始创建的基础上进行的处理,而在实际情况下我们还有一些东西需要进行定义如:HTTPMethod、parameters、uploadProgress、downloadProgress
所以我们要对netPot:success:failure方法进行一些改造


首先是HTTPMethod和parameter
这两者是和NSURLRequest有关的,但是parameter就需要特殊处理一下。
因为GET等请求parameter都是使用?拼接在URL后的,而POST则是放在请求头中的。
当然两者的参数拼接形式都是一样的。
所以在处理上则是先对参数中的数据先进行拼接然后再视情况不同选择不同的拼接方式。
同时因为只需要传入一个字典就可以了,在参数上没有做任何的限定。不免被传入各种奇怪的数据。
这直接导致关于parameter的处理可以算是一个大工程了,已经可以当做一个模块单独抽离出来,连带着就也把request的处理全部抽离了出去。
而对于request的处理则被抽离成一个类AFHTTPRequestSerializer

AFHTTPRequestSerializer

首先是最简单的请求方式写入以及Request创建

NSURL *url = [NSURL URLWithString:URLString];
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
复制代码

抛开其他全都不谈,我们要拼接的URL是没有明显的多层级关系,所以我们第一件事就是将可能出现多层级的字典转成一个单层级的数组。

/**
 将字典的多层结构弄成一层结构的数组
 
 返回回来的数组装的是一个个实例化好的用来储存键值对的对象
 
 @param dictionary 参数字典
 @return 一层结构的数组
 */
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}


/**
 解析key和value的层次将这些存在多层结构的东西变成一层结构
 
 关于value的处理上只是区分Set、Array、Dictionary 这几个集合类型和别的其他类型
 具体的你的Value是否是自定义的非继承于前面的三种集合的集合类型或其他系统的集合是不进行考虑的
 
 @param key 键值 可能为nil
 @param value 数值 
 @return 解析为单层结构之后的数组
 */
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

    /*
     在这里所有的key的description都是String 使用compare进行升序排序
     */
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    
    if ([value isKindOfClass:[NSDictionary class]]) {
        // 如果是字典类型,那么就对字典的信息进行拆分
        // 如果进来的时候表明这个字典的上方存在key值
        // 那么使用key[newKey]进行递归向下
        // 否则使用newKey直接递归向下
        
        NSDictionary *dictionary = value;
        
        // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
        // 对字典的键值进行排序可以减少在反序列化的时候的问题
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            // 在这里拿出来的如果常规的数据
            // 两个字典中保存的key值是可以找到对应的
            // 但是如果使用的是非常规数据可能就会出现非对应的情况
            if (nestedValue) {
                [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }
    } else if ([value isKindOfClass:[NSArray class]]) {
        // 如果是array
        // 只要不是编写问题这都不是第一层 也就是说必有key
        // 那么直接使用key[]作为key值进行递归向下
        NSArray *array = value;
        for (id nestedValue in array) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
    } else if ([value isKindOfClass:[NSSet class]]) {
        // 如果是set
        // AF在这里并没有把这个当做array处理而是直接使用key进行递归向下
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        // 其他情况直接创建一个键值对储存对象进行保存
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }

    return mutableQueryStringComponents;
}
复制代码

一大堆,其实总的来说在通过这些方法处理之后会把字典中各个层级的数据,在一维数组中带有层级信息的通过一个类AFQueryStringPair保存下来。获取到这个数组之后就使用&将这些键值对的拼装结果链接起来。
前面我们就说过,因为传入的是字典,所以对传入的数据是没有任何的类型限制的。所以难免出现奇怪的数据。而对这些奇怪的数据的兼容则在AFQueryStringPair中进行。

AFQueryStringPair

AFQueryStringPair是一个非常简单的类
只有两个方法:
一个是用来初始化的方法功能简单只是进行两个数值赋值
重点在于第二个用来生成键值对拼装结果的。

/**
![](https://user-gold-cdn.xitu.io/2018/1/17/16103779ff6645c7?w=2056&h=1352&f=png&s=215187)
 对当前类的数据进行拼装

 @return 拼装之后的结果
 */
- (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        // 不管你传进来的到底是什么奇葩数据反正一定会被规则化好为NSString
        // 如果用法实在不对那也是没有办法的事情 
        // 但是数据一定会被传输出去 
        // PS:如果真是要传进来自定义类型只要进行description的实现就可以
        // 这里对于key值进行这样的操作更多还是基于安全性的考虑
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}
复制代码

在这里我突然发现自己的叙述从发展变成了解释。emmmmm
毕竟继续那种风格篇幅实在太恐怖,为了更好的分P抱歉了。

由于数据类型不可控,当description函数调用之后返回的数据无法保证是否存在保留字符,所以在进行储存的时候我们还要进行最后一次处理。

/**
 根据RFC 3986下面的字符为保留字符
    - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
    - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="

 在RFC 3986的3.4中指出?和/是不需要转义的。
 */
NSString * AFPercentEscapedStringFromString(NSString *string) {
    // does not include "?" or "/" due to RFC 3986 - Section 3.4
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; 
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";

    // 这里表示在URL中不需要编码的内容
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    // 移除掉保留字符
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

	// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028

    static NSUInteger const batchSize = 50;

    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    // 这里有个点 在iOS7的时候直接进行的时候会崩溃估计iOS7的时候是使用的length遍历的 但是到了现在不会崩溃了可能进行了修复 但是这段代码却被保留了下来
    // 崩溃原因:存在可能出现的多字符内容如emoji表情 这样情况下如果使用length遍历就会出现将一个文本进行了拆分情况
    while (index < string.length) {
        NSUInteger length = MIN(string.length - index, batchSize);
        NSRange range = NSMakeRange(index, length);

        // To avoid breaking up character sequences such as ????
        // 为了防止将一个字符拆分成两个 调用这个方法就能解决这个问题 他会直接最大
        range = [string rangeOfComposedCharacterSequencesForRange:range];

        NSString *substring = [string substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];

        index += range.length;
    }

	return escaped;
}
复制代码

由于历史原因在iOS7中如果不进行处理直接进行操作是会崩溃的。
据说是SDK在进行转义的时候使用的是length遍历,其实很奇怪就算这样换成子字符串也一样会出现这样的问题啊,毕竟调用那个方法的这道坎没法绕过去。好吧奇怪的问题。
PS:原本出问题的代码是直接对stringByAddingPercentEncodingWithAllowedCharacters进行调用,并没有进行子串拆分,子串拆分和NSRange处理是一起出现的。
这个地方卡住我很久,但是后来我想通了。先就这样吧

如此就将一个可能存在多维的字典转换成了一串串编码好的字符串,然后再通过&将他们连接在一起。
再然后就是对HTTPMethod进行判断,对于直接拼接在URL上的则判断query是否存在,存在则使用&连接否则使用?连接。
同时对于x-www-form-urlencoded情况下,如果参数为空则对HTTPBody放入空字符串的Data。

而关于两个进度的话,我们先简单点,只需要对 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
等代理方法进行处理即可。那么发送请求的代码就变成这样了

- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                                  uploadProgress:(nullable void (^)(CGFloat uploadProgress)) uploadProgress
                                downloadProgress:(nullable void (^)(CGFloat downloadProgress)) downloadProgress
                                         success:(void (^)(NSURLSessionDataTask *, id))success
                                         failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
    // 创建Request
    NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters];

    // 创建Task
    __block NSURLSessionDataTask *dataTask = nil;
    dataTask = [self dataTaskWithRequest:request
                          uploadProgress:uploadProgress
                        downloadProgress:downloadProgress
                       completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
        // 基于error的存在判断是否在连接层面上是否失败
        if (error) {
            if (failure) {
                failure(dataTask, error);
            }
        } else {
            if (success) {
                success(dataTask, responseObject);
            }
        }
    }];
    return dataTask;
}
复制代码

然后我们经常会比较反感字符串的直接输入,尽管这些字符串都相当简单。但是没联想的感觉确实不好,而且参数过多。比如通常我们并不是都需要一个上传进度一个下载进度。所以我们可以在这个基础上创建很多适配的方法,对这个方法进行一定程度的包装。
就下面这些了


尽管AF并没有开放创建Task的基础方法,但是他还是把task的resume分散在多个方法中。

然后我们发现我们每一次请求都需要创建一个SessionManager实例,这就需要我们在AF的基础上再添加一层,使用单例模式获取一个创建好的SessionManager。

static NSString * const AFAppDotNetAPIBaseURLString = @"https://api.app.net/";

@implementation AFAppDotNetAPIClient

+ (instancetype)sharedClient {
    static AFAppDotNetAPIClient *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedClient = [[AFAppDotNetAPIClient alloc] initWithBaseURL:[NSURL URLWithString:AFAppDotNetAPIBaseURLString]];
    });
    
    return _sharedClient;
}

@end
复制代码

至此一个简单的能实现一些基础需求的网络请求模块就创建完毕。调用起来就像这样:

+ (NSURLSessionDataTask *)globalTimelinePostsWithBlock:(void (^)(NSArray *posts, NSError *error))block 
{
    return [[AFAppDotNetAPIClient sharedClient] GET:@"stream/0/posts/stream/global"
                                         parameters:nil
                                           progress:nil
                                            success:^(NSURLSessionDataTask * __unused task, id JSON)
    {
        NSArray *postsFromResponse = [JSON valueForKeyPath:@"data"];
        NSMutableArray *mutablePosts = [NSMutableArray arrayWithCapacity:[postsFromResponse count]];
        for (NSDictionary *attributes in postsFromResponse) {
            Post *post = [[Post alloc] initWithAttributes:attributes];
            [mutablePosts addObject:post];
        }

        if (block) {
            block([NSArray arrayWithArray:mutablePosts], nil);
        }
    } failure:^(NSURLSessionDataTask *__unused task, NSError *error) {
        if (block) {
            block([NSArray array], error);
        }
    }];
}
复制代码

小结

整理一下当前结构关系

当然不可能就这样完结~\(≧▽≦)/~

转载于:https://juejin.im/post/5a5d60516fb9a01cb42c5b5d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值