HTTP 的請求和響應,都需要配置好響應的格式才能正常進行苍姜。比如牢酵,一個請求中,就包含了請求行衙猪、請求頭馍乙、請求體三個部分,同樣一個響應垫释,也包含了響應行丝格、響應頭與響應體,而序列化的目的就是如何定義描述這些內(nèi)容的棵譬。
AFNetworking (以下簡稱 AF )序列化分為兩大類:RequestSerialization 和 ResponseSerialization 显蝌。分別代表了請求序列化和響應序列化。
1. RequestSerialization
AF 的 RequestSerialization 所有的內(nèi)容在 AFURLRequestSerialization.h
和 AFURLRequestSerialization.m
文件中订咸。通篇下來曼尊,包含了幾個協(xié)議酬诀、類。他們的名稱和關(guān)系如下圖:
HTTP 中的請求中骆撇,對于請求體的內(nèi)容編碼格式有三種:
- application/x-www-urlencoded
- multipart/form-data
- text-plain
默認情況下,請求的編碼是 application/x-www-urlencoded
瞒御,當使用 POST 進行請求時,數(shù)據(jù)會被以 x-www-urlencoded
方式編碼到 Body 中來傳送神郊,而如果 GET 請求肴裙,則是附在 url 鏈接后面來發(fā)送。
multipart/form-data
僅僅用在 POST 中涌乳,方便大文件的傳輸蜻懦,在請求頭中需要注明content-type
為multipart/form-data
。一般的上傳我們使用這個格式編碼居多爷怀。
text-plain
則表示用普通文本的方式進行格式編碼阻肩。
上圖中,通用方式表示一般的請求數(shù)據(jù)結(jié)構(gòu)运授。如果是對 POST 有 multipart/form-data
數(shù)據(jù)格式的需求(比如上傳文件)烤惊,需要用到右邊的一些協(xié)議和類。
1.1 AFURLRequestSerialization
AFURLRequestSerialization 是一個協(xié)議吁朦。它對指定HTTP請求的參數(shù)進行編碼柒室。請求序列化操作可以將參數(shù)編碼為查詢字符串、HTTP主體逗宜,根據(jù)需要設置適當?shù)腍TTP頭部字段雄右。
它僅僅定義了一個協(xié)議方法:
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
該方法接受一個 NSURLRequest 以及請求的參數(shù) parameters。(parameters 的類型之所以設置為ID纺讲,是因為繼承該協(xié)議之后的實體擂仍,可以自定義參數(shù)類型。)
__autoreleasing:將對象賦值給附有
__autoreleasing
修飾符的變量等同于ARC 無效時調(diào)用對象的autorelease
方法熬甚。方法中 error 是一個二級指針逢渔,ARC下,并不能監(jiān)聽二級指針的 release乡括,也即是肃廓,如果一個二級指針沒有被正確的釋放處理,他可能會成為一個野指針诲泌!上述的 error 就是此例盲赊,__autoreleasing
的做法是將其放入自動釋放池,盡管 ARC 沒有自動釋放它敷扫,但是在 releasePool 中能保證釋放哀蘑。NS_SWIFT_NOTHROW: Swift 中的錯誤處理和 OC 并不相同,主要體現(xiàn)在,Swift 不會使用類似的 NSError 的二級指針递礼,它采用向上拋出異常的方式惨险。該字段表示,這個方法在 Swift 中不會拋出異常(實質(zhì)是因為脊髓,錯誤會在語言設計方面處理了辫愉,這個里面是不會有異常的)。
1.2 AFHTTPRequestSerializer
AFHTTPRequestSerializer 是 AF 對于 Http 請求的基礎類将硝。他定義了主要的請求序列化的主要內(nèi)容恭朗。一般情況下,我們會使用的二進制進行數(shù)據(jù)的傳輸 NSData依疼,也即是最基本的數(shù)據(jù)格式痰腮。如果我需要編碼為其他的格式,比如 JSON 或者 XML 律罢,可以定義成它的子類膀值,覆寫 AFURLRequestSerialization
協(xié)議的- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error
方法,在內(nèi)部對parameters 數(shù)據(jù)格式轉(zhuǎn)換即可實現(xiàn) JSON 或者 XML 的請求序列化误辑。
AFHTTPRequestSerializer 的屬性
在看這個協(xié)議方法實現(xiàn)之前沧踏,先看下 AFHTTPRequestSerializer 的屬性。
特別的巾钉,AF 中對其中的六個屬性進行了 KVO 監(jiān)聽牵啦。目的是為了能夠統(tǒng)一的這個六個屬性進行讀寫監(jiān)聽洛口。監(jiān)聽的過程中,使用了一個屬 AFHTTPRequestSerializerObservedKeyPaths
保存這些屬性,它是一個 NSSet嚼松,如果設置為 nil 的屬性黄伊,將會被移出容器柱蟀。設置好的容器尚粘,會在下一次創(chuàng)建請求的過程中將這些屬性設置到每一個請求中去卿拴。
AFHTTPRequestSerializer 的方法
- 初始化
AF 的初始化方法中,會將所有擁有默認的值的屬性設置成默認值吼旧,這種方式值得我們借鑒凰锡。在頭文件中,已經(jīng)寫明的只有一個類方法:
+ (instancetype)serializer;
+ (instancetype)serializer {
return [[self alloc] init];
}
當然我們也可以直接使用正常的構(gòu)造方法
[[AFHTTPRequestSerializer alloc] init];
來創(chuàng)建一個對象黍少,因為在其內(nèi)部其實已經(jīng)覆寫了 init 構(gòu)造方法寡夹。
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 設置默認文字編碼格式
self.stringEncoding = NSUTF8StringEncoding;
// 初始化 mutableHTTPRequestHeaders , 這個字典將用來保存請求頭的字段处面。
self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
// 開辟一個隊列厂置,用于請求頭的內(nèi)容的設置和獲取時的線程安全。
self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);
// 設置請求頭中的 Accept-Language 字段的默認值魂角。 這里 AF 利用最近用戶使用過的語言的時間作為優(yōu)先級來排序昵济,將最多六個語言設置為 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" 默認的值。這會根據(jù)不同的平臺來設置访忿。 其中瞧栗,在 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
// 在拼接完字符之后珍语,對字符傳進行過濾和轉(zhuǎn)化锤岸。 CFStringTransform 很有效的轉(zhuǎn)化函數(shù)。
// http://www.reibang.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"];
}
// 設置將請求參數(shù)編碼到連接的請求方式的集合
// HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
// 對指定的請求信息進行監(jiān)聽板乙。
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;
}
其中用到兩個比較有效的技巧是偷。一個是根據(jù)用戶手機的 NSLocale (本地設置)來獲取用戶近期使用過的語言,并根據(jù)時間的前后順序設置一個優(yōu)先級(默認最高優(yōu)先級顯示當前使用的語言)募逞。另一個是蛋铆,使用 CFStringTransform
對文字的編碼進行轉(zhuǎn)換,像 AF 中就對獲取的文字進行過濾和轉(zhuǎn)換:
CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)
- Any-Latin : 將任意字符裝成拉丁
- Latin-ASCII : 將拉丁文字轉(zhuǎn)換成 ASCII
- [:^ASCII:] Remove : 刪除 ASCII 碼中的特殊字符
CFStringTransform很強大凡辱,如果要了解詳情可以查看: CFString?Transform戒职,這是 Mattt 大神親自寫的。
- 其他方法
暴露在外的方法中透乾,還有一個設置和獲取請求頭的字段的一對方法洪燥。這一對方法實際改動的是在類實現(xiàn)內(nèi)部的一個可變的字典,最終獲取是通過類對外暴露的HTTPRequestHeaders
來獲取乳乌。
/**
設置請求頭中的某個已經(jīng)存在的字段的值捧韵,如果設置的值為 nil。 那么將會移除字段
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
/**
獲取一個指定的請求頭的字段的值汉操。 如果字段不存在再来,則返回 nil。
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
/**
請求頭的 Authorization 字段值設置磷瘤,它的值內(nèi)容為 base + base64b編碼內(nèi)容
*/
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
/**
清空 Authorization 字段的內(nèi)容芒篷。
*/
- (void)clearAuthorizationHeader;
除此之外,AF 還提供了一個自定義編碼請求參數(shù)的功能采缚。它主要體現(xiàn)在以下兩個函數(shù):
/**
設置請求參數(shù)的編碼類型针炉。目前 AF 只提供了一種類型,寂寞人類型AFHTTPRequestQueryStringDefaultStyle
*/
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
/**
通過設置 block 扳抽,兼容開發(fā)者自己的請求參數(shù)編碼方式篡帕。
*/
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
這兩個方法的目的是提供對請求的參數(shù)進行編碼的渠道殖侵。在 AF 提供的默認方法中,在 HTTPMethodsEncodingParametersInURI
包含的請求方式中镰烧,我們會將參數(shù)并入到 URL 當中拢军;而在 POST 請求中,則會把參數(shù)放在請求體怔鳖。AF 提供了一個 block 讓我們自己可以選擇使用怎樣的方式進行編碼茉唉。
AFURLRequestSerialization 協(xié)議方法
AFHTTPRequestSerializer 遵循了 AFURLRequestSerialization
協(xié)議。下面是實現(xiàn)協(xié)議的代碼结执,有點長赌渣,我在代碼中注釋說明。
#pragma mark - AFURLRequestSerialization
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
// request為空昌犹,直接中斷程序
NSParameterAssert(request);
/* HTTPRequestHeaders 可以讓調(diào)用者個設置坚芜。(這個調(diào)用者實際上就是 AF 內(nèi)部中的其他類,我們也可以通過設置 HTTPRequestHeaders 的方式來自定義斜姥,如果不自定義鸿竖,那么將會使用默認的請求頭)
將請求頭 HTTPRequestHeaders 中的參數(shù)逐個轉(zhuǎn)換成字典,并將所有的字典保存在一個數(shù)組中
*/
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
/*
檢驗請求原文铸敏。 請求原文是用戶調(diào)用者(這里的調(diào)用者一般主要是我們的使用 AF 的開發(fā)者)的請求附帶參數(shù)缚忧。
*/
NSString *query = nil;
if (parameters) {
// queryStringSerialization 是一個block,這個 block 將提供一個給我外部調(diào)用者一個自定義參數(shù)教研杈笔。如果我們設置了這個 block 才會使用闪水。不然會使用 AF 自帶的方式。
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
// 自帶的請求參數(shù)處理蒙具。 這個處理過程包含了對字符的百分號字符轉(zhuǎn)化球榆、無效字符的刪除字符等操作。具體的實現(xiàn)后文描述禁筏。
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
// HTTPMethodsEncodingParametersInURI 表示一系列將參數(shù)放在鏈接后尾的請求方式持钉。比如我我們說的 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 請求體的內(nèi)容按照設定的字符編碼格式填充每强。
// #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;
}
可以看到,基本上州刽,在這個序列化方法中空执,已經(jīng)將一個請求的需要序列化的東西全都涵蓋進去了。當然穗椅,主要是針對 x-www-form-urlencoded
方式辨绊。
實際上除了這個方法之外,我們更容易用到的應該是對這個方法的一個封裝的方法:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
這個方法比之上面的協(xié)議方法房待,增加了對 AFHTTPRequestSerializerObservedKeyPaths
的監(jiān)聽過程邢羔。
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
之前設置的這些屬性,全都會被一一設置到一個具體的請求當中桑孩。
更多的私有方法
AF 的請求序列化中拜鹤,有很多個私有方法,這些方法有的很有技巧流椒,值得我們?nèi)ソ梃b敏簿。這挑選有個又代表性的說下。
-
百分號化符號私有化方法
NSString * AFPercentEscapedStringFromString(NSString *string) {
// 先提取了不需要轉(zhuǎn)換的字符
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];
// 將不需要轉(zhuǎn)換的字符集從 URL 允許的字符集中刪除宣虾。
[allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];
// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
// 定義一個空的字符串惯裕,用于保存最后結(jié)果。 因此绣硝,如果下放所有的轉(zhuǎn)化都失敗了蜻势,或者傳入的參數(shù) 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 中挠铲,可能包含了很多特殊字符,比如::# [ ] @
寂诱。這些字符不能被正確的識別拂苹,所以應該使用百分號符號的方式進行轉(zhuǎn)換。
PS:因為不需要對
/
和?
進行轉(zhuǎn)換痰洒,因此叫做百分號符號?
- ????????這種字符串跟普通的字符創(chuàng)并不一樣瓢棒,實際上是由多個字符拼接而成,因此丘喻,轉(zhuǎn)換的時候需要對這個表情做處理音羞,而不是的單個字符。
rangeOfComposedCharacterSequencesForRange
便是由此而用仓犬。 -
stringByAddingPercentEncodingWithAllowedCharacters
用于對字符進行百分號符號化嗅绰。
-
請求參數(shù)編碼
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
傳入一個字典參數(shù)的時候搀继,會將參數(shù)編碼成以&
為分割的鍵值對窘面,如:key1=value1&key2=value2
。
1.2 AFMultipartFormData 協(xié)議
這個協(xié)議是為了定義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協(xié)議詳解.pdf"
Content-Type: application/octet-stream
%PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload"
Submit Query
--${bound}--
--${bound} 為分隔符谍夭,如果一次性上傳不止一個內(nèi)容,則應該使用分隔符來隔開憨募。
--${bound}-- 用于結(jié)尾
AFMultipartFormData 協(xié)議的作用則是用于拼接每一個被隔開的小部分紧索。所以它提供了一系列的 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;
以上幾個函數(shù)都是協(xié)議提供的拼接函數(shù)。實際上菜谣,任何一個有此特征的實例都可以遵循這個協(xié)議珠漂。后面將在具體的實例中,展示如何使用這個協(xié)議的尾膊。
我們通過HTTP請求發(fā)送數(shù)據(jù)的時候媳危,實際上數(shù)據(jù)是以Packet的形式存在于一個Send Buffer中的,應用層平時感知不到這個Buffer的存在冈敛。TCP提供可靠的傳輸待笑,在弱網(wǎng)環(huán)境下,一個Packet一次傳輸失敗的概率會升高抓谴,即使一次失敗滋觉,TCP并不會馬上認為請求失敗了,而是會繼續(xù)重試一段時間齐邦,同時TCP還保證Packet的有序傳輸椎侠,意味著前面的Packet如果不被ack,后面的Packet就會繼續(xù)等待措拇,如果我們一次往Send Buffer中寫入大量的數(shù)據(jù)我纪,那么在弱網(wǎng)環(huán)境下,排在后面的Packet失敗的概率會變高丐吓,也就意味著我們HTTP請求失敗的幾率會變大浅悉。AF 中,對此也保留了相關(guān)的設置接口。通過改變 PacketSize
的大小溶解阻塞時間券犁,并設置了延時的時間术健,減低阻塞頻率。 實現(xiàn)在弱網(wǎng)的情況下雖慢卻快
的效果粘衬。
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
AFStreamingMultipartFormData 實體
AFStreamingMultipartFormData 是 AF 對于AFMultipartFormData 協(xié)議的實現(xiàn)者荞估。它的作用是解決在 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 數(shù)據(jù)格式褂删。所以重點講一將這個方法飞醉。 multipartFormData 本身也是一種請求,只不過它的格式不一樣屯阀,所以在方法的開始缅帘,就是使用以下代碼初始化一個mutableRequest
轴术。
// 先排除 GET和HEAD請求方式
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
// 初始化一個請求對象
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
這個方法的具體實現(xiàn),我們在上面已經(jīng)介紹過了钦无,就是創(chuàng)建一個普通的請求逗栽。
然后通過下面的代碼引入 AFStreamingMultipartFormData :
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc]
initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
然后遍歷所有的參數(shù),將參數(shù)抽相為一個個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]];
}
}
}
// 如果我們需要對組裝好的請求體添加自定義內(nèi)容苍凛,可通過這個 block 傳入趣席。
if (block) {
block(formData);
}
// 以 --XXX-- 結(jié)尾
return [formData requestByFinalizingMultipartFormData];
每一個 fromData
的部分都使用 AFHTTPBodyPart
進行抽象,因此每個 AFHTTPBodyPart
都包含了分割符合自己保有的請求頭醇蝴,如果只考慮上傳這個功能的話宣肚,AFStreamingMultipartFormData 只需要直接使用 AFHTTPBodyPart
組合的數(shù)據(jù)接口。但是為了能夠切實的知道悠栓,每一次上傳的過程變化霉涨, AF 引入了流。因此并沒有直接 AFHTTPBodyPart 組合的數(shù)據(jù)惭适,而是使用了一個 AFMultipartBodyStream
類來封裝流和 AFHTTPBodyPart
笙瑟,AFStreamingMultipartFormData 則進一步在 AFMultipartBodyStream
之上進行封裝。
-
AFStreamingMultipartFormData 流的使用:AFMultipartBodyStream
AFMultipartBodyStream 是 NSInputSteam 的一個子類癞志。NSInputSteam 用于上傳時候的輸入往枷,我們將服務器的地址作為目的地,將本地源文件目的地作為起點凄杯,將數(shù)據(jù)傳輸以流的形式進行監(jiān)聽错洁,以獲取實時的上傳進度和上傳狀態(tài)。這就是 AFMultipartBodyStream 要做的工作戒突。
AFMultipartBodyStream 有一套代理屯碴,通過代理就能獲取流的實時狀態(tài),這是我們知道上傳進度的關(guān)鍵原因膊存。
這里發(fā)現(xiàn)导而,AF 有一個比較有技巧性的操作, 為了能夠重寫系統(tǒng)NSStream
的某個屬性,可以直接將 NSSteam 的接口重寫一遍隔崎,并在重寫的實現(xiàn)中覆寫這個屬性嗡载,但是缺點是,這個重寫的接口并不能用在文件之外仍稀,只能在局部使用洼滚,盡管如此,我們依然能夠借助這樣的方式技潘,實現(xiàn)屬性的覆蓋或者添加而并不需要建立子類遥巴。具體的代碼如下:
上圖中千康,
NSString
通過覆寫接口添加了兩個屬性,而NSStream
通過覆寫獲取到屬性铲掐,并能對屬性進行更改(比如修飾等)拾弃。
2. ResponseSerialization
ResponseSerialization 響應序列化和請求序列化實現(xiàn)的邏輯是一樣的。但是相比之下摆霉,響應序列化沒有了類似 Multipart/form-data
格式的區(qū)分豪椿,會顯得更加簡單。我同樣使用一副圖來描述携栋。
同樣的定義一個協(xié)議搭盾,然后構(gòu)建實例來遵循協(xié)議,實際適用則用到具體的響應格式婉支,比如JSON
或者XML
,則可以通過實現(xiàn)子類的的方式來繼承實例和獲得協(xié)議的定義的接口鸯隅。
2.1 AFURLResponseSerialization 協(xié)議
AFURLResponseSerialization 協(xié)議定義了一個方法,這個方法就是我們使用過程中最基本的方法向挖,子類通過實現(xiàn)這個方法蝌以,對response
進行對應的格式化,最終獲取的是我們想要的格式的內(nèi)容何之。
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
2.2 AFHTTPResponseSerializer
AFHTTPResponseSerializer 的屬性不多跟畅,只有三個。
/**
設定允許的 HTTP 狀態(tài)碼溶推。
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
*/
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;
/**
用于設定能夠接受的 MIME 類型. 帶有 MIME 類型的 `Content-Type` 字段碍彭,不會加入到這其中,因為這在驗證的時候發(fā)生錯誤悼潭。
*/
@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
是否有效的函數(shù)舰褪。這是它核心函數(shù)皆疹。我在代碼中的注釋講解。
- (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 狀態(tài)
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 進行甄別,但是并沒有一個返回最終結(jié)果的函數(shù)敲茄。是因為返回的最終數(shù)據(jù)默認是 NSData
位谋,大部分情況下,我們不能直接使用堰燎,因此 AF 定義了幾個子類用于實現(xiàn)各個不同的格式轉(zhuǎn)化掏父。在上圖中,已經(jīng)看到了目前支持 6 種不同的數(shù)據(jù)格式秆剪。下面只針對常用的 JSON 進行分析赊淑。
PS:AFHTTPResponseSerialize 遵循了NScoping 協(xié)議,并對相關(guān)的協(xié)議方法進行了實現(xiàn)仅讽。
2.3 AFJSONResponseSerializer
AFJSONResponseSerializer 繼承自 AFHTTPResponseSerializer 陶缺,除了擁有 AFHTTPResponseSerializer 的一切之外,另外定義了一下幾個方法用戶設置該子類特有的信息何什。
+ (instancetype)serializer {
// 返回一個 JSONReading 序列化內(nèi)容组哩。
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 數(shù)據(jù)中的空值。
@property (nonatomic, assign) BOOL removesKeysWithNullValues;
接下來是 AFHTTPResponseSerializer
每個子類的核心方法位仁,也即是AFURLResponseSerialization
唯一的協(xié)議方法的實現(xiàn)柑贞。其中 AFJSONResponseSerializer
中實現(xiàn)如下:
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
// 在轉(zhuǎn)化數(shù)據(jù)格式之前,先檢測的 response 合法性 和 data 的有效性聂抢。
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
// data 數(shù)據(jù)為 nil 時钧嘶,直接返回nil。 或者在某些情況(Safari中)琳疏,data 解析為 @“ ”有决。 這時候?qū)嶋H也是數(shù)據(jù)為空。
// 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 數(shù)據(jù)中為空的字段刪除。
if (self.removesKeysWithNullValues) {
// 詳情可以查看 AFJSONObjectByRemovingKeysWithNullValues 揽趾,比較簡單
return AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}
return responseObject;
}
AFJSONResponseSerializer
的實現(xiàn)是一個代表台汇,更多的其他子類的實現(xiàn),有興趣的同學可以自行查看。
3. 最后
對于 AF 序列化閱讀励七,除了吸取作者的一些設計方案之外智袭,還能督促我們了解更多的有關(guān) Http 的相關(guān)知識。所以掠抬,不管是出于什么目的去閱讀源碼吼野,都對開發(fā)者受益匪淺。最后感謝開源两波!
參考:
iOS-使用CFStringTransform漢字轉(zhuǎn)拼音
iOS122-移動混合開發(fā)研究院
CFString?Transform
iOS中流(NSStream)的使用