AFNetworking 2.x 阅读笔记(四)

前一篇文章讲解了requestSerialization中关于参数格式化,http headers设置,timeout等属性设置的内容。本文来剖析请求序列化过程中另一个重要的部分: multipart post 请求。multipart所有的内容都是围绕着AFStreamingMultipartFormData类,AFMultipartBodyStream类,AFMultipartFormData协议。

0 涉及到类的关系

在bang's blog中他梳理的很清楚了:

通过constructingBodyWithBlock向使用者提供了一个AFStreamingMultipartFormData对象,调这个对象的几种append方法就可以添加不同类型的数据,包括FileURL/NSData/NSInputStream,AFStreamingMultipartFormData内部把这些append的数据转成不同类型的AFHTTPBodyPart,添加到自定义的AFMultipartBodyStream里。最后把AFMultipartBodyStream赋给原来NSMutableURLRequest的bodyStream。NSURLConnection发送请求时会读取这个bodyStream,在读取数据时会调用这个bodyStream的-read:maxLength:方法,AFMultipartBodyStream重写了这个方法,不断读取之前append进来的AFHTTPBodyPart数据直到读完。
AFHTTPBodyPart封装了各部分数据的组装和读取,一个AFHTTPBodyPart就是一个数据块。实际上三种类型(FileURL/NSData/NSInputStream)的数据在AFHTTPBodyPart都转成NSInputStream,读取数据时只需读这个inputStream。inputStream只保存了数据的实体,没有包括分隔符和头部,AFHTTPBodyPart是边读取变拼接数据,用一个状态机确定现在数据读取到哪一部份,以及保存这个状态下已被读取的字节数,以此定位要读的数据位置,详见AFHTTPBodyPart的-read:maxLength:方法。
AFMultipartBodyStream封装了整个multipart数据的读取,主要是根据读取的位置确定现在要读哪一个AFHTTPBodyPart。AFStreamingMultipartFormData对外提供友好的append接口,并把构造好的AFMultipartBodyStream赋回给NSMutableURLRequest,关系大致如下图:

AFNetworking 2.x 阅读笔记(四)_第1张图片
几个类的关系图

1 multipart请求基础知识

requestSerializer中另外一个重要的部分是构建multipart请求,关于multipart的基础知识可以参考:http://stackoverflow.com/questions/16958448/what-is-http-multipart-request

简单来说,multipart请求有如下几个特点:

  • multipart/form-data的基础方法是post,也就是说是由post方法来组合实现的
  • multipart/form-data与post方法的不同之处:请求头,请求体。
  • multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post的内容。例如:Content-Type: multipart/form-data; boundary=XXXXXX}
    引用stackoverflow上面的一个实例:
POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://aram/~martind/banner.htm
Content-Type: multipart/form-data; boundary=----------287032381131322
Content-Length: 514

------------287032381131322
Content-Disposition: form-data; name="name"

brownfeng
------------287032381131322
Content-Disposition: form-data; name="gifpic"; filename="g.gif"
Content-Type: image/gif

GIF87a.............,...........D..;
------------287032381131322
Content-Disposition: form-data; name="textfile"; filename="text.txt"
Content-Type: text/plain

… contents of text.txt …;
------------287032381131322--

以上表示数据name=brown,和一个gif图片以及一个txt文件,filename是文件名, … contents of text.txt …以及GIF87a.............,...........D..是文件实体内容。分隔符----------287032381131322是可以自定义的,写在HTTP头部里:Content-type: multipart/form-data, boundary=----------287032381131322
每一个部分都有自己的头部,表明这部分的数据类型以及其他一些参数,例如文件名,普通字段的key,content-Type等。最后一个分隔符会多加两横,表示数据已经结束:------------287032381131322--

一般而言,构造Multipart里的数据,按照上述格式拼接数据。写入NSURLRequest的http body字段即可,但是当发送文件过大,这种方式就可使用了。第二种方法是在临时文件拼接,加入格式化的内容,然后将零时文件的file stream设置成request 的http body stream。

AFNetworking中,并非使用上述两种方法,是边拼数据边上传数据。

2 multipart post的调用栈

AFNetwoking官方用例中,调用方法如下:

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *parameters = @{@"foo": @"bar"};
NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];
[manager POST:@"http://example.com/resources.json" parameters:parameters constructingBodyWithBlock:^(id formData) {
    [formData appendPartWithFileURL:filePath name:@"image" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"Success: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"Error: %@", error);
}];

在constructingBodyWithBlock中,将只需要传入filePathurl,以及name即可。manager的POST方法中,会调用requestSerialization的以下方法创建request。其中会通过AFStreamingMultipartFormData以及其配套方法完成multi part body 的组装,并设置到request中。

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id  formData))block
                                                  error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);

    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
//传入request参数,创建AFStreamingMultipartFormData对象
    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
//如果有parameters值,则转化成data,并写入formData
    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];
            }
//向formData中加入普通参数
            if (data) {
                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
    }
//向formData中添加其他的内容-appendPartWithFileURL:name:error: 等方法
    if (block) {
        block(formData);
    }
//返回最终完成的以后的request
    return [formData requestByFinalizingMultipartFormData];
}

3 AFStreamingMultipartFormData类以及AFMultipartFormData protocol

所有关于multipart的方法都与核心类AFStreamingMultipartFormData有关,它最重要的属性就是body part的分隔符boundary,以及AFMultipartBodyStream *bodyStream,同时它会遵守AFMultipartFormData协议,该协议是为AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:方法中的block内的添加data准备的,传入到formdata的输出可以包括FileURL/NSData/NSInputStream。协议里面的appendXXXXX方法将传入的formdata数据转成不同类型的部分(AFNetworking用AFHTTPBodyPart类来表示)。

@interface AFStreamingMultipartFormData : NSObject 
- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest
                    stringEncoding:(NSStringEncoding)encoding;
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData;
@end

@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

在AFStreamingMultipartFormData的init方法中,会随机生成一定格式的multipart boundary属性,另一个重点是初始化bodyStream属性。

- (id)initWithURLRequest:(NSMutableURLRequest *)urlRequest
          stringEncoding:(NSStringEncoding)encoding
{
    ...
    self.request = urlRequest;
    self.stringEncoding = encoding;
    self.boundary = AFCreateMultipartFormBoundary();
//初始化bodyStream
    self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
    return self;
}
//生成boundary的函数
static NSString * AFCreateMultipartFormBoundary() {
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}

协议的内容是将NSData,NSFile等数据添加到AFStreamingMultipartFormData的bodyStream中。

@protocol AFMultipartFormData
//Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary.
//The filename and MIME type for this data in the form will be automatically generated, using the last path component of the `fileURL` and system associated MIME type for the `fileURL` extension, respectively.

- (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);
//一定要是fileURL
    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) {//reachable
        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;
    }
//向header dict中加入[Content-Disposition:form-data; name="%@"; filename="%@"],注意转意
    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
//向header dict中加入[Content-Type:mimeType]
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];
//创建AFHTTPBodyPart对象,代表一个bodypart,并设置其重要属性
    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = mutableHeaders;
    bodyPart.boundary = self.boundary;
    bodyPart.body = fileURL;
    bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
//加入到body stream中
    [self.bodyStream appendHTTPBodyPart:bodyPart];

    return YES;
}

对AFStreamingMultipartFormData以及AFMultipartFormData protocol的总结:

  • init AFStreamingMultipartFormData时候传入request,encoding,boundary以及bodyStream
  • 调用AFMultipartFormData协议实现appendXXX方法:
    • 根据传入的类型--file,data等等--添加headers的Content-Disposition、MIME-Type.
    • 创建AFHTTPBodyPart临时对象
  • 将AFHTTPBodyPart临时对象加入到AFStreamingMultipartFormData对象的bodyStream中
  • 调用block中完成同样的事情,将part内容加入Formdata bodystream
  • 调用 [formData requestByFinalizingMultipartFormData]返回组装完成的request

4 AFMultipartBodyStream的调用使用

对于iOS中流概念理解比较薄弱可以先看 apple 官方文档: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Streams/Streams.html

直接引用 bang's blog:

NSURLRequest的setHTTPBodyStream接受的是一个NSInputStream*参数,那我们要自定义inputStream的话,创建一个NSInputStream的子类传给它是不是就可以了?实际上不行,这样做后用NSURLRequest发出请求会导致crash,提示[xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。

这是因为NSURLRequest实际上接受的不是NSInputStream对象,而是CoreFoundation的CFReadStreamRef对象,因为CFReadStreamRef和NSInputStream是toll-free bridged,可以自由转换,但CFReadStreamRef会用到CFStreamScheduleWithRunLoop这个方法,当它调用到这个方法时,object-c的toll-free bridging机制会调用object-c对象NSInputStream的相应函数,这里就调用到了_scheduleInCFRunLoop:forMode:,若不实现这个方法就会crash。详见这篇文章。

5 Request 中bodyStream内容设置完成以后的工作

在前面multipartFormRequstWithMethod...方法最后会调用return [formData requestByFinalizingMultipartFormData], 这个方法的源码如下

- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {
        return self.request;
    }
// 设置bodyStream中的初始和结束boundaries,设置 request的body stream
    [self.bodyStream setInitialAndFinalBoundaries];
    [self.request setHTTPBodyStream:self.bodyStream];

//设置request header 'Content-Type' 和 'Content-Length'字段
    [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;
}

最后完整的代码解析

可以查看 bangs blog: http://blog.cnbang.net/tech/2371/

·

你可能感兴趣的:(AFNetworking 2.x 阅读笔记(四))