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,关系大致如下图:

几个类的关系图

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<AFMultipartFormData> 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 <AFMultipartFormData> 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 <AFMultipartFormData>
- (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/

·

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容