AFNetWorking(3.0)原始碼分析(四)——AFHTTPSessionManager(2)
在上一篇部落格中,我們分析了AFHTTPSessionManager
,以及它是如何實現GET/HEAD/PATCH/DELETE
相關介面的。
我們還剩下POST
相關介面沒有分析,在這篇部落格裡面,我們就來分析一下POST
相關介面是如何實現的。
multipart/form-data請求
在繼續理解POST介面之前,我們先來了解一下HTTP協議中和POST相關的multipart/form-data
請求。
關於multipart/form-data
請求的內容,大部分都來源於這篇部落格:HTTP協議之multipart/form-data請求分析。
我們知道,根據HTTP 1.1的協議規定,我們的請求型別可以是GET, HEAD, PATCH, POST, DELETE, OPTIONS, TRANCE。 那麼,什麼是multipart/form-data請求
http協議大家都知道是規定了以ASCII碼傳輸
,建立在tcp、ip
協議之上的應用層規範
。http協議的格式我們在上一篇部落格中已經提及,分為 請求行
,請求頭
,請求體
三部分。並且在我們傳送請求時,可以附加上相關的引數
。對於GET/PUT
等方法,引數都是按照"key=vaule"
的格式附加到URL
中的,其中key
和vaule
都是ASCII的字串。而當我們呼叫POST方法,將這些"key=vaule"
引數新增加到body中時,則需要在請求頭
中指明Content-Type
是application/x-www-form-urlencoded
,並在body中寫入這些引數,而不是附加在URL
中。
到目前為止,我所說的引數型別都是key=value格式
的,但如果我們想向伺服器上傳一個檔案,那麼這種key=value格式
顯然是不太合適的。
為了解決向伺服器上傳檔案及其他資訊的需求,人們對POST請求
作出擴充套件:在POST請求
中,支援Content-Type:multipart/form-data
的請求。 為了區別與其他型別的POST請求,我們在這裡可以先將這類POST請求稱作:
multipart/form-data請求
。
multipart/form-data請求
:
- 請求行上與其他
POST請求
一致,需要寫明POST方法,URL,協議型別。 - 但是在請求頭中,需要加上下面的頭資訊:
Content-Type: multipart/form-data; boundary=${bound}
首先,它聲明瞭請求體 body內容是符合multipart/form-data格式
。之後,指明body將會用到的分隔符boundary=${bound}
,${bound}
是我們指定的分隔符,用來分隔body的內容。 這個分隔符是可以任意自定義的,但是為了區別與body中的內容,我們都會將其定義為一個比較複雜的字串,如--------------------56423498738365
。
- 設定完請求頭後,接下來就是設定
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}--
上面是一個典型的multipart/form-data格式
的請求體。
其中${bound}為之前頭資訊中的分隔符
,如果頭資訊中規定為123,那麼這裡也要為123。
這個請求體是多個部分組成的:每一個部分都是以--分隔符
開始的,然後是該部分內容的描述資訊Content-Disposition:
,如果傳送的內容是一個檔案的話,那麼還會包含檔名資訊,以及檔案內容的型別。上面的第二個小部分其實是一個檔案體的結構然後一個回車,然後是描述資訊的具體內容
;
最後會以--分隔符--
結尾,表示請求體結束。
以上就是關於multipart/form-data請求
的概要知識。我們需要重點記憶的是form-data的body格式,在下面的程式碼分析中,我們會了解到,AFHTTPRequestSerializer是如何組裝multipart/form-data請求
的body的。
AFHTTPSessionManager & POST
我們先來看一下AFHTTPSessionManager
提供的關於POST的介面:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
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 DEPRECATED_ATTRIBUTE;
- (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;
介面比較多,一共6個,但其中的4個AF已經宣告為廢棄了。剩下的2個,才是AF所提供的POST介面。其餘4個最終都會呼叫到這2個POST介面之一:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
- (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;
這兩個介面的區別在於是否存在constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block引數
。
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
這個block引數,我們可以理解為AF的一個block回撥,當AF在組裝POST的form-data的body時,會回撥到這個block,使用者可以通過設定符合AFMultipartFormData協議
的formData
,將自己要上傳的檔案資訊附加到formData
中,AF會將檔案data新增的request 的body中。 具體是怎麼做的,我們稍後會看到。
這樣就是說,上面兩個POST介面,一個不需要上傳檔案,而另一個需要上傳檔案。是否需要上傳檔案的POST介面的實現是不一樣的。
我們先來看一下不需要上傳檔案的POST介面:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters headers:headers uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];
[dataTask resume];
return dataTask;
}
可以發現,它最終還是會呼叫AFHTTPSessionManager
的dataTaskWithHTTPMethod
方法。這和我們上一篇中介紹的GET/PUT
等方法的實現是一樣的。沿著我們上一篇中介紹的脈絡,就可以理解其實現。需要注意的是,與GET
等方法不同,最終POST的引數是寫在body中的,其Content-Type: application/x-www-form-urlencoded
。這裡就不再冗述。
我們重點來看一下上傳檔案版本的POST介面
的實現:
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
constructingBodyWithBlock:(void (^)(id<AFMultipartFormData> _Nonnull))block
progress:(void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
NSError *serializationError = nil;
// 1. 用 AFHTTPRequestSerializer組裝 multipart-Form 的body及相關的header,同時返回request
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
for (NSString *headerField in headers.keyEnumerator) {
[request addValue:headers[headerField] forHTTPHeaderField:headerField];
}
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
// 2. 對於multi-part form, 呼叫父類的upload task 方法,返回upload task
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
// 3. 返回task
return task;
}
可以看到,POST介面
與之前介紹過的GET/PUT
等介面的實現類似,均是用三步來提供task:
- 呼叫
AFHTTPRequestSerializer
的相關介面來組裝request
- 呼叫父類
AFURLSessionManager
的方法來返回task
- 呼叫
task resume
啟動任務,並向外返回該task
與之前介紹的介面的不同之處在於2點,
- 對於
AFHTTPRequestSerializer
, POST請求呼叫的是multipartFormRequestWithMethod
而不是之前的requestWithMethod
方法。 - 呼叫的父類方法,不是返回的
NSURLSessionDataTask
,而是呼叫uploadTaskWithStreamedRequest
介面。從這裡也可以看出,帶有block回撥的POST介面,是設計用來向伺服器上傳檔案的。
multipartFormRequestWithMethod
讓我們把目光移到AFHTTPSessionManager
的HTTP請求組裝器AFHTTPRequestSerializer
中,來看一下它是怎麼組裝POST request的。
AFHTTPRequestSerializer
會呼叫multipartFormRequestWithMethod
來組裝上傳檔案的POST請求:
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]); // 肯定不是GET 和 HEAD方法
// 1. 先獲取request。 由於POST方法的parameters要用Form格式放在 body中,所以這裡的 parameters 引數填寫nil
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
// 2. 生成AFStreamingMultipartFormData物件,用來儲存將會新增到POST body中的parameters
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
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]];
}
}
}
// 如果有使用者傳入的 construct Body block, 則會呼叫。這裡主要是向用戶提供回撥時機,讓使用者可以傳入要新增到POST body的file data
if (block) {
block(formData);
}
// 3. 將formData 真正附加到request 的body中 並返回
return [formData requestByFinalizingMultipartFormData];
}
multipartFormRequestWithMethod
方法會分3個步驟來組裝POST請求:
- 用
requestWithMethod
方法,來返回對應的POST request
。 - 生成
AFStreamingMultipartFormData
物件form data
,來儲存要新增到POST request中的body 資料。 - 呼叫
form data
的requestByFinalizingMultipartFormData
方法,將form data附加到POST request中。
關於第1個步驟,我們在上一篇中已經分析過,不再多說。重點是第2,3步驟,POST的body data是如何生成的,body data又是如何附加到POST request中的。
Generate body data
AFHTTPRequestSerializer
是利用AFStreamingMultipartFormData
物件來生成body data的。從類的命名就可以看出,AFStreamingMultipartFormData
是通過資料流
來提供body data的(主要是要上傳的file data)。
關於生成body data的程式碼摘抄出來如下:
// 2. 生成AFStreamingMultipartFormData物件,用來儲存將會新增到POST body中的parameters
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
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]];
}
}
}
// 如果有使用者傳入的 construct Body block, 則會呼叫。這裡主要是向用戶提供回撥時機,讓使用者可以傳入要新增到POST body的file data
if (block) {
block(formData);
}
上面的內容可以分為兩部分:
(1) 生成AFStreamingMultipartFormData物件
,並傳入parameters
(2)呼叫block
回撥,讓AFStreamingMultipartFormData物件
接受來自使用者的檔案data
。
我們先來看一下AFStreamingMultipartFormData物件
是如何生成的:
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
@interface AFStreamingMultipartFormData ()
@property (readwrite, nonatomic, copy) NSMutableURLRequest *request;
@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding;
@property (readwrite, nonatomic, copy) NSString *boundary;
@property (readwrite, nonatomic, strong) AFMultipartBodyStream *bodyStream;
@end
- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest
stringEncoding:(NSStringEncoding)encoding
{
self = [super init];
if (!self) {
return nil;
}
self.request = urlRequest;
self.stringEncoding = encoding;
self.boundary = AFCreateMultipartFormBoundary();
self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
return self;
}
form data物件
的初始化函式很簡單,就是記錄了從外界傳入的引數:urlRequest和encoding
型別。同時,初始化了其成員AFMultipartBodyStream物件
。
其中,boundary
成員是form 的分隔符,是由AFCreateMultipartFormBoundary()
函式生成的一個隨機字串。
而AFMultipartBodyStream* bodyStream
,則用來記錄POST的body data
。關於它是如何記錄的,我們稍後會做分析。
知道了AFStreamingMultipartFormData form data物件
是如何生成的後,我們回過頭來看一下引數是如何附加到form data中的:
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]];
}
}
}
這裡用到了上一篇部落格中提到的AFQueryStringPairsFromDictionary
方法以及AFQueryStringPair
型別。然後,AF會將AFQueryStringPair
中儲存的value轉換為NSData
型別。
將vaule轉換為NSData
型別後,呼叫form data的:
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
將data和其對應的key附加到form data中。
我們來看一下AFStreamingMultipartFormData
的appendPartWithFormData:name:
方法是如何實現的:
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
{
NSParameterAssert(name);
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];
// 附加一個AFHTTPBodyPart
[self appendPartWithHeaders:mutableHeaders body:data];
}
form 首先會生成一個表示該data節點頭的字典mutableHeaders,然後存入如下內容:
key: @"Content-Disposition" value:@"form-data; name=\"%@\"", name
然後,將header 和 data 組合起來,儲存到AFHTTPBodyPart
中。
// 附加一個AFHTTPBodyPart
[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;
bodyPart.boundary = self.boundary;
bodyPart.bodyContentLength = [body length];
bodyPart.body = body;
[self.bodyStream appendHTTPBodyPart:bodyPart];
}
傳入的引數被form data轉換為了對應的AFHTTPBodyPart
,然後,AFHTTPBodyPart
會被AFMultipartBodyStream *bodyStream
新增到其HTTPBodyPart
中。
OK,到這裡,我們已經涉及到了好幾個類的關係。我們現在先暫停一下,總結一下上面AFHTTPSessionManager
是如何為POST請求生成form data body的。上面涉及到的幾個類的關係如下圖:
通過上圖,這幾個類之間的關係會清楚許多。首先,我們要生成form data型別
的POST請求
,需要呼叫AFHTTPRequestSerializer
的相關方法,利用AFHTTPRequestSerializer
來生成對應的request,這個和其他請求GET/PUT
等是一樣的。
而在AFHTTPRequestSerializer
, 會生成一個AFStreamingMultipartFormData
物件來儲存所有POST 請求的body data
。
在AFStreamingMultipartFormData
的內部實現中,會針對每一個POST 請求
引數,生成一個AFHTTPBodyPart
物件,然後這些物件又會儲存到其成員變數AFMultipartBodyStream物件
中。
如果細心的話,可以注意到,用於儲存引數的AFMultipartBodyStream類
是繼承自NSInputStream
的,這也就暗示了,最終將這些引數附加到request中時,是通過流的方式進行的,這對於上傳大的檔案,很有幫助。
對於各個類的實現細節,我們暫不去管,首先從整體上把握類之間的關係。在稍後的部分中,我們將會進一步分析類實現的細節。
上面是關於parameter的儲存方式,如果使用者需要上傳檔案的話,AF會呼叫block回撥來給使用者上傳檔案的時機:
// 如果有使用者傳入的 construct Body block, 則會呼叫。這裡主要是向用戶提供回撥時機,讓使用者可以傳入要新增到POST body的file data
if (block) {
block(formData);
}
這裡的block定義是:
(void (^)(id <AFMultipartFormData> formData))block
這裡的block會傳入一個符合AFMultipartFormData協議
的物件, 這裡傳入的是AFStreamingMultipartFormData
物件。
這裡有個小思考,為什麼block的引數是一個協議型別,而不是具體的AFStreamingMultipartFormData
型別? 其實我們之間將block定義改寫為:
(void (^)(AFStreamingMultipartFormData *formData))block
在邏輯上也是完全行得通的。但是,這會對外部物件過多的暴露AF
的實現細節,或者說和使用者需求功能不相干的細節,也暴露給了使用者,這樣就對程式碼的誤用留下了隱患,同時,對於使用者的使用也造成了不必要的麻煩。
AF
在這裡的處理是向外暴露一個AFMultipartFormData協議
,該協議只有和使用者上傳檔案相關的介面,而遮蔽了AFStreamingMultipartFormData
中如boundary
,request
等無關的屬性。
這就是通過協議
向外提供了一個窄介面
,遮蔽了無關的實現。這也是我們可以借鑑的一個面向物件程式設計的技巧。
我們來看一下AFMultipartFormData協議
都定義了那些介面:
@protocol AFMultipartFormData
// 將file data附加到form data中
- (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;
// 將headers資訊新增到form data中,並跟一個body data
- (void)appendPartWithHeaders:(nullable NSDictionary <NSString *, NSString *> *)headers
body:(NSData *)body;
// 考慮到3G頻寬的限制,檔案流可能會報錯:"request body stream exhausted"。因此AF提供了一個可以設定包大小和延遲時間的介面。
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
@end
可以看到,AFMultipartFormData協議
主要是提供了三個功能:
(1)使用者上傳檔案data
(2)新增form 的headers
(3)設定form data的流 包大小和延遲時間。
我們先來看使用者上傳data相關的介面在AFStreamingMultipartFormData
中是如何實現的:
那其中一個介面做例子:
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSParameterAssert(fileName);
NSParameterAssert(mimeType);
// 檢測檔案的相關屬性,如果有錯誤,直接返回NO及error
if (![fileURL isFileURL]) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
} else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
}
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error];
if (!fileAttributes) {
return NO;
}
// 組裝form data form data中關於file的節點的頭資訊
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
// 將頭資訊以及file data的相關資訊儲存為AFHTTPBodyPart.
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = mutableHeaders;
bodyPart.boundary = self.boundary;
bodyPart.body = fileURL; // 注意AFHTTPBodyPart的body屬性,是id型別,可以直接儲存file URL,file data, 或NSInputStream
bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
[self.bodyStream appendHTTPBodyPart:bodyPart];
return YES;
}
其餘的介面都大同小異,讀者可以自行分析。
我們再來看一下AFStreamingMultipartFormData
中又如何設定包的大小和延遲時間,這裡只是簡單的記錄下來:
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay
{
self.bodyStream.numberOfBytesInPacket = numberOfBytes;
self.bodyStream.delay = delay;
}
Append body data to POST Request
通過上面的分析,我們知道,form data是如何儲存傳入POST body的headers和file data資訊的。這其中涉及到三個類: AFStreamingMultipartFormData
,AFMultipartBodyStream
,AFHTTPBodyPart
。
對於multi form data中的每一個節(被分隔符分割),都是對應一個AFHTTPBodyPart
,而所有的這些節,都被AFStreamingMultipartFormData
統一append 到AFMultipartBodyStream
中。
到目前為止,AFStreamingMultipartFormData
和POST request
還沒有發生實質性關係,AFStreamingMultipartFormData
中僅是儲存了相關body data,但還未將這些body data附加到request中。
當呼叫AFStreamingMultipartFormData
的requestByFinalizingMultipartFormData
時,會設定POST request,並將其中儲存的POST form data資訊附加到request 上:
return [formData requestByFinalizingMultipartFormData];
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
if ([self.bodyStream isEmpty]) {
return self.request;
}
// Reset the initial and final boundaries to ensure correct Content-Length
[self.bodyStream setInitialAndFinalBoundaries];
// 將request的body stream設定為self.bodyStream。使得POST request的body和self.bodyStream建立關聯(AFMultipartBodyStream)
[self.request setHTTPBodyStream:self.bodyStream];
// 設定request 的請求頭為:Content-Type:multipart/form-data; boundary=self.boundary, 表明request body是multi-part form型別
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
[self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
return self.request;
}
上面的邏輯比較好理解,重點是設定request的bodyStream:
[self.request setHTTPBodyStream:self.bodyStream];
當把self.bodyStream(AFMultipartBodyStream
)設定為request的body stream後,當request請求被髮送時,會自動呼叫read方法:
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
// 如果當前的stream 沒有開啟,則直接返回0
if ([self streamStatus] == NSStreamStatusClosed) {
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) { // 讀取iOS stream指定的大小 或 使用者設定的最小包大小 (以min為準)
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) { // update currentHTTPBodyPart
break;
}
} else {
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead; // 本次while迴圈可以讀取的buffer 大小: 本次read:maxLength允許讀取的最大位元組數 - 已經讀取的位元組數
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength]; //讀取資料到buffer中,並返回讀取到的位元組數
if (numberOfBytesRead == -1) { // 讀取位元組數等於-1 表示讀取失敗,退出迴圈
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
totalNumberOfBytesRead += numberOfBytesRead; // 更新總的位元組數
if (self.delay > 0.0f) { // 根據使用者設定的延遲時間 ,休息一下
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
return totalNumberOfBytesRead; // 返回讀取的總資料
}
在AFMultipartBodyStream
的read:maxLength
方法中,會依次遍歷其儲存的AFHTTPBodyPart
,並呼叫AFHTTPBodyPart
的read:maxLength
方法:
// AFHTTPBodyPart
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
// 讀取buffer, 並更新phase
NSInteger totalNumberOfBytesRead = 0;
// AFHTTPBodyPart 讀取stream 時,會分為三個/四個階段 對應了multipart form data的結構
// phase 1. 開頭的分隔符
if (_phase == AFEncapsulationBoundaryPhase) {
NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
// phase 2. form 節點的header資訊
if (_phase == AFHeaderPhase) {
NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
// phase 3. form body
if (_phase == AFBodyPhase) {
NSInteger numberOfBytesRead = 0;
numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
if (numberOfBytesRead == -1) {
return -1;
} else {
totalNumberOfBytesRead += numberOfBytesRead;
if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
[self transitionToNextPhase];
}
}
}
// phase 4. form 的結束符(如果是最後一個 form 節點才會有這個階段)
if (_phase == AFFinalBoundaryPhase) {
NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
return totalNumberOfBytesRead;
}
因為iOS每次input stream
讀取的位元組流大小並不能預知,因此,AFHTTPBodyPart
會用變數_phase
來記錄body data
已經讀取到了哪一個部分。當下次input stream
再來讀取資料時,會按照當前的_phase
來讀取AFHTTPBodyPart
的不同內容。
對於AFHTTPBodyPart
的內容讀取,會呼叫AFHTTPBodyPart
的
- (NSInteger)readData:(NSData *)data intoBuffer:(uint8_t *)buffer maxLength:(NSUInteger)length
方法:
- (NSInteger)readData:(NSData *)data
intoBuffer:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length));
[data getBytes:buffer range:range];
_phaseReadOffset += range.length;
if (((NSUInteger)_phaseReadOffset) >= [data length]) {
[self transitionToNextPhase];
}
return (NSInteger)range.length;
}
在readData
方法中,會記錄在當前階段已經讀取資料的偏移值_phaseReadOffset
。在下次讀取時,會從偏移值的地方開始,讀取data
剩餘的資料。如果_phaseReadOffset >= [data length]
,則說明當前階段的data
已經讀取完畢,呼叫transitionToNextPhase
轉入到下一個階段。
當然,對於AFHTTPBodyPart
的 AFBodyPhase階段
所對應的inputStream屬性data,因為本身就是流,因此不用記錄_phaseReadOffset
,而直接呼叫NSInputStream
的read:maxLength
方法即可。在AFBodyPhase階段
,需要手動判斷流狀態,來轉入到下一個階段:
if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
[self transitionToNextPhase];
}
然我們在看一下,AFHTTPBodyPart
是如何轉入到下一個階段的:
- (BOOL)transitionToNextPhase {
if (![[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self transitionToNextPhase]; // transitionToNextPhase 必須在主執行緒呼叫
});
return YES;
}
// 根據當前階段,轉換到下一個階段
switch (_phase) {
case AFEncapsulationBoundaryPhase:
_phase = AFHeaderPhase;
break;
case AFHeaderPhase:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
case AFBodyPhase:
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
case AFFinalBoundaryPhase:
default:
_phase = AFEncapsulationBoundaryPhase; // 預設或初始化為 AFEncapsulationBoundaryPhase(包裝分隔符) 階段
break;
}
_phaseReadOffset = 0;
return YES;
}
在transitionToNextPhase方法中,轉換階段要做的事情多數時間很簡單:
- 修改當前的_phase等於下一個階段
_phase =nextPhase
- 清空當前階段已經讀取的偏移量
_phaseReadOffset = 0
;
這裡需要注意的是兩點,第一,transitionToNextPhase
會保證在main執行緒
呼叫:
if (![[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self transitionToNextPhase]; // transitionToNextPhase 必須在主執行緒呼叫
});
return YES;
}
第二,對於AFBodyPhase
,會將input stream
附加到main 執行緒的runloop
中,並開啟流:
case AFHeaderPhase:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
當AFBodyPhase
階段結束,轉入AFFinalBoundaryPhase
階段前,需要將流關閉:
case AFBodyPhase:
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
上面就是POST 請求的multipart form data的組裝過程。我們要留意的是NSInputStream的使用方式,即必須附加到runloop上:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
這樣做可以避免在沒有資料可讀時阻塞代理物件的操作。
總結
在這一篇部落格中,我們瞭解了AFHTTPSessionManager
中關於POST介面的實現。同時,我們也瞭解了HTTP協議中,multipart form data的body格式。
至此,對於AFHTTPSessionManager
的分析也就告一段落。同時,我們也瞭解了AFHTTPRequestSerializer的大部分介面。
接下來,我們將會對AFHTTPRequestSerializer
剩下的介面進行分析,同時會了解reponse的解析類AFHTTPResponseSerializer
和AFJSONResponseSerializer
。