AFNetworking 傳送 GET、POST 等請求時可以直接將引數按照字典結構傳入,最終編碼到 url 中或者是 body 實體中,同時也支援按照 multipart/form-data 格式,將多種不同的資料合入到 body 中進行傳送,而這些就涉及到 AFNetworking 的請求序列化類,也就是 AFURLRequestSerialization。
AFURLRequestSerialization 是一個協議,它定義了一個方法用於序列化引數到 NSURLRequest 中,AFHTTPRequestSerializer 實現了這個協議,並實現了相應的方法。它不僅提供了普通的引數編碼方法,也提供了 form-data 格式的 request 構建方法,也就是下面的方法
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
複製程式碼
1. form-data
首先簡單介紹一下 form-data,multipart/form-data 主要用於 POST方法中傳遞多種格式和含義的資料,在 body 中引入 boundary 的概念,用分割線將多部分資料融合到一個 body 中傳送給服務端。那麼對於一個簡單的 form-data,它傳送的 body 內容可能如下
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"
v1
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"
v2
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"
v3
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="mydic[key1]"
value1
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="mydic[key2]"
value2
--Boundary+FD2E180F039993ED
header: headerkey
BodyData
--Boundary+FD2E180F039993ED--
複製程式碼
它的特點是
- 每一部分都可以包含 header,一般預設必須包含的標識 header 是 Content-Disposition
- 頭部和每一部分需要以 --Boundary+{XXX} 格式分割
- 末尾以 --Boundary+{XXX}-- 結束
- 請求頭中,要設定 Content-Type: multipart/form-data; boundary=Boundary+{XXX}
- 請求頭要設定 Content-Length 為 body 總長度
2. 一個 form-data 型別的 POST 請求
在 AFNetworking 中,要傳送 form-data,可以通過如下方式傳送
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer.timeoutInterval = 100;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain", @"text/html",@"application/json", @"text/json" ,@"text/javascript", nil];;
[manager POST:@"https://www.baidu.com" parameters:@{@"mydic":@{@"key1":@"value1",@"key2":@"value2"},
@"myArray":@[@"v1", @"v2", @"v3"]
} headers:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
[formData appendPartWithFileData:[@"Data" dataUsingEncoding:NSUTF8StringEncoding]
name:@"DataName"
fileName:@"DataFileName"
mimeType:@"data"];
} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
複製程式碼
主要用到 AFHTTPSessionManager 定義的如下方法
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
複製程式碼
它的內部實現,主要做了這幾件事
- 通過 requestSerializer 的 multipartFormRequestWithMethod 方法構建 NSMutableURLRequest 物件
- 設定頭部
- 通過 AFURLSessionManager 建立 NSURLSessionUploadTask 物件
從中可以看出,請求序列化主要發生在 multipartFormRequestWithMethod 方法中,而 AFHttpSessionManager 預設的 requestSerializer 是 AFHTTPAFHTTPRequestSerializer。
3. 請求序列化
AFHTTPAFHTTPRequestSerializer 對於 form-data 提供瞭如下方法進行序列化
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
複製程式碼
在方法實現裡主要做了以下事情
- 與普通的 urlencode 請求類似,先設定 request 相關引數,仍然是通過 KVO 記錄需要設定的引數,其他都走預設邏輯
- 構造 AFStreamingMultipartFormData 物件,將傳入的引數深度遍歷後一一通過
appendPartWithFormData: name:
方法新增到 AFStreamingMultipartFormData 中 - 提供外部 block,對 AFStreamingMultipartFormData 物件進一步新增資料
- 通過 AFStreamingMultipartFormData 的
requestByFinalizingMultipartFormData
方法構建 request
那麼 AFStreamingMultipartFormData 是一個什麼類呢。
4. 構造 form-data 資料
AFNetworking 定義的 AFStreamingMultipartFormData 類用於表徵一個 form-data 格式 body 的資料,它遵循 AFMultipartFormData 協議,能管理 boundary 字串、用於向 request 傳輸資料的 NSInputStream 物件。
其中對於 form-data 的每一個 part,AFNetworking 定義了一個 AFHTTPBodyPart 類,其中包含如下資訊
- 這個 part 的頭部 header
- 分割字串 boundary
- 內容區長度
- id 型別的 body
- 資料流 inputStream
AFStreamingMultipartFormData 所包含的 NSInputStream 類,實質上是繼承自 NSInputStream 的子類 AFMultipartBodyStream,AFMultipartBodyStream 有一個 HTTPBodyParts 屬性,是一個 AFHTTPBodyPart 型別的陣列,所有 append 到 AFStreamingMultipartFormData 的 part,最後都轉化為一個 AFHTTPBodyPart 物件加入到了 AFMultipartBodyStream 的 HTTPBodyParts 中。
具體來說,AFMultipartFormData 協議(也就是 AFStreamingMultipartFormData 類)定義瞭如下一些 append 方法
appendPartWithFileURL: name: error:
新增檔案路徑內的檔案內容到 form-dataappendPartWithFileURL: name: fileName: mimeType: error:
新增檔案路徑內的檔案內容到 form-data,指定檔名和 mimeTypeappendPartWithInputStream: name: fileName: length: mimeType:
新增 inputStream 到 form-dataappendPartWithFileData: name: fileName: mimeType:
新增 NSData 到 form-dataappendPartWithFormData: name:
新增 NSData 到 form-dataappendPartWithHeaders: body:
新增自定義 header 和 body 到 form-data
下面以 appendPartWithFormData 為例看下具體實現
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
{
NSParameterAssert(name);
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
// 每一塊資料,預設帶上 Content-Disposition 作為頭部
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];
[self appendPartWithHeaders:mutableHeaders body:data];
}
- (void)appendPartWithHeaders:(NSDictionary *)headers
body:(NSData *)body
{
NSParameterAssert(body);
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = headers;
// 複用一個 boundary
bodyPart.boundary = self.boundary;
// body 長度
bodyPart.bodyContentLength = [body length];
bodyPart.body = body;
// 新增到 stream 中
[self.bodyStream appendHTTPBodyPart:bodyPart];
}
複製程式碼
可以看到,就是根據資料構造一個 AFHTTPBodyPart 物件新增到 bodyStream 屬性中;至於檔案和 inputStream,則是直接將檔案 url 和 inputStream 物件賦值給 id 型別的 body。
這樣將所有資料都 append 到了 AFStreamingMultipartFormData 中以後,再呼叫 AFStreamingMultipartFormData 的 requestByFinalizingMultipartFormData 方法就可以構造一個 NSMutableURLRequest 物件了,而在 requestByFinalizingMultipartFormData 方法中,主要做了如下工作
- 將構造出來的 NSMutableURLRequest 的 HTTPBodyStream 屬性設定為 AFStreamingMultipartFormData 的 bodyStream 物件,也就是 AFMultipartBodyStream 作為 NSMutableURLRequest 的 body 資料來源
- 設定 Content-Type
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
複製程式碼
- 設定 Content-Length
[self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
複製程式碼
5. 從 bodyStream 讀取資料
AFMultipartBodyStream 直接繼承自 NSInputStream,它維護一個 包含全部 AFHTTPBodyPart 的陣列,當通過 request 發起一個 NSURLSessionUploadTask 以後,由於設定了 request 的 HTTPBodyStream,則系統會嘗試從 AFMultipartBodyStream 讀取 body 資料,這裡就涉及到了 AFMultipartBodyStream 的 read: maxLength:
方法,它從流中讀取資料到 buffer 中,並返回實際讀取的資料長度(該長度最大為 len)。而實際上 AFMultipartBodyStream 的 numberOfBytesInPacket 屬性就可以限制讀取資料的最大長度。
{
if ([self streamStatus] == NSStreamStatusClosed) {
// 流已關閉,返回長度 0
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
// 一直從 HTTPBodyParts 讀取到位元組數達到 length 為止
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
// 如果還未開始讀取,或者當前 part 已經讀取結束,則進入下一個
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
break;
}
} else {
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
// 從 part 中讀取資料
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
if (numberOfBytesRead == -1) {
// 讀取出錯
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
// 更新總讀取位元組數
totalNumberOfBytesRead += numberOfBytesRead;
if (self.delay > 0.0f) {
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
return totalNumberOfBytesRead;
}
複製程式碼
這裡通過一個 currentHTTPBodyPart 物件對 AFMultipartBodyStream 維護的 AFHTTPBodyPart 陣列進行遍歷,讀取其中每一個 AFHTTPBodyPart 物件的資料到 buffer 中。AFHTTPBodyPart 類也實現了同名的 read 方法,在這個方法裡,按照如下順序,讀取相應部分的資料
- AFEncapsulationBoundaryPhase 頂部邊界
- AFHeaderPhase 頭部資料
- AFBodyPhase 實體
- AFFinalBoundaryPhase 底部邊界
例如讀取頂部邊界資料如下
NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
複製程式碼
但是當讀取到 body 部分時要注意,由於 body 是一個 id 型別,外界主要設定的可能值有 NSData、NSURL、NSInputStream 等,AFNetworking 在這裡統一將 body 的讀取歸一化為 inputStream 流方式讀取,按照如下規則構建 inputStream
- (NSInputStream *)inputStream {
// inputStream 根據 body 的類別返回不同的資料來源
if (!_inputStream) {
if ([self.body isKindOfClass:[NSData class]]) {
_inputStream = [NSInputStream inputStreamWithData:self.body];
} else if ([self.body isKindOfClass:[NSURL class]]) {
_inputStream = [NSInputStream inputStreamWithURL:self.body];
} else if ([self.body isKindOfClass:[NSInputStream class]]) {
_inputStream = self.body;
} else {
_inputStream = [NSInputStream inputStreamWithData:[NSData data]];
}
}
return _inputStream;
}
複製程式碼
讀取到 body 部分時則啟動 stream,讀取完 body 以後關閉 stream
// 這裡是根據當前 phase 切換到下一端 phase 的邏輯
case AFHeaderPhase:
// header -> body
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
case AFBodyPhase:
// body -> 底部邊界
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
複製程式碼
以上就是 AFNetworking 對於 form-data 請求的完整處理,基於 inputStream,將多種不同型別的 form-data 用統一的程式碼模型處理,對外暴露的方法簡潔一致,因而便於使用和理解。