AFNetworking 發(fā)送 GET、POST 等請求時可以直接將參數(shù)按照字典結構傳入,最終編碼到 url 中或者是 body 實體中,同時也支持按照 multipart/form-data 格式,將多種不同的數(shù)據(jù)合入到 body 中進行發(fā)送,而這些就涉及到 AFNetworking 的請求序列化類,也就是 AFURLRequestSerialization。
AFURLRequestSerialization 是一個協(xié)議,它定義了一個方法用于序列化參數(shù)到 NSURLRequest 中,AFHTTPRequestSerializer 實現(xiàn)了這個協(xié)議,并實現(xiàn)了相應的方法。它不僅提供了普通的參數(shù)編碼方法,也提供了 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方法中傳遞多種格式和含義的數(shù)據(jù),在 body 中引入 boundary 的概念,用分割線將多部分數(shù)據(jù)融合到一個 body 中發(fā)送給服務端。那么對于一個簡單的 form-data,它發(fā)送的 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 中,要發(fā)送 form-data,可以通過如下方式發(fā)送
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;
它的內部實現(xiàn),主要做了這幾件事
- 通過 requestSerializer 的 multipartFormRequestWithMethod 方法構建 NSMutableURLRequest 對象
- 設置頭部
- 通過 AFURLSessionManager 創(chuàng)建 NSURLSessionUploadTask 對象
從中可以看出,請求序列化主要發(fā)生在 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
在方法實現(xiàn)里主要做了以下事情
- 與普通的 urlencode 請求類似,先設置 request 相關參數(shù),仍然是通過 KVO 記錄需要設置的參數(shù),其他都走默認邏輯
- 構造 AFStreamingMultipartFormData 對象,將傳入的參數(shù)深度遍歷后一一通過
appendPartWithFormData: name:方法添加到 AFStreamingMultipartFormData 中 - 提供外部 block,對 AFStreamingMultipartFormData 對象進一步添加數(shù)據(jù)
- 通過 AFStreamingMultipartFormData 的
requestByFinalizingMultipartFormData方法構建 request
那么 AFStreamingMultipartFormData 是一個什么類呢。
4. 構造 form-data 數(shù)據(jù)
AFNetworking 定義的 AFStreamingMultipartFormData 類用于表征一個 form-data 格式 body 的數(shù)據(jù),它遵循 AFMultipartFormData 協(xié)議,能管理 boundary 字符串、用于向 request 傳輸數(shù)據(jù)的 NSInputStream 對象。
其中對于 form-data 的每一個 part,AFNetworking 定義了一個 AFHTTPBodyPart 類,其中包含如下信息
- 這個 part 的頭部 header
- 分割字符串 boundary
- 內容區(qū)長度
- id 類型的 body
- 數(shù)據(jù)流 inputStream
AFStreamingMultipartFormData 所包含的 NSInputStream 類,實質上是繼承自 NSInputStream 的子類 AFMultipartBodyStream,AFMultipartBodyStream 有一個 HTTPBodyParts 屬性,是一個 AFHTTPBodyPart 類型的數(shù)組,所有 append 到 AFStreamingMultipartFormData 的 part,最后都轉化為一個 AFHTTPBodyPart 對象加入到了 AFMultipartBodyStream 的 HTTPBodyParts 中。
具體來說,AFMultipartFormData 協(xié)議(也就是 AFStreamingMultipartFormData 類)定義了如下一些 append 方法
-
appendPartWithFileURL: name: error:添加文件路徑內的文件內容到 form-data -
appendPartWithFileURL: name: fileName: mimeType: error:添加文件路徑內的文件內容到 form-data,指定文件名和 mimeType -
appendPartWithInputStream: name: fileName: length: mimeType:添加 inputStream 到 form-data -
appendPartWithFileData: name: fileName: mimeType:添加 NSData 到 form-data -
appendPartWithFormData: name:添加 NSData 到 form-data -
appendPartWithHeaders: body:添加自定義 header 和 body 到 form-data
下面以 appendPartWithFormData 為例看下具體實現(xiàn)
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
{
NSParameterAssert(name);
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
// 每一塊數(shù)據(jù),默認帶上 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];
}
可以看到,就是根據(jù)數(shù)據(jù)構造一個 AFHTTPBodyPart 對象添加到 bodyStream 屬性中;至于文件和 inputStream,則是直接將文件 url 和 inputStream 對象賦值給 id 類型的 body。
這樣將所有數(shù)據(jù)都 append 到了 AFStreamingMultipartFormData 中以后,再調用 AFStreamingMultipartFormData 的 requestByFinalizingMultipartFormData 方法就可以構造一個 NSMutableURLRequest 對象了,而在 requestByFinalizingMultipartFormData 方法中,主要做了如下工作
- 將構造出來的 NSMutableURLRequest 的 HTTPBodyStream 屬性設置為 AFStreamingMultipartFormData 的 bodyStream 對象,也就是 AFMultipartBodyStream 作為 NSMutableURLRequest 的 body 數(shù)據(jù)源
- 設置 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 讀取數(shù)據(jù)
AFMultipartBodyStream 直接繼承自 NSInputStream,它維護一個 包含全部 AFHTTPBodyPart 的數(shù)組,當通過 request 發(fā)起一個 NSURLSessionUploadTask 以后,由于設置了 request 的 HTTPBodyStream,則系統(tǒng)會嘗試從 AFMultipartBodyStream 讀取 body 數(shù)據(jù),這里就涉及到了 AFMultipartBodyStream 的 read: maxLength: 方法,它從流中讀取數(shù)據(jù)到 buffer 中,并返回實際讀取的數(shù)據(jù)長度(該長度最大為 len)。而實際上 AFMultipartBodyStream 的 numberOfBytesInPacket 屬性就可以限制讀取數(shù)據(jù)的最大長度。
{
if ([self streamStatus] == NSStreamStatusClosed) {
// 流已關閉,返回長度 0
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
// 一直從 HTTPBodyParts 讀取到字節(jié)數(shù)達到 length 為止
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
// 如果還未開始讀取,或者當前 part 已經(jīng)讀取結束,則進入下一個
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
break;
}
} else {
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
// 從 part 中讀取數(shù)據(jù)
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
if (numberOfBytesRead == -1) {
// 讀取出錯
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
// 更新總讀取字節(jié)數(shù)
totalNumberOfBytesRead += numberOfBytesRead;
if (self.delay > 0.0f) {
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
return totalNumberOfBytesRead;
}
這里通過一個 currentHTTPBodyPart 對象對 AFMultipartBodyStream 維護的 AFHTTPBodyPart 數(shù)組進行遍歷,讀取其中每一個 AFHTTPBodyPart 對象的數(shù)據(jù)到 buffer 中。AFHTTPBodyPart 類也實現(xiàn)了同名的 read 方法,在這個方法里,按照如下順序,讀取相應部分的數(shù)據(jù)
- AFEncapsulationBoundaryPhase 頂部邊界
- AFHeaderPhase 頭部數(shù)據(jù)
- AFBodyPhase 實體
- AFFinalBoundaryPhase 底部邊界
例如讀取頂部邊界數(shù)據(jù)如下
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 在這里統(tǒng)一將 body 的讀取歸一化為 inputStream 流方式讀取,按照如下規(guī)則構建 inputStream
- (NSInputStream *)inputStream {
// inputStream 根據(jù) body 的類別返回不同的數(shù)據(jù)源
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
// 這里是根據(jù)當前 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 用統(tǒng)一的代碼模型處理,對外暴露的方法簡潔一致,因而便于使用和理解。