HTTP 的请求和响应,都需要配置好响应的格式才能正常进行。比如,一个请求中,就包含了请求行、请求头、请求体三个部分,同样一个响应,也包含了响应行、响应头与响应体,而序列化的目的就是如何定义描述这些内容的。
AFNetworking (以下简称 AF )序列化分为两大类:RequestSerialization 和 ResponseSerialization 。分别代表了请求序列化和响应序列化。
1. RequestSerialization
AF 的 RequestSerialization 所有的内容在 AFURLRequestSerialization.h
和 AFURLRequestSerialization.m
文件中。通篇下来,包含了几个协议、类。他们的名称和关系如下图:
HTTP 中的请求中,对于请求体的内容编码格式有三种:
- application/x-www-urlencoded
- multipart/form-data
- text-plain
默认情况下,请求的编码是 application/x-www-urlencoded
,当使用 POST 进行请求时,数据会被以 x-www-urlencoded
方式编码到 Body 中来传送,而如果 GET 请求,则是附在 url 链接后面来发送。
multipart/form-data
仅仅用在 POST 中,方便大文件的传输,在请求头中需要注明content-type
为multipart/form-data
。一般的上传我们使用这个格式编码居多。
text-plain
则表示用普通文本的方式进行格式编码。
上图中,通用方式表示一般的请求数据结构。如果是对 POST 有 multipart/form-data
数据格式的需求(比如上传文件),需要用到右边的一些协议和类。
1.1 AFURLRequestSerialization
AFURLRequestSerialization 是一个协议。它对指定HTTP请求的参数进行编码。请求序列化操作可以将参数编码为查询字符串、HTTP主体,根据需要设置适当的HTTP头部字段。
它仅仅定义了一个协议方法:
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
该方法接受一个 NSURLRequest 以及请求的参数 parameters。(parameters 的类型之所以设置为ID,是因为继承该协议之后的实体,可以自定义参数类型。)
__autoreleasing:将对象赋值给附有
__autoreleasing
修饰符的变量等同于ARC 无效时调用对象的autorelease
方法。方法中 error 是一个二级指针,ARC下,并不能监听二级指针的 release,也即是,如果一个二级指针没有被正确的释放处理,他可能会成为一个野指针!上述的 error 就是此例,__autoreleasing
的做法是将其放入自动释放池,尽管 ARC 没有自动释放它,但是在 releasePool 中能保证释放。NS_SWIFT_NOTHROW: Swift 中的错误处理和 OC 并不相同,主要体现在,Swift 不会使用类似的 NSError 的二级指针,它采用向上抛出异常的方式。该字段表示,这个方法在 Swift 中不会抛出异常(实质是因为,错误会在语言设计方面处理了,这个里面是不会有异常的)。
1.2 AFHTTPRequestSerializer
AFHTTPRequestSerializer 是 AF 对于 Http 请求的基础类。他定义了主要的请求序列化的主要内容。一般情况下,我们会使用的二进制进行数据的传输 NSData,也即是最基本的数据格式。如果我需要编码为其他的格式,比如 JSON 或者 XML ,可以定义成它的子类,覆写 AFURLRequestSerialization
协议的- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error
方法,在内部对parameters 数据格式转换即可实现 JSON 或者 XML 的请求序列化。
AFHTTPRequestSerializer 的属性
在看这个协议方法实现之前,先看下 AFHTTPRequestSerializer 的属性。
特别的,AF 中对其中的六个属性进行了 KVO 监听。目的是为了能够统一的这个六个属性进行读写监听。监听的过程中,使用了一个属 AFHTTPRequestSerializerObservedKeyPaths
保存这些属性,它是一个 NSSet,如果设置为 nil 的属性,将会被移出容器。设置好的容器,会在下一次创建请求的过程中将这些属性设置到每一个请求中去。
AFHTTPRequestSerializer 的方法
- 初始化
AF 的初始化方法中,会将所有拥有默认的值的属性设置成默认值,这种方式值得我们借鉴。在头文件中,已经写明的只有一个类方法:
+ (instancetype)serializer;
+ (instancetype)serializer {
return [[self alloc] init];
}
当然我们也可以直接使用正常的构造方法
[[AFHTTPRequestSerializer alloc] init];
来创建一个对象,因为在其内部其实已经覆写了 init 构造方法。
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 设置默认文字编码格式
self.stringEncoding = NSUTF8StringEncoding;
// 初始化 mutableHTTPRequestHeaders , 这个字典将用来保存请求头的字段。
self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
// 开辟一个队列,用于请求头的内容的设置和获取时的线程安全。
self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);
// 设置请求头中的 Accept-Language 字段的默认值。 这里 AF 利用最近用户使用过的语言的时间作为优先级来排序,将最多六个语言设置为 Accept-Language 对默认值。
// Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
NSMutableArray *acceptLanguagesComponents = [NSMutableArray array];
[[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
float q = 1.0f - (idx * 0.1f);
[acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]];
*stop = q <= 0.5f;
}];
[self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"];
// 设置字段 "User-Agent" 默认的值。这会根据不同的平台来设置。 其中,在 iOS 平台上,是一个集 kCFBundleExecutableKey、kCFBundleIdentifierKey、kCFBundleVersionKey、currentDevice、systemVersion、mainScreen.scale 的字符串。
NSString *userAgent = nil;
#if TARGET_OS_IOS
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif TARGET_OS_WATCH
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
// 在拼接完字符之后,对字符传进行过滤和转化。 CFStringTransform 很有效的转化函数。
// https://www.jianshu.com/p/c03402203ae7 参考
if (userAgent) {
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
[self setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
// 设置将请求参数编码到连接的请求方式的集合
// HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
// 对指定的请求信息进行监听。
self.mutableObservedChangedKeyPaths = [NSMutableSet set];
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
}
}
return self;
}
其中用到两个比较有效的技巧。一个是根据用户手机的 NSLocale (本地设置)来获取用户近期使用过的语言,并根据时间的前后顺序设置一个优先级(默认最高优先级显示当前使用的语言)。另一个是,使用 CFStringTransform
对文字的编码进行转换,像 AF 中就对获取的文字进行过滤和转换:
CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)
- Any-Latin : 将任意字符装成拉丁
- Latin-ASCII : 将拉丁文字转换成 ASCII
- [:^ASCII:] Remove : 删除 ASCII 码中的特殊字符
CFStringTransform很强大,如果要了解详情可以查看: CFStringTransform,这是 Mattt 大神亲自写的。
- 其他方法
暴露在外的方法中,还有一个设置和获取请求头的字段的一对方法。这一对方法实际改动的是在类实现内部的一个可变的字典,最终获取是通过类对外暴露的HTTPRequestHeaders
来获取。
/**
设置请求头中的某个已经存在的字段的值,如果设置的值为 nil。 那么将会移除字段
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
/**
获取一个指定的请求头的字段的值。 如果字段不存在,则返回 nil。
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
/**
请求头的 Authorization 字段值设置,它的值内容为 base + base64b编码内容
*/
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
/**
清空 Authorization 字段的内容。
*/
- (void)clearAuthorizationHeader;
除此之外,AF 还提供了一个自定义编码请求参数的功能。它主要体现在以下两个函数:
/**
设置请求参数的编码类型。目前 AF 只提供了一种类型,寂寞人类型AFHTTPRequestQueryStringDefaultStyle
*/
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
/**
通过设置 block ,兼容开发者自己的请求参数编码方式。
*/
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
这两个方法的目的是提供对请求的参数进行编码的渠道。在 AF 提供的默认方法中,在 HTTPMethodsEncodingParametersInURI
包含的请求方式中,我们会将参数并入到 URL 当中;而在 POST 请求中,则会把参数放在请求体。AF 提供了一个 block 让我们自己可以选择使用怎样的方式进行编码。
AFURLRequestSerialization 协议方法
AFHTTPRequestSerializer 遵循了 AFURLRequestSerialization
协议。下面是实现协议的代码,有点长,我在代码中注释说明。
#pragma mark - AFURLRequestSerialization
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
// request为空,直接中断程序
NSParameterAssert(request);
/* HTTPRequestHeaders 可以让调用者个设置。(这个调用者实际上就是 AF 内部中的其他类,我们也可以通过设置 HTTPRequestHeaders 的方式来自定义,如果不自定义,那么将会使用默认的请求头)
将请求头 HTTPRequestHeaders 中的参数逐个转换成字典,并将所有的字典保存在一个数组中
*/
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
/*
检验请求原文。 请求原文是用户调用者(这里的调用者一般主要是我们的使用 AF 的开发者)的请求附带参数。
*/
NSString *query = nil;
if (parameters) {
// queryStringSerialization 是一个block,这个 block 将提供一个给我外部调用者一个自定义参数教研。如果我们设置了这个 block 才会使用。不然会使用 AF 自带的方式。
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
// 自带的请求参数处理。 这个处理过程包含了对字符的百分号字符转化、无效字符的删除字符等操作。具体的实现后文描述。
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
// HTTPMethodsEncodingParametersInURI 表示一系列将参数放在链接后尾的请求方式。比如我我们说的 GET。
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query && query.length > 0) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
// x-www-form-urlencoded 是默认的 URL 编码,直接将 HTTP 请求体的内容按照设定的字符编码格式填充。
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
可以看到,基本上,在这个序列化方法中,已经将一个请求的需要序列化的东西全都涵盖进去了。当然,主要是针对 x-www-form-urlencoded
方式。
实际上除了这个方法之外,我们更容易用到的应该是对这个方法的一个封装的方法:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
这个方法比之上面的协议方法,增加了对 AFHTTPRequestSerializerObservedKeyPaths
的监听过程。
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
之前设置的这些属性,全都会被一一设置到一个具体的请求当中。
更多的私有方法
AF 的请求序列化中,有很多个私有方法,这些方法有的很有技巧,值得我们去借鉴。这挑选有个又代表性的说下。
-
百分号化符号私有化方法
NSString * AFPercentEscapedStringFromString(NSString *string) {
// 先提取了不需要转换的字符
static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
// 获取 URL 允许请求的字符集
NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
// 将不需要转换的字符集从 URL 允许的字符集中删除。
[allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];
// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
// 定义一个空的字符串,用于保存最后结果。 因此,如果下放所有的转化都失败了,或者传入的参数 string 本身为空,那么将返回一个空字符串。
static NSUInteger const batchSize = 50;
NSUInteger index = 0;
NSMutableString *escaped = @"".mutableCopy;
// 批处理
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;
}
这是一个将字符串百分号符号化的方法。 所谓的百分号符号化,是因为,在 URL 中,可能包含了很多特殊字符,比如::# [ ] @
。这些字符不能被正确的识别,所以应该使用百分号符号的方式进行转换。
PS:因为不需要对
/
和?
进行转换,因此叫做百分号符号?
- 这种字符串跟普通的字符创并不一样,实际上是由多个字符拼接而成,因此,转换的时候需要对这个表情做处理,而不是的单个字符。
rangeOfComposedCharacterSequencesForRange
便是由此而用。 -
stringByAddingPercentEncodingWithAllowedCharacters
用于对字符进行百分号符号化。
-
请求参数编码
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
return [mutablePairs componentsJoinedByString:@"&"];
}
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];
if ([value isKindOfClass:[NSDictionary class]]) {
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];
if (nestedValue) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}
}
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray *array = value;
for (id nestedValue in array) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}
} else if ([value isKindOfClass:[NSSet class]]) {
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;
}
这个方法其实很简单,当我们对这个 AFQueryStringFromParameters
传入一个字典参数的时候,会将参数编码成以&
为分割的键值对,如:key1=value1&key2=value2
。
1.2 AFMultipartFormData 协议
这个协议是为了定义POST 请求使用 Multipart/form-data
格式上传文件时提供了API。
Multipart/form-data
格式的特点是,请求头中,必须包含 Content-Type
,且该请求头字段对应的值是Multipart/form-data
,如:'Content-Type': 'multipart/form-data'
。
以下是一个案例:
--${bound}
Content-Disposition: form-data; name="Filename"
HTTP.pdf
--${bound}
Content-Disposition: form-data; name="file000"; filename="HTTP协议详解.pdf"
Content-Type: application/octet-stream
%PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload"
Submit Query
--${bound}--
--${bound} 为分隔符,如果一次性上传不止一个内容,则应该使用分隔符来隔开。
--${bound}-- 用于结尾
AFMultipartFormData 协议的作用则是用于拼接每一个被隔开的小部分。所以它提供了一系列的 API :
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * _Nullable __autoreleasing *)error;
- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream
name:(NSString *)name
fileName:(NSString *)fileName
length:(int64_t)length
mimeType:(NSString *)mimeType;
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType;
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name;
//
- (void)appendPartWithHeaders:(nullable NSDictionary *)headers
body:(NSData *)body;
以上几个函数都是协议提供的拼接函数。实际上,任何一个有此特征的实例都可以遵循这个协议。后面将在具体的实例中,展示如何使用这个协议的。
我们通过HTTP请求发送数据的时候,实际上数据是以Packet的形式存在于一个Send Buffer中的,应用层平时感知不到这个Buffer的存在。TCP提供可靠的传输,在弱网环境下,一个Packet一次传输失败的概率会升高,即使一次失败,TCP并不会马上认为请求失败了,而是会继续重试一段时间,同时TCP还保证Packet的有序传输,意味着前面的Packet如果不被ack,后面的Packet就会继续等待,如果我们一次往Send Buffer中写入大量的数据,那么在弱网环境下,排在后面的Packet失败的概率会变高,也就意味着我们HTTP请求失败的几率会变大。AF 中,对此也保留了相关的设置接口。通过改变 PacketSize
的大小溶解阻塞时间,并设置了延时的时间,减低阻塞频率。 实现在弱网的情况下虽慢却快
的效果。
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
AFStreamingMultipartFormData 实体
AFStreamingMultipartFormData 是 AF 对于AFMultipartFormData 协议的实现者。它的作用是解决在 AF 中对于Multipart/form-data
格式的序列化,同时兼具流的控制。
AFStreamingMultipartFormData
-
AFStreamingMultipartFormData 对
Multipart/form-data
格式的序列化
AFStreamingMultipartFormData 对Multipart/form-data
格式的序列化只用在AFHTTPRequestSerializer 类的一个特殊方法中:
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id formData))block
error:(NSError *__autoreleasing *)error;
这个方法从字面的意思即可知道,适用于 multipartFormData 数据格式。所以重点讲一将这个方法。 multipartFormData 本身也是一种请求,只不过它的格式不一样,所以在方法的开始,就是使用以下代码初始化一个mutableRequest
。
// 先排除 GET和HEAD请求方式
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
// 初始化一个请求对象
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
这个方法的具体实现,我们在上面已经介绍过了,就是创建一个普通的请求。
然后通过下面的代码引入 AFStreamingMultipartFormData :
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc]
initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
然后遍历所有的参数,将参数抽相为一个个AFQueryStringPair对象,用于拼接使用。
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
// 如果我们需要对组装好的请求体添加自定义内容,可通过这个 block 传入。
if (block) {
block(formData);
}
// 以 --XXX-- 结尾
return [formData requestByFinalizingMultipartFormData];
每一个 fromData
的部分都使用 AFHTTPBodyPart
进行抽象,因此每个 AFHTTPBodyPart
都包含了分割符合自己保有的请求头,如果只考虑上传这个功能的话,AFStreamingMultipartFormData 只需要直接使用 AFHTTPBodyPart
组合的数据接口。但是为了能够切实的知道,每一次上传的过程变化, AF 引入了流。因此并没有直接 AFHTTPBodyPart 组合的数据,而是使用了一个 AFMultipartBodyStream
类来封装流和 AFHTTPBodyPart
,AFStreamingMultipartFormData 则进一步在 AFMultipartBodyStream
之上进行封装。
-
AFStreamingMultipartFormData 流的使用:AFMultipartBodyStream
AFMultipartBodyStream 是 NSInputSteam 的一个子类。NSInputSteam 用于上传时候的输入,我们将服务器的地址作为目的地,将本地源文件目的地作为起点,将数据传输以流的形式进行监听,以获取实时的上传进度和上传状态。这就是 AFMultipartBodyStream 要做的工作。
AFMultipartBodyStream 有一套代理,通过代理就能获取流的实时状态,这是我们知道上传进度的关键原因。
这里发现,AF 有一个比较有技巧性的操作, 为了能够重写系统NSStream
的某个属性,可以直接将 NSSteam 的接口重写一遍,并在重写的实现中覆写这个属性,但是缺点是,这个重写的接口并不能用在文件之外,只能在局部使用,尽管如此,我们依然能够借助这样的方式,实现属性的覆盖或者添加而并不需要建立子类。具体的代码如下:
上图中,
NSString
通过覆写接口添加了两个属性,而
NSStream
通过覆写获取到属性,并能对属性进行更改(比如修饰等)。
2. ResponseSerialization
ResponseSerialization 响应序列化和请求序列化实现的逻辑是一样的。但是相比之下,响应序列化没有了类似 Multipart/form-data
格式的区分,会显得更加简单。我同样使用一副图来描述。
同样的定义一个协议,然后构建实例来遵循协议,实际适用则用到具体的响应格式,比如JSON
或者XML
,则可以通过实现子类的的方式来继承实例和获得协议的定义的接口。
2.1 AFURLResponseSerialization 协议
AFURLResponseSerialization 协议定义了一个方法,这个方法就是我们使用过程中最基本的方法,子类通过实现这个方法,对response
进行对应的格式化,最终获取的是我们想要的格式的内容。
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
2.2 AFHTTPResponseSerializer
AFHTTPResponseSerializer 的属性不多,只有三个。
/**
设定允许的 HTTP 状态码。
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
*/
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;
/**
用于设定能够接受的 MIME 类型. 带有 MIME 类型的 `Content-Type` 字段,不会加入到这其中,因为这在验证的时候发生错误。
*/
@property (nonatomic, copy, nullable) NSSet *acceptableContentTypes;
/**
编码格式,但是整个 AFHTTPResponseSerializer 不会使用到这个属性。
*/
@property (nonatomic, assign) NSStringEncoding stringEncoding DEPRECATED_MSG_ATTRIBUTE("The string encoding is never used. AFHTTPResponseSerializer only validates status codes and content types but does not try to decode the received data in any way.");
AFHTTPResponseSerializer 提供了一个检测response
是否有效的函数。这是它核心函数。我在代码中的注释讲解。
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError * __autoreleasing *)error
{
// 定义量个变量, 用于下面的合法性判断保存值。
BOOL responseIsValid = YES;
NSError *validationError = nil;
// 返回类型必须是 NSHTTPURLResponse
if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
// 必须带有认可的 MIME 类型。
if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
!([response MIMEType] == nil && [data length] == 0)) {
if ([data length] > 0 && [response URL]) {
// 如果是 content-type 带有 MIME。 报错。
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
}
responseIsValid = NO;
}
// 必须符合认可的 http 状态
if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);
responseIsValid = NO;
}
}
if (error && !responseIsValid) {
*error = validationError;
}
return responseIsValid;
}
这个认证方法,就是判断返回的 response ,是不是有效的 response 。并且对错误的原因进行了指定。
AFHTTPResponseSerializer 仅仅对 response 进行甄别,但是并没有一个返回最终结果的函数。是因为返回的最终数据默认是 NSData
,大部分情况下,我们不能直接使用,因此 AF 定义了几个子类用于实现各个不同的格式转化。在上图中,已经看到了目前支持 6 种不同的数据格式。下面只针对常用的 JSON 进行分析。
PS:AFHTTPResponseSerialize 遵循了NScoping 协议,并对相关的协议方法进行了实现。
2.3 AFJSONResponseSerializer
AFJSONResponseSerializer 继承自 AFHTTPResponseSerializer ,除了拥有 AFHTTPResponseSerializer 的一切之外,另外定义了一下几个方法用户设置该子类特有的信息。
+ (instancetype)serializer {
// 返回一个 JSONReading 序列化内容。
return [self serializerWithReadingOptions:(NSJSONReadingOptions)0];
}
+ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions {
AFJSONResponseSerializer *serializer = [[self alloc] init];
serializer.readingOptions = readingOptions;
return serializer;
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 设定 JOSN 解析的默认支持的 MIME , 如果我们需要支持更多的类型,可以对这个方法进行自定义。或者直接修改 acceptableContentTypes。
self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
return self;
}
另外,AFJSONResponseSerializer
还有一个属性 removesKeysWithNullValues
:
// 默认为NO, 如果设置为YES, 将会删除 JSON 数据中的空值。
@property (nonatomic, assign) BOOL removesKeysWithNullValues;
接下来是 AFHTTPResponseSerializer
每个子类的核心方法,也即是AFURLResponseSerialization
唯一的协议方法的实现。其中 AFJSONResponseSerializer
中实现如下:
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
// 在转化数据格式之前,先检测的 response 合法性 和 data 的有效性。
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
// data 数据为 nil 时,直接返回nil。 或者在某些情况(Safari中),data 解析为 @“ ”。 这时候实际也是数据为空。
// Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
// See https://github.com/rails/rails/issues/1742
BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
if (data.length == 0 || isSpace) {
return nil;
}
// 保存错误的变量
NSError *serializationError = nil;
// 使用 NSJSONSerialization 进行序列化。
id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
if (!responseObject)
{
if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
}
return nil;
}
// 如果设置 removesKeysWithNullValues 为YES,将会将 JSON 数据中为空的字段删除。
if (self.removesKeysWithNullValues) {
// 详情可以查看 AFJSONObjectByRemovingKeysWithNullValues ,比较简单
return AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}
return responseObject;
}
AFJSONResponseSerializer
的实现是一个代表,更多的其他子类的实现,有兴趣的同学可以自行查看。
3. 最后
对于 AF 序列化阅读,除了吸取作者的一些设计方案之外,还能督促我们了解更多的有关 Http 的相关知识。所以,不管是出于什么目的去阅读源码,都对开发者受益匪浅。最后感谢开源!
参考:
iOS-使用CFStringTransform汉字转拼音
iOS122-移动混合开发研究院
CFStringTransform
iOS中流(NSStream)的使用