這篇就講到了跟請求相關的類了
關於AFNetworking 3.0 源碼解讀 的文章篇幅都會很長,因為不僅僅要把代碼進行詳細的的解釋,還會大概講解和代碼相關的知識點。
上半篇: URI編碼的知識
關於什么叫URI編碼和為什么要編碼,請看我轉載的這篇文章 url 編碼(percentcode 百分號編碼)
給定一個URL:http://www.imkevinyang.com/2009/08/%E8%AF%A6%E8%A7%A3javascript%E4%B8%AD%E7%9A%84url%E7%BC%96%E8%A7%A3%E7%A0%81.html
其實這個URL並不算復雜,查詢部分(query)都是簡單的key:NSString value: NSString 還有一些比這個更復雜的例子。
我們現在這埋個伏筆:假如你的請求參數是這樣的,那么編碼后的URL是什么樣呢? 不用着急,后邊會做出詳細解答。
根據RFC 3986的規定:URL百分比編碼的保留字段分為:
1. ':' '#' '[' ']' '@' '?' '/'
2. '!' '$' '&' ''' '(' ')' '*' '+' ',' ';' '='
在對查詢字段百分比編碼時,'?'和'/'可以不用編碼,其他的都要進行編碼。
這個方法是上邊的那個方法的實現部分。
這里值得注意的是:
1. 字符串需要經過過濾 ,過濾法則通過 NSMutableCharacterSet 實現。添加規則后,只對規則內的因子進行編碼。
2. 為了處理類似emoji這樣的字符串,rangeOfComposedCharacterSequencesForRange 使用了while循環來處理,也就是把字符串按照batchSize分割處理完再拼回。
該方法最終把類型為NSDictionary的參數處理為字符串類型。
舉個簡單的沒進行百分百編碼的例子:
如果參數是NSDictionary *info = @{@"name":@"zhangsan",@"age":20} ;
AFQueryStringFromParameters(info) 的結果就是:name=zhangsan&age=20 (沒有百分比編碼)
上邊的AFQueryStringFromParameters 這個方法的實現部分還是有點復雜的。我們先逐步分析一下。
可以看出最終轉化后的結果肯定有一個=號,這個等號左邊和右邊都有一個數據。而且這個數據是字符串,因此我們定義一個模型用來記錄左邊和右邊的數據。
有名字就可得知這個類 代表一個查詢字符串對。field代表=號左邊的,value代表=號右邊的數據。
核心方法URLEncodedStringValue 可定也是把左右的數據使用AFPercentEscapedStringFromString函數百分比編碼后用=拼接起來。
看到那兩個方法的聲明了嗎?我們不看下邊的代碼也應該能猜到這個核心方法的實現肯定借助了這兩個被聲明的方法。當然也可以調整函數的順序,沒有這么做也是為了可讀性的考慮吧。
看這個方法的實現,由一個字符串數組根據連接符&拼接成一個字符串並返回。
這行代碼也說明上邊聲明的AFQueryStringPairsFromDictionary函數接受一個字典參數,最終返回一個裝着AFQueryStringPair模型的數組。便利數組后取出模型,然后轉換成字符串,保存到新的數組中。
接下來在看看AFQueryStringPairsFromDictionary 這個函數的實現部分。
可以看出,這個函數又借助了另外一個函數AFQueryStringPairsFromKeyAndValue
其實到了這里,有一個疑問,感覺AFQueryStringPairsFromDictionary這個函數有點多余,這兩個函數差不多啊,能不能使用一個呢?
其實也是可以的,這里這么寫我猜應該也是出於函數編程的想法吧。更容易理解為根據一個字典得到一個數組,當然還有就是參數的限制,這里只能接受字典,下邊的方法的value是id類型。(如有錯誤,請留言指正,謝謝)
改成這樣也行:
好了,在來看看下班的函數
1 // 把 key value 數據轉換成數組 2 NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) { 3 NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; 4 5 //排序: 升序 6 NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)]; 7 8 // 如果參數的value 是字典數據 9 /* 10 舉個例子: 11 key: info 12 value: @{@"name":@"zhangsan",@"age": @"30"} 13 */ 14 if ([value isKindOfClass:[NSDictionary class]]) { 15 NSDictionary *dictionary = value; 16 // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries 17 for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { 18 id nestedValue = dictionary[nestedKey]; 19 if (nestedValue) { 20 [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; 21 } 22 } 23 } else if ([value isKindOfClass:[NSArray class]]) { 24 NSArray *array = value; 25 for (id nestedValue in array) { 26 [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; 27 } 28 } else if ([value isKindOfClass:[NSSet class]]) { 29 NSSet *set = value; 30 for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { 31 [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)]; 32 } 33 } else { 34 [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]]; 35 } 36 37 return mutableQueryStringComponents; 38 }
這里值得注意的有兩點:
1. 函數迭代的一種思想 ,當value時NSDictionary/NSArray/NSSet 這種集合類型的時候,通過調用自身這種思想來實現功能,這個很重要,在很多地方能夠用到,
看看YYModel 源碼解讀(二)之NSObject+YYModel.h (4)就知道了。
2. 就是為什么排序呢?這個不是很懂,如果有懂得,可以留言。
=============================== 分割線 ============================
介紹了URI編碼的一些知識,我們繼續看源碼,但是,往下看,就感覺有點亂了,1000多行的帶碼中有好幾個小類,要是按照順序一點點看,難免讓人煩躁,難度也不小。
因此,我決定對它進行拆分,分成好幾部分進行介紹,然后再串聯起來。
那就先從HTTPBody開始說起。大家在使用AFNetworking上傳圖片或者其他文件的時候會用到AFMultipartFormData這個協議,然后調用協議內的方法,拼接數據,這個使用起來非常簡單,但是它內部又是如何實現的呢?接下來就對他進行解析。
我們先來看一個完整的post請求:
某app的一個登錄POST請求: POST / HTTP/1.1 Host: log.nuomi.com Content-Type: multipart/form-data; boundary=Boundary+6D3E56AA6EAA83B7 Cookie: access_log=7bde65268e2260bb0a85c7de2c67c468; BAIDUID=428D86FDBA6028DE2A5496BE3E7FC308:FG=1; BAINUOCUID=4368e1b7499c455dcd437da336ca1ca9feb8f57d; BDUSS=Ecwa3NvN1NjNWhsVGxWZktFfkc2bzJxQjZ3RFJpTFBiUzZqZUJZU0ZTSmZsN0ZXQVFBQUFBJCQAAAAAAAAAAAEAAABxbLRYWXV1dXV3dXV1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF8KilZfCopWR; bn_na_copid=60139b4b2ba75706fc384d987c2e4007; bn_na_ctag=W3siayI6Imljb25fMSIsInMiOiJ0dWFuIiwidiI6IjMyNiIsInQiOiIxNDUxODg2OTE0In1d; channel=user_center%7C%7C; channel_content=; channel_webapp=webapp; condition=6.0.3; domainUrl=sh; na_qab=6be39bfce918bb7b51887412e009faa6; UID=1488219249 Connection: keep-alive Accept: */* User-Agent: Bainuo/6.1.0 (iPhone; iOS 9.0; Scale/2.00) Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9 Content-Length: 22207 Accept-Encoding: gzip, deflate --Boundary+6D3E56AA6EAA83B7 /// 開始 Content-Disposition: form-data; name="app_version" 6.1.0 --Boundary+6D3E56AA6EAA83B7
HTTP請求頭我們就暫時不說了,看這個body的內容
--Boundary+6D3E56AA6EAA83B7 /// 開始 Content-Disposition: form-data; name="app_version" 6.1.0 --Boundary+6D3E56AA6EAA83B7
組成分為4個部分: 1.初始邊界 2.body頭 3.body 4.結束邊界。 下邊就會用着這些知識。
我把AFMultipartFormData協議提供的方法總結成了一張圖?
當然這幾個方法在下邊都會有介紹。我們還是介紹body部分。
看着個body類的聲明就能夠知道body包含的所有信息,這些信息也和例子中的body的4大組成部分息息相關。
對AFHTTPBodyPart的擴展部分,可以看出曾加了三個屬性:
1. phase:使用枚舉包裝body4大組成部分
2. 輸入流
3. 每個組成部分的位置
增加了兩個方法:
- (BOOL)transitionToNextPhase; 轉移到下一個階段
- (NSInteger)readData:(NSData *)data
intoBuffer:(uint8_t *)buffer
maxLength:(NSUInteger)length; 讀取數據
實現部分呢,這個就沒什么好解釋的了,我們看看transitionToNextPhase這個函數的實現部分。
這個就是把這幾個階段,依次往后傳遞,值得注意的是在第②個階段打開了流,第③個階段關閉了流。
body可能有好幾種類型,根據不同的類型返回不同方法創建的NSInputStream 。
這個方法是根據headers字典來拼接body頭,看個例子:
Content-Disposition: form-data; name="record"; filename="record.jpg" Content-Type: application/json
規則:Content-Disposition + : + 空格 + 其他 然后以\r\n結尾,在頭部結束部分再拼接一個\r\n
這個比較好理解了,HTTP協議就是這么用的,在這個例子中self.headers 的值為:
{ "Content-Disposition" = "form-data; name=\"record\"; filename=\"record.jpg\""; "Content-Type" = "application/json"; }
這個方法用來獲取body的大小的。方法實現比較簡單,需要注意的是初始和結束邊界的問題,要做個判斷,然后調用函數轉換為NSData,計算大小
該方法返回是否還有數據可讀。
1 - (NSInteger)read:(uint8_t *)buffer 2 maxLength:(NSUInteger)length 3 { 4 NSInteger totalNumberOfBytesRead = 0; 5 6 if (_phase == AFEncapsulationBoundaryPhase) { 7 NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding]; 8 totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; 9 } 10 11 if (_phase == AFHeaderPhase) { 12 NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding]; 13 totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; 14 } 15 16 if (_phase == AFBodyPhase) { 17 NSInteger numberOfBytesRead = 0; 18 19 numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; 20 if (numberOfBytesRead == -1) { 21 return -1; 22 } else { 23 totalNumberOfBytesRead += numberOfBytesRead; 24 25 if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) { 26 [self transitionToNextPhase]; 27 } 28 } 29 } 30 31 if (_phase == AFFinalBoundaryPhase) { 32 NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]); 33 totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; 34 } 35 36 return totalNumberOfBytesRead; 37 } 38 39 - (NSInteger)readData:(NSData *)data 40 intoBuffer:(uint8_t *)buffer 41 maxLength:(NSUInteger)length 42 { 43 #pragma clang diagnostic push 44 #pragma clang diagnostic ignored "-Wgnu" 45 // 比較數據和允許的最大長度 選取比較小的那個 46 NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length)); 47 48 // copy data中range的數據到buffer 49 [data getBytes:buffer range:range]; 50 #pragma clang diagnostic pop 51 52 _phaseReadOffset += range.length; 53 54 if (((NSUInteger)_phaseReadOffset) >= [data length]) { 55 [self transitionToNextPhase]; 56 } 57 58 return (NSInteger)range.length; 59 }
這兩個方法是把body數據寫入到buffer中。通過觀察着這兩個方法,可得知,這兩個方法肯定在其他的代碼中的某個循環中被調用,目的是得到想要的數據格式。
舉個正常點的例子吧:
假如說初始的邊界的大小為10 頭部為20 主體為30 結束邊界為10 最大長度為100
在
- (NSInteger)readData:(NSData *)data intoBuffer:(uint8_t *)buffer maxLength:(NSUInteger)length
內部大概是這樣的:
_phaseReadOffset == 0 -》 range == (0,10) -》 buffer[0-10] == data(10) -》 _phaseReadOffset == 10
由於_phaseReadOffset == 10 >= 10 -》 [self transitionToNextPhase]; 進入下一個階段。
由於這個小類遵守NSCopying協議,加了下邊方法。
============================= 分割線 ==============================
在這里補充一點。對於NSInputStream的使用來說,我們要手動實現方法
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length;
這樣當我們使用open打開流的時候,就會調用這個方法,我們需要在這個方法中處理我們的邏輯。
這也是上邊中為什么能完整的讀取body中的數據的解釋。
還有這個maxLength和buffer有關系,uint8_t在mac 64為上為32768.
============================= 分割線 ===========================
下邊介紹 AFMultipartBodyStream
其實AFHTTPBodyPart就像是一個個具體的數據一樣,而AFMultipartBodyStream更像是一個管道,和body相連,數據從body沿着管道流入request中去。
這層抽象的概念還是蠻重要的。再設計之初,這兩個抽象類就應該各自完成各自的任務,即使body中也有stream 但那也只屬於body自身的業務。
它是繼承自NSInputStream的,那么為什么要繼承自這個呢? 就要看下邊的這個方法了。
由此可以看出,數據最終是通過setHTTPBodySteam方法傳遞給Request的。是一個NSInputStream類型,因此AFMultipartBodyStream 繼承自NSInputStream。
這個是又增加了一些需要的屬性,這個會在下邊的方法中介紹到,在這里就先略過。
初始化一些屬性。
重置初始邊界和結束邊界,當有多個body的時候,只需要拼接一個頭部邊界和一個結束邊界就可以了。
1 - (NSInteger)read:(uint8_t *)buffer 2 maxLength:(NSUInteger)length 3 { 4 5 NSLog(@"length:%ld",(long)length); 6 if ([self streamStatus] == NSStreamStatusClosed) { 7 return 0; 8 } 9 10 NSInteger totalNumberOfBytesRead = 0; 11 12 #pragma clang diagnostic push 13 #pragma clang diagnostic ignored "-Wgnu" 14 // 遍歷讀取數據 15 while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) { 16 // 如果當前讀取的body不存在或者body沒有可讀字節 17 if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) { 18 //把下一個body賦值給當前的body 如果下一個為nil 就退出循環 19 if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) { 20 break; 21 } 22 } else { // 當前body存在 23 24 // 剩余可讀文件的大小 25 NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead; 26 NSLog(@"maxLength:%ld",(long)maxLength); 27 // 把當前的body的數據讀入到buffer中 28 NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength]; 29 // NSInteger numberOfBytesRead = 0; 30 NSLog(@"numberOfBytesRead:%ld",(long)maxLength); 31 if (numberOfBytesRead == -1) { 32 self.streamError = self.currentHTTPBodyPart.inputStream.streamError; 33 break; 34 } else { 35 36 // 37 totalNumberOfBytesRead += numberOfBytesRead; 38 39 if (self.delay > 0.0f) { 40 [NSThread sleepForTimeInterval:self.delay]; 41 } 42 } 43 } 44 } 45 #pragma clang diagnostic pop 46 47 return totalNumberOfBytesRead; 48 }
這個方法是AFMultipartBodyStream通過body讀取數據的核心方法。下面通過舉一個例子來看看這個方法究竟是怎么工作的?
1. 假如我們上傳一張圖片img.png 他的大小為80000,也就是差不多80k吧。
2. 通過AFMultipartBodyStream讀取數據,會首先調用上邊的方法。讀取數據並不是一次性讀取的,而是分批分次讀取的,這這個方法中,每次讀取的大小為32k,也就是32*1024 = 32768的大小。
3. 第一次調用后self.currentHTTPBodyPart 指向我們的img.png 通過
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength]; 方法在body中讀取了32768大小的數據保存到了緩存buffer中。
4. 由於整個圖片大小是80000 一次調用只讀取了32768 還有數據沒讀完,一次這個方法還會再次被調用。
5. 第二次調用這個方法,由於[self.currentHTTPBodyPart hasBytesAvailable]還有數據,所以還是會走到else的方法中,self.currentHTTPBodyPart並沒有指向別的body。因此繼續執行 3.的方法。
6. 至於為什么能接着從上次的已讀取的數據開始讀數據,這個是body內部封裝實現的,可參考本文上邊關於body的介紹。
7. 重復 3 4 5 的步驟,直到沒有數據可讀時,stream就會關閉流。到此我們的突變數據就以流的形式上傳到服務器了。
ps: 如有錯誤地方,請給與指正哦。
關閉讀取緩存和設置getter方法
重寫open close方法
重寫NSSteam的方法,說實話不太明白為什么寫這個??
返回總大小
設置 跟CoreFoundation相關的方法。
實現NSCopying協議。
============================= 分割線 ==============================
在寫一個功能的時候,我們往往並不能把業務功能分隔的很完美,這個就跟經驗相關了,通過封裝AFHTTPBodyPart和AFMultipartBodyStream這兩個小工具,我們已經能夠拿到數據了。還記得之前的 AFMultipartFormData 協議嗎?在使用時,我們調用協議的方法,來把數據上傳的。理所當然,我們只要讓AFMultipartBodyStream實現這個協議不就可以做到我們的目的了嗎?
但這顯然是不夠好的,因此AFNetworking 又 再次對 AFHTTPBodyPart和AFMultipartBodyStream 進行了封裝。
看到這個類,應該能聯想到它的作用了。再看這個方法
那個formData 正好是AFStreamingMultipartFormData啊,他也遵守AFMultipartFormData協議,這個下邊會詳細說。
之所以說他起到了連接request和數據的作用,就是因為用於這兩個屬性。
初始化方法,在這里,創建了邊界和管道。
1 - (BOOL)appendPartWithFileURL:(NSURL *)fileURL 2 name:(NSString *)name 3 fileName:(NSString *)fileName 4 mimeType:(NSString *)mimeType 5 error:(NSError * __autoreleasing *)error 6 { 7 NSParameterAssert(fileURL); 8 NSParameterAssert(name); 9 NSParameterAssert(fileName); 10 NSParameterAssert(mimeType); 11 12 if (![fileURL isFileURL]) { 13 NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)}; 14 if (error) { 15 *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo]; 16 } 17 18 return NO; 19 } else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) { 20 NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)}; 21 if (error) { 22 *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo]; 23 } 24 25 return NO; 26 } 27 28 NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error]; 29 if (!fileAttributes) { 30 return NO; 31 } 32 33 NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; 34 [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; 35 [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; 36 37 AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; 38 bodyPart.stringEncoding = self.stringEncoding; 39 bodyPart.headers = mutableHeaders; 40 bodyPart.boundary = self.boundary; 41 bodyPart.body = fileURL; 42 bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue]; 43 [self.bodyStream appendHTTPBodyPart:bodyPart]; 44 45 return YES; 46 }
這個方法其實很好理解,就是通過本地的一個文件的URL獲取數據。
我們通過這個URL能夠獲取到一些和文件相關的信息,然后再進行一些必要的判斷,最后生成一個AFHTTPBodyPart模型,最終把這個模型拼接到管道的模型數組中。就完成任務了。也就是說,一個良好的設計,在使用時就會很順暢。
我們能夠在這個方法中學到的“
1. NSParameterAssert() 用來判斷參數是否為空,如果為空就拋出異常
2. 使用isFileURL 判斷一個URL是否為fileURL 使用checkResourceIsReachableAndReturnError判斷路徑能夠到達
3. 使用 [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error] 獲取本地文件屬性
1 - (BOOL)appendPartWithFileURL:(NSURL *)fileURL 2 name:(NSString *)name 3 error:(NSError * __autoreleasing *)error 4 { 5 NSParameterAssert(fileURL); 6 NSParameterAssert(name); 7 8 NSString *fileName = [fileURL lastPathComponent]; 9 NSString *mimeType = AFContentTypeForPathExtension([fileURL pathExtension]); 10 11 return [self appendPartWithFileURL:fileURL name:name fileName:fileName mimeType:mimeType error:error]; 12 }
再來看這個方法,參數比上邊的那個方法少了幾個,不難猜測,應該是有些參數會采取計算或者默認的方法初始化的。
1. lastPathComponent ,https://www.baidu.com/abc.html 結果就是abc.html
2. pathExtension https://www.baidu.com/abc.html 結果就是html
這里邊有一個AFContentTypeForPathExtension() 方法,我們看看是怎么實現的:
static inline NSString * AFContentTypeForPathExtension(NSString *extension) { NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL); NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType); if (!contentType) { return @"application/octet-stream"; } else { return contentType; } }
首先這算是一個內聯函數,可以根據一個后綴名獲取contentType 這個記住就行了,中間的兩個函數包含在
#import <MobileCoreServices/MobileCoreServices.h>中。
1 - (void)appendPartWithInputStream:(NSInputStream *)inputStream 2 name:(NSString *)name 3 fileName:(NSString *)fileName 4 length:(int64_t)length 5 mimeType:(NSString *)mimeType 6 { 7 NSParameterAssert(name); 8 NSParameterAssert(fileName); 9 NSParameterAssert(mimeType); 10 11 NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; 12 [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; 13 [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; 14 15 AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; 16 bodyPart.stringEncoding = self.stringEncoding; 17 bodyPart.headers = mutableHeaders; 18 bodyPart.boundary = self.boundary; 19 bodyPart.body = inputStream; 20 21 bodyPart.bodyContentLength = (unsigned long long)length; 22 23 [self.bodyStream appendHTTPBodyPart:bodyPart]; 24 }
通過流來獲取數據。這個沒什么好說的。
1 - (void)appendPartWithFileData:(NSData *)data 2 name:(NSString *)name 3 fileName:(NSString *)fileName 4 mimeType:(NSString *)mimeType 5 { 6 NSParameterAssert(name); 7 NSParameterAssert(fileName); 8 NSParameterAssert(mimeType); 9 10 NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; 11 [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; 12 [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; 13 14 [self appendPartWithHeaders:mutableHeaders body:data]; 15 } 16 17 - (void)appendPartWithFormData:(NSData *)data 18 name:(NSString *)name 19 { 20 NSParameterAssert(name); 21 22 NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; 23 [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"]; 24 25 [self appendPartWithHeaders:mutableHeaders body:data]; 26 } 27 28 - (void)appendPartWithHeaders:(NSDictionary *)headers 29 body:(NSData *)body 30 { 31 NSParameterAssert(body); 32 33 AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; 34 bodyPart.stringEncoding = self.stringEncoding; 35 bodyPart.headers = headers; 36 bodyPart.boundary = self.boundary; 37 bodyPart.bodyContentLength = [body length]; 38 bodyPart.body = body; 39 40 [self.bodyStream appendHTTPBodyPart:bodyPart]; 41 }
這三個方法,是根據NSData 獲取數據的方法,為了盡量不出現重復的代碼,抽象了這個方法
- (void)appendPartWithHeaders:(NSDictionary *)headers
body:(NSData *)body
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes delay:(NSTimeInterval)delay { self.bodyStream.numberOfBytesInPacket = numberOfBytes; self.bodyStream.delay = delay; }
設置管道的兩個屬性
1 - (NSMutableURLRequest *)requestByFinalizingMultipartFormData { 2 if ([self.bodyStream isEmpty]) { 3 return self.request; 4 } 5 6 // Reset the initial and final boundaries to ensure correct Content-Length 7 [self.bodyStream setInitialAndFinalBoundaries]; 8 [self.request setHTTPBodyStream:self.bodyStream]; 9 10 [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"]; 11 [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"]; 12 13 return self.request; 14 }
這個是把數據跟請求建立聯系的核心方法,通過 [self.request setHTTPBodyStream:self.bodyStream];這個方法建立聯系,然后設置Content-Type
和 Content-Length 最后返回一個NSMutableURLRequest。
================================= 分割線 =====================================
看完了上邊所有的算是輔助功能的小工具類后,我們進入正題,也正驗證了那句話,一切復雜的問題,分割成若干子問題后,就很容已解決。
我們先看看AFHTTPRequestSerializer 暴露出來的接口有什么
我們給每一個暴露出來的屬性或方法添加一個標記,到后邊實現方法的時候我們就使用標記來代替名稱
1. 標記①
2. 標記②
3. 標記③
這個要仔細介紹下,在某些特殊的場景下還是能用到的。我們點開NSURLRequestCachePolicy 可以看到是一個枚舉值
1 typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy) 2 { 3 NSURLRequestUseProtocolCachePolicy = 0, 4 5 NSURLRequestReloadIgnoringLocalCacheData = 1, 6 NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented 7 NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData, 8 9 NSURLRequestReturnCacheDataElseLoad = 2, 10 NSURLRequestReturnCacheDataDontLoad = 3, 11 12 NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented 13 };
NSURLRequestUseProtocolCachePolicy 這個是默認的緩存策略,緩存不存在,就請求服務器,緩存存在,會根據response中的Cache-Control字段判斷下一步操作,如: Cache-Control字段為must-revalidata, 則詢問服務端該數據是否有更新,無更新的話直接返回給用戶緩存數據,若已更新,則請求服務端。
NSURLRequestReloadIgnoringLocalCacheData 這個策略是不管有沒有本地緩存,都請求服務器。
NSURLRequestReloadIgnoringLocalAndRemoteCacheData 這個策略會忽略本地緩存和中間代理 直接訪問源server
NSURLRequestReturnCacheDataElseLoad 這個策略指,有緩存就是用,不管其有效性,即Cache-Control字段 ,沒有就訪問源server
NSURLRequestReturnCacheDataDontLoad 這個策略只加載本地數據,不做其他操作,適用於沒有網路的情況
NSURLRequestReloadRevalidatingCacheData 這個策略標示緩存數據必須得到服務器確認才能使用,未實現。
4. 標記④
在HTTP連接中,一般都是一個請求對應一個連接,每次簡歷tcp連接是需要一定時間的。管線化,允許一次發送一組請求而不必等到相應。但由於目前並不是所有的服務器都支持這項功能,因此這個屬性默認是不開啟的。管線化使用同一tcp連接完成任務,因此能夠大大提交請求的時間。但是響應要和請求的順序 保持一致才行。使用場景也有,比如說首頁要發送很多請求,可以考慮這種技術。但前提是建立連接成功后才可以使用。
5. 標記⑤
網絡服務類型 是一個枚舉值
typedef NS_ENUM(NSUInteger, NSURLRequestNetworkServiceType) { NSURLNetworkServiceTypeDefault = 0, // Standard internet traffic NSURLNetworkServiceTypeVoIP = 1, // Voice over IP control traffic NSURLNetworkServiceTypeVideo = 2, // Video traffic NSURLNetworkServiceTypeBackground = 3, // Background traffic NSURLNetworkServiceTypeVoice = 4 // Voice data };
可以通過這個值來指定當前的網絡類型,系統會跟據制定的網絡類型對很多方面進行優化,這個就設計到很細微的編程技巧了,可作為一個優化的點備用。
6. 標記⑥
設置請求超時時間,以秒為單位,默認為60秒。
7. 標記⑦
8. 標記⑧
9. 標記⑨
10. 標記⑩
這兩個方法Authorization 這個詞有關,上邊的那個方法是根據用戶名和密碼 生成一個 Authorization 和值,拼接到請求頭中規則是這樣的
Authorization: Basic YWRtaW46YWRtaW4= 其中Basic表示基礎認證,當然還有其他認證,如果感興趣,可以看看本文開始提出的那本書。后邊的YWRtaW46YWRtaW4= 是根據username:password 拼接后然后在經過Base64編碼后的結果。
如果header中有 Authorization這個字段,那么服務器會驗證用戶名和密碼,如果不正確的話會返回401錯誤。
11. 標記⑪
12. 標記⑫
13. 標記⑬
ps 這個要介紹一下 ,當我們需要一些東西或控件或對象需要自定義的時候,我們可以把我們的要求封裝到一個block中。通過定制這個block達到自由定制的目的。
舉個簡單的例子,加入我們需要創建一個view,這個view跟兩個參數相關,title,subtitle。 我們就可以使用上邊的這個思想,返回一個返回值為view的block,這樣通過參數,我們可以自由定制各種各樣的view。
這樣的場景還是很多,我現在也還沒想好具體怎么用,只是突然有了這樣的想法,大家如果有好的想法,可以留言。
14. 標記⑭
下邊這三個是核心方法了,用來創建NSMutableURLRequest 這個對象,這個對象的創建又與上邊①--⑬的設置息息相關
參數就不必介紹了,大家應該都懂,根據這個方法的注釋,當method為GET/HEAD/DELETE 時,參數會被拼接到URL中,其他情況則會當做requset的body處理。
這個方法支持上傳數據,值得注意的是之所以能夠把本地磁盤或者內存中的數據發送到服務器,是因為NSURLRequest 有兩個屬性 :
NSData *HTTPBody;
NSInputStream *HTTPBodyStream;
這個方法可以把一個請求中的body數據保存到一個文件中,然后返回一個HTTPBodyStream為nil的請求,按照注釋說的,NSURLSessionTask在使用流傳數據時。如果沒拼接Content-Length 會有問題。然后可以把這文件上傳或者把它轉為二進制文件上傳。
=================================== 分割線 =====================================
我們已經知道了很多了,這篇真的很長,但耐心看完,一定會受益匪淺,知道了每個屬性或者方法的作用,那么它的實現也基本上有點思路了。
這個是需要監聽的屬性,但看這些屬性而言,要想實現當屬性變化時,就調用監聽方法,就需要我們手動實現監聽方法。這也就說明,如果在平時開發中想要監聽一個對象中某個自定義的屬性時,只需要手動實現監聽方法就行了。看看是怎么做的:
1 // Workarounds for crashing behavior using Key-Value Observing with XCTest 2 // See https://github.com/AFNetworking/AFNetworking/issues/2523 3 /** 4 * 下邊的這幾個setter方法,主要目的是觸發kvo監聽 5 */ 6 - (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess { 7 [self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))]; 8 _allowsCellularAccess = allowsCellularAccess; 9 [self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))]; 10 } 11 12 - (void)setCachePolicy:(NSURLRequestCachePolicy)cachePolicy { 13 [self willChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))]; 14 _cachePolicy = cachePolicy; 15 [self didChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))]; 16 } 17 18 - (void)setHTTPShouldHandleCookies:(BOOL)HTTPShouldHandleCookies { 19 [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))]; 20 _HTTPShouldHandleCookies = HTTPShouldHandleCookies; 21 [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))]; 22 } 23 24 - (void)setHTTPShouldUsePipelining:(BOOL)HTTPShouldUsePipelining { 25 [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))]; 26 _HTTPShouldUsePipelining = HTTPShouldUsePipelining; 27 [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))]; 28 } 29 30 - (void)setNetworkServiceType:(NSURLRequestNetworkServiceType)networkServiceType { 31 [self willChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))]; 32 _networkServiceType = networkServiceType; 33 [self didChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))]; 34 } 35 36 - (void)setTimeoutInterval:(NSTimeInterval)timeoutInterval { 37 [self willChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))]; 38 _timeoutInterval = timeoutInterval; 39 [self didChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))]; 40 }
可以看出只要在setter方法中加入兩行代碼就行了。
1 - (instancetype)init { 2 self = [super init]; 3 if (!self) { 4 return nil; 5 } 6 7 self.stringEncoding = NSUTF8StringEncoding; 8 9 self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary]; 10 11 // Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 12 /** 13 * 傳遞可接受的語言,q代表對語言的喜好程度,默認是取出前5個的數據,不足5個,取實際的個數 14 */ 15 NSMutableArray *acceptLanguagesComponents = [NSMutableArray array]; 16 [[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 17 float q = 1.0f - (idx * 0.1f); 18 [acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]]; 19 *stop = q <= 0.5f; 20 }]; 21 22 // 設置請求頭 23 [self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"]; 24 25 // 獲取信息 26 NSString *userAgent = nil; 27 #pragma clang diagnostic push 28 #pragma clang diagnostic ignored "-Wgnu" 29 #if TARGET_OS_IOS 30 // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 31 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]]; 32 #elif TARGET_OS_WATCH 33 // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 34 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]]; 35 #elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED) 36 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]]; 37 #endif 38 #pragma clang diagnostic pop 39 if (userAgent) { 40 if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) { 41 NSMutableString *mutableUserAgent = [userAgent mutableCopy]; 42 43 // 轉換字符串的方法 http://nshipster.com/cfstringtransform/ 44 if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) { 45 userAgent = mutableUserAgent; 46 } 47 } 48 [self setValue:userAgent forHTTPHeaderField:@"User-Agent"]; 49 } 50 51 52 // HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 53 self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil]; 54 55 // 設置監聽 56 self.mutableObservedChangedKeyPaths = [NSMutableSet set]; 57 for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { 58 if ([self respondsToSelector:NSSelectorFromString(keyPath)]) { 59 [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext]; 60 } 61 } 62 63 return self; 64 } 65 66 - (void)dealloc { 67 68 // 取消監聽 69 for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { 70 if ([self respondsToSelector:NSSelectorFromString(keyPath)]) { 71 [self removeObserver:self forKeyPath:keyPath context:AFHTTPRequestSerializerObserverContext]; 72 } 73 } 74 }
這個比較好理解,就是對請求頭字典的操作。
這兩個方法是標記⑩的實現部分,上邊也說明了,拼接Authorization 只要按照一定的規則就可以了。
這個是對標記⑫ 和 標記⑬ 的實現部分,再次聲明下,關於URI中查詢字段 也就是 query部分,的轉換是通過block來轉換的。
1 - (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request 2 withParameters:(id)parameters 3 error:(NSError *__autoreleasing *)error 4 { 5 NSParameterAssert(request); 6 7 NSMutableURLRequest *mutableRequest = [request mutableCopy]; 8 9 // 設置請求頭 10 [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { 11 if (![request valueForHTTPHeaderField:field]) { 12 [mutableRequest setValue:value forHTTPHeaderField:field]; 13 } 14 }]; 15 16 // 設置查詢字段 17 NSString *query = nil; 18 if (parameters) { 19 if (self.queryStringSerialization) { 20 NSError *serializationError; 21 query = self.queryStringSerialization(request, parameters, &serializationError); 22 23 if (serializationError) { 24 if (error) { 25 *error = serializationError; 26 } 27 28 return nil; 29 } 30 } else { 31 switch (self.queryStringSerializationStyle) { 32 case AFHTTPRequestQueryStringDefaultStyle: 33 query = AFQueryStringFromParameters(parameters); 34 break; 35 } 36 } 37 } 38 39 // 如果請求的method 為 GET/HEAD/DELETE 直接把查詢拼接到URL中 40 if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { 41 if (query && query.length > 0) { 42 mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]]; 43 } 44 } 45 46 // 其他的 要設置下邊的內容,然后給請求的HTTPBody 賦值就可以了 47 else { 48 // #2864: an empty string is a valid x-www-form-urlencoded payload 49 if (!query) { 50 query = @""; 51 } 52 if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { 53 [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; 54 } 55 [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]]; 56 } 57 58 return mutableRequest; 59 }
這個方法也不是很復雜,主要的作用就是根據參數對NSURLRequest 進行設置,設置包括
1. 請求頭
2. query字段,如果是GET/HEAD/DELETE 直接拼接到URL中,其他情況拼接到HTTPBody中。
注意:這個方法不處理數據流,只處理參數類型的數據。
1 - (NSMutableURLRequest *)requestWithMethod:(NSString *)method 2 URLString:(NSString *)URLString 3 parameters:(id)parameters 4 error:(NSError *__autoreleasing *)error 5 { 6 NSParameterAssert(method); 7 NSParameterAssert(URLString); 8 9 NSURL *url = [NSURL URLWithString:URLString]; 10 11 NSParameterAssert(url); 12 13 NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url]; 14 mutableRequest.HTTPMethod = method; 15 16 // 設置mutableRequest的一些屬性,這些屬性就是AFHTTPRequestSerializerObservedKeyPaths() f返回的數組, 17 for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { 18 if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) { 19 [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath]; 20 } 21 } 22 23 mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy]; 24 25 return mutableRequest; 26 }
這個是一個創建NSMutableURLRequest 的方法,也是對上邊的標記⑭的實現部分,簡單說一下創建的過程
1. 新建一個NSMutableURLRequest
2. HTTPMethod 賦值
3. 根據 mutableObservedChangedKeyPaths 設置請求的一些屬性
4. 通過
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error 方法過濾和設置請求
1 - (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method 2 URLString:(NSString *)URLString 3 parameters:(NSDictionary *)parameters 4 constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block 5 error:(NSError *__autoreleasing *)error 6 { 7 // method 不能為空 8 // method 不能是GET 和 HEAD 9 NSParameterAssert(method); 10 NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]); 11 12 // 通過私有方法獲取NSMutableURLRequest 13 NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error]; 14 15 // 創建一個AFStreamingMultipartFormData實例,用來處理數據。 16 __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding]; 17 18 if (parameters) { 19 20 // 遍歷parameters后 把value轉成NSData然后拼接到formData中 21 for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) { 22 NSData *data = nil; 23 if ([pair.value isKindOfClass:[NSData class]]) { 24 data = pair.value; 25 } else if ([pair.value isEqual:[NSNull null]]) { 26 data = [NSData data]; 27 } else { 28 data = [[pair.value description] dataUsingEncoding:self.stringEncoding]; 29 } 30 31 if (data) { 32 [formData appendPartWithFormData:data name:[pair.field description]]; 33 } 34 } 35 } 36 37 if (block) { 38 block(formData); 39 } 40 41 return [formData requestByFinalizingMultipartFormData]; 42 }
這個方法是專門處理上傳數據的方法,這里就不允許使用GET / HEAD HTTPMethod了。而且會把參數拼到formdata中了。
1 - (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request 2 writingStreamContentsToFile:(NSURL *)fileURL 3 completionHandler:(void (^)(NSError *error))handler 4 { 5 NSParameterAssert(request.HTTPBodyStream); 6 NSParameterAssert([fileURL isFileURL]); 7 8 // 加上上邊的兩個判斷,下邊的這些代碼就是把文件寫到另一個地方的典型使用方法了 9 NSInputStream *inputStream = request.HTTPBodyStream; 10 NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO]; 11 __block NSError *error = nil; 12 13 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 14 [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 15 [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 16 17 [inputStream open]; 18 [outputStream open]; 19 20 // 讀取數據 21 while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) { 22 uint8_t buffer[1024]; 23 24 NSInteger bytesRead = [inputStream read:buffer maxLength:1024]; 25 if (inputStream.streamError || bytesRead < 0) { 26 error = inputStream.streamError; 27 break; 28 } 29 30 NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead]; 31 if (outputStream.streamError || bytesWritten < 0) { 32 error = outputStream.streamError; 33 break; 34 } 35 36 if (bytesRead == 0 && bytesWritten == 0) { 37 break; 38 } 39 } 40 41 [outputStream close]; 42 [inputStream close]; 43 44 if (handler) { 45 dispatch_async(dispatch_get_main_queue(), ^{ 46 handler(error); 47 }); 48 } 49 }); 50 51 NSMutableURLRequest *mutableRequest = [request mutableCopy]; 52 mutableRequest.HTTPBodyStream = nil; 53 54 return mutableRequest; 55 }
這個方法可以說是一個關於使用NSInputStream和NSOutputSteam 的經典案例,用法可以記下來或那這個方法的代碼做參考。
1 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { 2 if ([AFHTTPRequestSerializerObservedKeyPaths() containsObject:key]) { 3 return NO; 4 } 5 6 return [super automaticallyNotifiesObserversForKey:key]; 7 } 8 9 - (void)observeValueForKeyPath:(NSString *)keyPath 10 ofObject:(__unused id)object 11 change:(NSDictionary *)change 12 context:(void *)context 13 { 14 if (context == AFHTTPRequestSerializerObserverContext) { 15 if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) { 16 [self.mutableObservedChangedKeyPaths removeObject:keyPath]; 17 } else { 18 [self.mutableObservedChangedKeyPaths addObject:keyPath]; 19 } 20 } 21 }
這兩個方法是關於kvo的。 值得學習的地方是我們通過判斷change[NSKeyValueChangeNewKey] 是不是等於[NSNull null] 來寫出不同的結果。
1 + (BOOL)supportsSecureCoding { 2 return YES; 3 } 4 5 - (instancetype)initWithCoder:(NSCoder *)decoder { 6 self = [self init]; 7 if (!self) { 8 return nil; 9 } 10 11 self.mutableHTTPRequestHeaders = [[decoder decodeObjectOfClass:[NSDictionary class] forKey:NSStringFromSelector(@selector(mutableHTTPRequestHeaders))] mutableCopy]; 12 self.queryStringSerializationStyle = (AFHTTPRequestQueryStringSerializationStyle)[[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(queryStringSerializationStyle))] unsignedIntegerValue]; 13 14 return self; 15 } 16 17 - (void)encodeWithCoder:(NSCoder *)coder { 18 [coder encodeObject:self.mutableHTTPRequestHeaders forKey:NSStringFromSelector(@selector(mutableHTTPRequestHeaders))]; 19 [coder encodeInteger:self.queryStringSerializationStyle forKey:NSStringFromSelector(@selector(queryStringSerializationStyle))]; 20 }
1 #pragma mark - NSCopying 2 3 - (instancetype)copyWithZone:(NSZone *)zone { 4 AFHTTPRequestSerializer *serializer = [[[self class] allocWithZone:zone] init]; 5 serializer.mutableHTTPRequestHeaders = [self.mutableHTTPRequestHeaders mutableCopyWithZone:zone]; 6 serializer.queryStringSerializationStyle = self.queryStringSerializationStyle; 7 serializer.queryStringSerialization = self.queryStringSerialization; 8 9 return serializer; 10 }
============================== 分割線 ==============================
1 @implementation AFJSONRequestSerializer 2 3 + (instancetype)serializer { 4 return [self serializerWithWritingOptions:(NSJSONWritingOptions)0]; 5 } 6 7 + (instancetype)serializerWithWritingOptions:(NSJSONWritingOptions)writingOptions 8 { 9 AFJSONRequestSerializer *serializer = [[self alloc] init]; 10 serializer.writingOptions = writingOptions; 11 12 return serializer; 13 } 14 15 #pragma mark - AFURLRequestSerialization 16 17 - (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request 18 withParameters:(id)parameters 19 error:(NSError *__autoreleasing *)error 20 { 21 NSParameterAssert(request); 22 23 if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { 24 return [super requestBySerializingRequest:request withParameters:parameters error:error]; 25 } 26 27 NSMutableURLRequest *mutableRequest = [request mutableCopy]; 28 29 [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { 30 if (![request valueForHTTPHeaderField:field]) { 31 [mutableRequest setValue:value forHTTPHeaderField:field]; 32 } 33 }]; 34 35 if (parameters) { 36 if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { 37 [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 38 } 39 40 [mutableRequest setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error]]; 41 } 42 43 return mutableRequest; 44 } 45 46 #pragma mark - NSSecureCoding 47 48 - (instancetype)initWithCoder:(NSCoder *)decoder { 49 self = [super initWithCoder:decoder]; 50 if (!self) { 51 return nil; 52 } 53 54 self.writingOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(writingOptions))] unsignedIntegerValue]; 55 56 return self; 57 } 58 59 - (void)encodeWithCoder:(NSCoder *)coder { 60 [super encodeWithCoder:coder]; 61 62 [coder encodeInteger:self.writingOptions forKey:NSStringFromSelector(@selector(writingOptions))]; 63 } 64 65 #pragma mark - NSCopying 66 67 - (instancetype)copyWithZone:(NSZone *)zone { 68 AFJSONRequestSerializer *serializer = [super copyWithZone:zone]; 69 serializer.writingOptions = self.writingOptions; 70 71 return serializer; 72 } 73 74 @end
AFJSONRequestSerializer 這個類呢,可以把參數 轉為json進行上傳,當服務器要求我們上傳的數據格式是json的時候呢,就用上了
============================== 分割線 ============================
1 @implementation AFPropertyListRequestSerializer 2 3 + (instancetype)serializer { 4 return [self serializerWithFormat:NSPropertyListXMLFormat_v1_0 writeOptions:0]; 5 } 6 7 + (instancetype)serializerWithFormat:(NSPropertyListFormat)format 8 writeOptions:(NSPropertyListWriteOptions)writeOptions 9 { 10 AFPropertyListRequestSerializer *serializer = [[self alloc] init]; 11 serializer.format = format; 12 serializer.writeOptions = writeOptions; 13 14 return serializer; 15 } 16 17 #pragma mark - AFURLRequestSerializer 18 19 - (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request 20 withParameters:(id)parameters 21 error:(NSError *__autoreleasing *)error 22 { 23 NSParameterAssert(request); 24 25 if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { 26 return [super requestBySerializingRequest:request withParameters:parameters error:error]; 27 } 28 29 NSMutableURLRequest *mutableRequest = [request mutableCopy]; 30 31 [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { 32 if (![request valueForHTTPHeaderField:field]) { 33 [mutableRequest setValue:value forHTTPHeaderField:field]; 34 } 35 }]; 36 37 if (parameters) { 38 if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { 39 [mutableRequest setValue:@"application/x-plist" forHTTPHeaderField:@"Content-Type"]; 40 } 41 42 [mutableRequest setHTTPBody:[NSPropertyListSerialization dataWithPropertyList:parameters format:self.format options:self.writeOptions error:error]]; 43 } 44 45 return mutableRequest; 46 } 47 48 #pragma mark - NSSecureCoding 49 50 - (instancetype)initWithCoder:(NSCoder *)decoder { 51 self = [super initWithCoder:decoder]; 52 if (!self) { 53 return nil; 54 } 55 56 self.format = (NSPropertyListFormat)[[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(format))] unsignedIntegerValue]; 57 self.writeOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(writeOptions))] unsignedIntegerValue]; 58 59 return self; 60 } 61 62 - (void)encodeWithCoder:(NSCoder *)coder { 63 [super encodeWithCoder:coder]; 64 65 [coder encodeInteger:self.format forKey:NSStringFromSelector(@selector(format))]; 66 [coder encodeObject:@(self.writeOptions) forKey:NSStringFromSelector(@selector(writeOptions))]; 67 } 68 69 #pragma mark - NSCopying 70 71 - (instancetype)copyWithZone:(NSZone *)zone { 72 AFPropertyListRequestSerializer *serializer = [super copyWithZone:zone]; 73 serializer.format = self.format; 74 serializer.writeOptions = self.writeOptions; 75 76 return serializer; 77 } 78 79 @end
好了,關於這篇文章,就到這了,其中還是設計了很多的知識點,而且設計上也非常完美,只有反復閱讀,才能領悟到精髓所在。