1. 程式人生 > >AFNetWorking(3.0)原始碼分析(四)——AFHTTPSessionManager(2)

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中的,其中keyvaule都是ASCII的字串。而當我們呼叫POST方法,將這些"key=vaule"引數新增加到body中時,則需要在請求頭中指明Content-Typeapplication/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請求

  1. 請求行上與其他POST請求一致,需要寫明POST方法,URL,協議型別。
  2. 但是在請求頭中,需要加上下面的頭資訊:
Content-Type: multipart/form-data; boundary=${bound}  

首先,它聲明瞭請求體 body內容是符合multipart/form-data格式 。之後,指明body將會用到的分隔符boundary=${bound}${bound} 是我們指定的分隔符,用來分隔body的內容。 這個分隔符是可以任意自定義的,但是為了區別與body中的內容,我們都會將其定義為一個比較複雜的字串,如--------------------56423498738365

  1. 設定完請求頭後,接下來就是設定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;
}

可以發現,它最終還是會呼叫AFHTTPSessionManagerdataTaskWithHTTPMethod方法。這和我們上一篇中介紹的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:

  1. 呼叫AFHTTPRequestSerializer的相關介面來組裝request
  2. 呼叫父類AFURLSessionManager的方法來返回task
  3. 呼叫task resume啟動任務,並向外返回該task

與之前介紹的介面的不同之處在於2點,

  1. 對於AFHTTPRequestSerializer, POST請求呼叫的是multipartFormRequestWithMethod而不是之前的requestWithMethod方法。
  2. 呼叫的父類方法,不是返回的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請求:

  1. requestWithMethod方法,來返回對應的POST request
  2. 生成AFStreamingMultipartFormData物件form data,來儲存要新增到POST request中的body 資料。
  3. 呼叫form datarequestByFinalizingMultipartFormData方法,將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中。
我們來看一下AFStreamingMultipartFormDataappendPartWithFormData: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中如boundaryrequest等無關的屬性。

這就是通過協議向外提供了一個窄介面,遮蔽了無關的實現。這也是我們可以借鑑的一個面向物件程式設計的技巧。

我們來看一下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資訊的。這其中涉及到三個類: AFStreamingMultipartFormDataAFMultipartBodyStreamAFHTTPBodyPart

對於multi form data中的每一個節(被分隔符分割),都是對應一個AFHTTPBodyPart,而所有的這些節,都被AFStreamingMultipartFormData統一append 到AFMultipartBodyStream中。

到目前為止,AFStreamingMultipartFormDataPOST request還沒有發生實質性關係,AFStreamingMultipartFormData中僅是儲存了相關body data,但還未將這些body data附加到request中。

當呼叫AFStreamingMultipartFormDatarequestByFinalizingMultipartFormData時,會設定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; // 返回讀取的總資料
}

AFMultipartBodyStreamread:maxLength方法中,會依次遍歷其儲存的AFHTTPBodyPart,並呼叫AFHTTPBodyPartread: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轉入到下一個階段。

當然,對於AFHTTPBodyPartAFBodyPhase階段所對應的inputStream屬性data,因為本身就是流,因此不用記錄_phaseReadOffset,而直接呼叫NSInputStreamread: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方法中,轉換階段要做的事情多數時間很簡單:

  1. 修改當前的_phase等於下一個階段 _phase =nextPhase
  2. 清空當前階段已經讀取的偏移量_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的解析類AFHTTPResponseSerializerAFJSONResponseSerializer