【AFNetworking 分析】POST 上传多表单类型数据

孤街浪徒 提交于 2021-02-06 20:34:21

[TOC]

Pre

  • AFNetworking分析版本:4.0.1

调试所用代码(AFNetworking demo iOS)

XCTestExpectation *expectation = [self expectationWithDescription:@"Request should succeed"];
[self.sessionManager.requestSerializer setValue:@"default value"
                             forHTTPHeaderField:@"field"];
[self.sessionManager
 POST:@"post"
 parameters:@{@"key":@"value"}
 headers:@{@"field":@"value"}
 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) {
     XCTAssertTrue([task.originalRequest.allHTTPHeaderFields[@"field"] isEqualToString:@"value"]);
     XCTAssertTrue([responseObject[@"files"][@"DataName"] isEqualToString:@"Data"]);
     XCTAssertTrue([responseObject[@"form"][@"key"] isEqualToString:@"value"]);
     [expectation fulfill];
 }
 failure:nil];
[self waitForExpectationsWithCommonTimeout];

AFMultipartFormData协议

AFHTTPSessionManager.h中,我们可以看到这样的方法

- (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;

这里的(nullable void (^)(id <AFMultipartFormData> formData))block是一个接受单个参数并向HTTP的body添加data的block。该block遵循AFMultipartFormData协议,在block中设置需要上传的文件。

多表单参数的请求封装(上述POST方法的实现流程)

封装NSMutableURLRequest对象

    NSError *serializationError = nil;
    NSMutableURLRequest *request = 
		[self.requestSerializer multipartFormRequestWithMethod:@"POST" 
		 URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] 
		 parameters:parameters 
		 constructingBodyWithBlock:block
		 error:&serializationError];
  1. 基本判断。方法不能为空且不能为GETHEAD
    NSParameterAssert(method);
    NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
  1. 基本请求封装(此处流程见【AFNetworking 分析】NSURLSessionManager & NSHTTPSessionManager 初始化及请求封装
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method 
						URLString:URLString 
						parameters:nil error:error];
  1. 用request创建一个AFStreamingMultipartFormData数据对象
__block AFStreamingMultipartFormData *formData =
		[[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest 
							stringEncoding:NSUTF8StringEncoding];

看一下AFStreamingMultipartFormData的整体结构。 初始化方法内部:

self.request = urlRequest; //对request进行持有
self.stringEncoding = encoding; //NSUTF8StringEncoding
self.boundary = AFCreateMultipartFormBoundary();
self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];

3.1 这里的%08X为十六进制,即生成两个十六进制随机数拼接在字符串后面,比如"Boundary+919640865615927E"

static NSString * AFCreateMultipartFormBoundary() {
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}

3.2 AFMultipartBodyStream对象初始化

  • AFMultipartBodyStream所含属性
  • 初始化方法
self.stringEncoding = encoding;
self.HTTPBodyParts = [NSMutableArray array];
self.numberOfBytesInPacket = NSIntegerMax;
  1. 参数处理(若parameter不为空)

4.1 先通过AFQueryStringPairsFromDictionary()函数对parameters进行处理,返回的是数组(此处流程见【AFNetworking 分析】NSURLSessionManager & NSHTTPSessionManager 初始化及请求封装

NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}

NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
	···
}
  • 用文首调试代码运行到这里可以看到,返回了一个数组,数组中包含一个AFQueryStringPair对象
  • 对象内容为: 4.2 遍历返回的AFQueryStringPair数组,对数据进行处理封装为NSData对象,并根据data和name构建Request的header和body
            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];
            }

4.3 根据data为request设置headers和body

            if (data) {
                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
- (void)appendPartWithFormData:(NSData *)data
                          name:(NSString *)name
{
    NSParameterAssert(name);

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];

    [self appendPartWithHeaders:mutableHeaders body:data];
}

走到这里看一下mutableHeaders

好的继续走到appendPartWithHeaders: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];

也就是说,将封装好的bodyPart添加至AFStreamingMultipartFormData -> AFMultipartBodyStream *bodyStream属性 -> 可变数组属性HTTPBodyParts中。 5. block处理formData(其实还是往formData中添加数据)

    if (block) {
        block(formData);
    }
  1. 对多表单数据的再处理,比如设置一下MultipartRequestbodyStream或者其特有的content-type等等
return [formData requestByFinalizingMultipartFormData];

6.1 若本AFStreamingMultipartFormData对象的bodyStream为空,则直接返回自己持有的request对象

    if ([self.bodyStream isEmpty]) {
        return self.request;
    }

6.2 重置初始和最终的boundaries以确保正确的Content-length

[self.bodyStream setInitialAndFinalBoundaries];
  • 在该方法中,首先判断本AFMultipartBodyStream *bodyStream对象的HTTPBodyParts数组中是否有元素,若有
// a. 逐个设置每个AFHTTPBodyPart元素的hasInitialBoundary和hasFinalBoundary为NO
        for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) {
            bodyPart.hasInitialBoundary = NO;
            bodyPart.hasFinalBoundary = NO;
        }
// b. 将本bodyStream的首个元素的hasInitialBoundary设置为YES,hasFinalBoundary设置为NO
        [[self.HTTPBodyParts firstObject] setHasInitialBoundary:YES];
        [[self.HTTPBodyParts lastObject] setHasFinalBoundary:YES];
    }
}

6.3 为request设置bodyStream

  • 那么,设置了bodyStream后什么时候对其进行处理?
[self.request setHTTPBodyStream:self.bodyStream];

我们跳转进入HTTPBodyStream理解一下官方注释:

HTTPBodyStream
将request的body设置为指定流的内容。
所提供的流应不开启;request将接管流的delegate。整个流的内容将作为请求的HTTP body进行传输。
注意,主体流和主体数据(由setHTTPBody:设置)是互斥的。
—设置一个会清除另一个。

  • 查看文件结构也可以发现

    说明可能后面会用到这里的方法,先放一放。

6.4 为request设置Content-typeContent-length

 [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];

6.5 为request设置Content-length

 [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
  • 计算bodyStream的contentLength,即是计算bodyStream中的HTTPBodyParts数组中每个AFHTTPBodyPart元素的contentLength之和(以下为计算AFHTTPBodyPart元素的contentLength代码)
    unsigned long long length = 0;

    NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
    length += [encapsulationBoundaryData length];

    NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
    length += [headersData length];

    length += _bodyContentLength;

    NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
    length += [closingBoundaryData length];

至今为止,bodyStream封装完毕,多表单必备的headers也已经写进了request

为请求添加headers

 for (NSString *headerField in headers.keyEnumerator) {
     [request setValue:headers[headerField] forHTTPHeaderField:headerField];
 }

请求封装序列化错误处理

 if (serializationError) {
     if (failure) {
         dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
             failure(nil, serializationError);
         });
     }
     return nil;
 }
  • 值得一提的是,这里的completionQueue属性来自于上层NSURLSessionManager,暴露在头文件中,若未指定的话则操作一般会放在主队列上。我们可以通过给它赋其他队列的方式(异步等)来使我们想要进行的操作异步执行(上传埋点等等),避免阻塞主线程。

NSURLSessionDataTask封装

    __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);
            }
        }
    }];
  1. 调用NSURLSessionuploadTaskWithStreamedRequest:方法将request封装为NSURLSessionUploadTask对象
    NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithStreamedRequest:request];
  1. 为上传任务添加代理(与addDelegateForDataTask:方法处理类似,详情见【AFNetworking 分析】NSURLSessionManager & NSHTTPSessionManager 初始化及请求封装
    [self addDelegateForUploadTask:uploadTask progress:uploadProgressBlock completionHandler:completionHandler];

执行任务

[task resume];

task生成后,就会去建立连接,request读取、编码等 bodyStream一定是连接服务器成功之后才会处理!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!