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 很有效的轉化函數。
// http://www.lxweimin.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很強大,如果要了解詳情可以查看: CFString?Transform,這是 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 <NSString *, NSString *> *)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 <AFMultipartFormData> 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 <NSString *> *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-移動混合開發研究院
CFString?Transform
iOS中流(NSStream)的使用