本篇是 AFNetworking 源码解析的第二批,主要讲解如何生成请求 request。请求的序列化过程是非常重要的一个过程,涉及到多种处理和数据结构。自顶向下分析难度较大,因此本篇将先分析最底层的类。
涉及到的类
AFQueryStringPair
该类用于保存请求要发送的参数,从他的头文件中的属性可以看出:
1 | @interface AFQueryStringPair : NSObject |
field
和 value
就代表着请求参数的键值对。它有一个方法,用于将参数转化为发送请求时的字符串样式以等号连接。它可以在 get 请求时被拼接到 url 中也可以在 post 请求时作为请求体放到 body 中:
1 | /// field value 用 = 连接 |
其中涉及到了一个方法 AFPercentEscapedStringFromString()
,它被用来将字符串编码。在 url 中,只能存在数字字母以及 -_.~
四个特殊符号,其他的符号都需要通过百分号转码,比如 !
要被转码为 %21
,=
要被转码为 %3D
。
AFPercentEscapedStringFromString()
会将字符串分割成数个小端,每个小端分别进行编码。编码的工作主要使用的是 NSString 的 stringByAddingPercentEncodingWithAllowedCharacters:
方法。还需要注意的是,对于 emoji 表情,一个 emoji 表情将占用两个字符的大小。因此,在分割字符串的时候,还需要对 emoji 在边界的情况做一个处理,防止劈开 emoji 表情:
1 |
|
AFHTTPBodyPart
一般请求不会用到这个类,当请求类型为 multipart/form-data 的时候,会创建多个 AFHTTPBodyPart
对象,每一个对象会有一个 NSInputStream
输入流,在生成请求的时候导入到 request 中。
头文件
AFHTTPBodyPart
保存了 HTTP body的全部的信息。来看它的头部信息:
1 | @interface AFHTTPBodyPart : NSObject |
它的类拓展中还提供了几个属性作用于读取数据阶段:
1 | @interface AFHTTPBodyPart () <NSCopying> { |
其中枚举类型为。将在下面讲解方法的时候看到:
1 | typedef enum { |
方法
inputStream
接着依次看 AFHTTPBodyPart
的方法。首先是 inputStream
的 get 方法:
1 | /// inputStream 的 get 方法 |
它会把 body
转化为 NSInputStream
,无论 body
原本是 NSData
类型,NSURL
类型还是 NSInputStream
类型。
stringForHeaders
stringForHeaders
方法将 headers
字典转化为字符串:
1 | /// 根据 self 的 header 字典属性来拼接 header 字符串 |
这里的 header 不是 request header,而是 request body 中的 header。下面会有举例
contentLength
contentLength
用于计算 HTTP body 的整体长度。body 包含四个部分,分别对应于上面的 AFHTTPBodyPartReadPhase
枚举:
1 | /// 总体的长度 |
这里涉及到以下四个方法:
1 | /// 生成随机 boundary 字符串 |
用来生成不同情况下的 boundary。具体作用将在 read:maxLength:
讲解。
hasBytesAvailable
AFHTTPBodyPart
中有一个输入流 NSInputStream
实例,这个方法就是用来查看输入流中是否还有可用数据的。根据输入流的状态判断:
1 | /// 是否还有数据可读 |
transitionToNextPhase
这里用到了上面的枚举对象。根据当前所处阶段,将阶段设置为下一阶段:
1 | /// 转移到下一阶段 |
read:maxLength:
这是 AFHTTPBodyPart
的核心方法。AFHTTPBodyPart
封装了 multipart/form-data
情况下 body 中的部分数据。那么在发送的时候一定要把它读出来吧。read:maxLength:
的目的就是如此,将数据放入 buffer
中:
1 | /// 读取 buffer |
这个方法中有涉及到了边界相关的几个方法。当 http 请求 type 为 multipart/form-data
时,它会将表单中的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来表名文件类型:content-disposition
,用来说明字段的一些信息。如:
1 | --boundary+004563210AB32145 |
每两个 boundary 之间的数据就是一个
AFHTTPBodyPart
实例。也就是read:maxLength:
读取生成的 buffer 的数据
--boundary+004563210AB32145
就是由上面的四个方法生成的。针对 boundary 所处的不同位置,会有不同的添加换行符的规则。
AFMultipartBodyStream
之前 AFHTTPBodyPart
是 form-data 下 body 中的各个部分。现在的 AFMutipartBodyStream
则是 AFHTTPBodyPart
的集合。
头文件
1 | @interface AFMultipartBodyStream : NSInputStream <NSStreamDelegate> |
可以发现 AFMutipartBodyStream
是继承于 NSInputStream
的。我们需要清楚的是,HTTPBodyParts
是以一个数组的形式存在于它的属性中。
方法
setInitialAndFinalBoundaries
由于它内内存存在一个 AFHTTPBodyPart
的数组,这个方法就是将数组中第一个设置为存在初始边界,最后一个存在末尾边界,中间的则是普通边界的情况:
1 | /// 一个 AFMultipartBodyStream 中有多个 HTTPBodyParts,最前面的设置 initialBoundary 最后面的设置 finalBoundary |
appendHTTPBodyPart:
既然有一个数组,那么数组元素当然要一个个添加进来啦。这个方法就是用来添加元素的:
1 | /// 增加HTTPBodyPart实例到数组中 |
read:maxLength:
这个方法在 AFHTTPBodyPart
中也有,其实他是 NSInputStream
的方法,在 AFMultipartBodyStream
中被重写了:
1 | /// 读取 HTTPBodyPart 中的数据到 buffer 中 |
简而言之,就是调用每一个 AFHTTPBodyPart
实例的相应方法,把他们的所有 buffer 汇集在一起。
buffer 读完之后有什么用,后面再说
contentLength
将所有 AFHTTPBodyPart
的长度都累加起来成为最终的 content-length
1 | - (unsigned long long)contentLength { |
开闭 NSInputStream
这也是 NSInputStream
中的方法,在读取 buffer 前后要分别 open 以及 close
1 | /// 重写 NSInputStream 的方法 |
AFStreamingMultipartFormData
前面 AFMultipartBodyStream
包含了多个 AFHTTPBodyPart
实例,且可以获取到所有的 buffer,他们准确的说就是model类,没有更多的操作。而此处的 AFStreamingMultipartFormData
则更像是一种 manager,可以管理 AFMultipartBodyStream
。
头文件
1 | @interface AFStreamingMultipartFormData () |
头文件中主需要关注两个属性,一个是 NSMutableURLRequest
实例,还有一个是 SFMultipartBodyStream
实例。它将请求和输入流封装了起来。
方法
初始化
初始化方法很简单,就是对头文件中属性的设置。它要传入一个 NSMutableURLRequest
实例。
1 | - (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest |
通过文件路径增加 AFHTTPBodyPart
如果要上传一个本地文件,那么就把本地文件的路径设置给新创建的 SFHTTPBodyPart
实例中。
1 | - (BOOL)appendPartWithFileURL:(NSURL *)fileURL |
通过输入流增加 AFHTTPBodyPart
把传入的输入流作为 AFHTTPBodyPart 的 body 保存
1 | /// 把 inputStream 封装为 AFHTTPBodyPart,保存 |
通过 NSData 增加 AFHTTPBodyPart
如果直接传入了 NSData,那么直接把他保存到 AFHTTPBodyPart
的 body 中:
1 | - (void)appendPartWithFileData:(NSData *)data |
返回 NSMutableURLRequest 实例
前面提到过,读取的 buffer 有什么用呢?答案就在这个方法中:
1 | - (NSMutableURLRequest *)requestByFinalizingMultipartFormData { |
NSMutableURLRequest
有一个属性是 HTTPBodyStream
,它是 NSInputStream
类型,在生成请求的时候会自动调用其 HTTPBodyStream
实例的 read:maxLength:
方法。获取 buffer 数据写入请求体中。
NSMutableURLRequest
有两个属性用来设置请求体:
NSInputStream
类型的HTTPBodyStream
,也就是上面说的,是以数据流的形式生成请求体,需要实现NSInputStream
的read:maxLength:
方法,返回 buffer。主要使用在content-type
为multipart/form-data
的情况。NSData
类型的HTTPBody
,直接将NSData
放入请求中。主要用在非multipart/form-data
的情况,包括application/json
,application/x-www-form-urlencoded
等
AFHTTPRequestSerializer
初始化
初始化方法比较长。做了以下几件事:
- 创建了一个并行队列,用于实现读写锁
- 设置请求头,包裹 UA 和 Accept-Language
- 给部分属性添加 KVO
1 | - (instancetype)init { |
添加 KVO
添加 KVO 的属性由以下方法提供:
1 |
|
包括 是否允许使用蜂窝网络,缓存策略,是否使用 cookies,是否使用管线连接,网络服务类型,超时时间。当他们被重新设置后,会触发 KVO 方法:
1 | - (void)observeValueForKeyPath:(NSString *)keyPath |
也就是说,如果改变了就会被加入到 mutableObservedChangedKeyPaths
数组中。这样,在创建 request 的时候就可以通过这个数组获取到变化的信息。
设置头部信息
设置 header 本来就是简单的两个 get set 方法,但是还是要拿出来说一下是因为它通过一个并行队列以及 dispatch_barrier_async
实现了一个读写锁:
1 | - (void)setValue:(NSString *)value |
各种创建 NSMutableURLRequest 的方式
application/x-www-form-urlencoded
application/x-www-form-urlencoded
的 request 将参数拼接到 url 上或者放在 body 中:
1 | /// 生成普通 urlrequest,param 设置到body或者uri上 |
调用 requestBySerializingRequest:withparameters:error:
处理参数:
1 | /// 将 param 拼到 uri 或放在 body |
如果是 get,delete,header 中的一个,那么就会直接把 query 信息拼到 uri 上,其他的都会塞到 body 中。
如果有自定义序列化方法会调用自定义的方法将 param 序列化成字符串。一般情况下我们可能不需要自定义的序列化方法,不过有些时候,比如说想通过 get 请求传递一个数组,服务端有自己的解析逻辑,那么数组如何展开就要通过自定义的序列化方式进行了:
1 | - (void)setQueryStringSerializationWithBlock:(NSString *(^)(NSURLRequest *, id, NSError *__autoreleasing *))block { |
没有就通过默认的方式,即 AFQueryStringFromParametors()
方法:
1 | /// 将传入的 parameters 字典转为数组,数组中的保存的是包含 key value 的 AFQueryStringPair 对象。 |
这种序列化方式是最常见的方式数组以 a[]=xxx&a[]=yyy&a[]=zzz
的形式提交,字典以a[b]=xxx&a[c]=yyy&a[d]=zzz
的方式提交。
不过这种一般以 json 的方式提交会更方便。
multipart/form-data
这种类型的请求上面已经讲了很多了,需要一个 NSInputStream
:
1 | - (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method |
它会把所有的参数都转为 NSData
的形式,然后转为 NSInputStream
,设置给 request。
特殊的 request
1 | - (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request |
这个方法有点特殊,它把 request 的输入流直接连向了本地的输出流。将输入流里的数据,直接写到了本地的 fileURL
中。不太明白其中的用意,以及使用场景。
AFHTTPRequestSerializer 的两个子类
AFJSONRequestSerializer
该类用于发送 application/json
请求。它仅重写了 AFHTTPRequestSerializer
的一个方法:
1 | - (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request |
将传入的参数转为了 JSON 的形式,设置为 request 的 HTTPBody
AFPropertyListRequestSerializer
它和上面的类似,也是重写了同样的方法,用于发送 application/x-plist
请求:
1 | - (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request |
它通过 NSPropertyListSerialization
将参数转为了 plist 的形式。
总结
本篇讲了创建 NSMutableURLRequest
的过程。request 的 content-type
分为四种形式:
- application/x-www-form-urlencoded
- application/json
- application/x-plist
- multipart/form-data
前三种都是将参数以 NSData
的形式设置给 request 的 HTTPBody
,最后一种则是通过 NSInputStream
设置给 request 的 HTTPBodyStream
。
第一种将参数以 a=x&b=y&c=z
的形式连接,需要对生成的字符串转码。
第二种将参数转为 json 再转为 NSData,直接设置给 request 就可以了。
第三种将参数通过 NSPropertyListSerialization
直接转为 NSData,设置给 request
第四种将参数以符合特定规则的 boundary 分割,可以传递文件。用 AFHTTPBodyPart
作为每一个参数的模型,AFMultipartBodyStream
作为输入流,包含所有参数,重写 read:maxLength:
方法,提供 buffer 给 request。AFStreamingMultipartFormData
作为 AFMultipartBodyStream
的管理者,将 AFHTTPBodyPart
设置给它,并且是它和 request 的桥梁。
其中只有第四种方式需要自己手动计算 content-Length
,其他都由框架通过 NSData
自己获取。