AFNeiworking 3.0源码分析 - AFURLRequestSerialization

AFURLRequestSerialization模块主要做的两样事情:
1.创建普通NSMutableURLRequest请求对象
2.创建multipart NSMutableURLRequest请求对象
此外还有比如:
处理查询的 URL 参数

AFURLRequestSerialization是一个协议,它定义了一个方法:

- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(nullable id)parameters
                                        error:(NSError * _Nullable __autoreleasing *)error

AFHTTPRequestSerializer及其子类遵循这个协议。

现在从AFHTTPRequestSerializer这个类入手分析。

1.创建普通NSMutableURLRequest请求

//创建一般的NSMutableURLRequest对象,设置HTTPMethod、请求属性、HTTPHeader和处理参数
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);

    NSURL *url = [NSURL URLWithString:URLString];

    NSParameterAssert(url);
    //创建URLRequest、设置请求的方法
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;
    //通过mutableObservedChangedKeyPaths设置NSMutableURLRequest请求属性
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            //用KVC的方式,给request设置属性值
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }
    //设置http header和参数(拼接到url还是放到http body中)
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];

    return mutableRequest;
}

这个方法做了三件事:
1.设置request请求类型mutableRequest.HTTPMethod = method;

2.设置request的一些属性。
2.1这里用到了AFHTTPRequestSerializerObservedKeyPaths()c函数。

//单例。观察者keyPath集合。需要观察的request属性:allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining、networkServiceType、timeoutInterval
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
    static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
    });
    return _AFHTTPRequestSerializerObservedKeyPaths;
}

这个函数创建了一个数组单例,里面装的都是NSURLRequest的属性。
2.2mutableObservedChangedKeyPaths是AFHTTPRequestSerializer类的一个属性,它在-init方法中进行了初始化。另外在-init方法中还对上面设置的6个与NSURLRequest相关的属性添加观察者(KVO):

    self.mutableObservedChangedKeyPaths = [NSMutableSet set];
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
            //为请求的属性添加观察者
            /*
             observer: 观察者对象. 其必须实现方法observeValueForKeyPath:ofObject:change:context:.
             keyPath: 被观察的属性,其不能为nil.
             options: 设定通知观察者时传递的属性值,新值、旧值,通常设置为NSKeyValueObservingOptionNew。
             context: 一些其他的需要传递给观察者的上下文信息,通常设置为nil
             */
            [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
        }
    }

KVO触发的方法,mutableObservedChangedKeyPaths用于记录这些属性的变化(由我们自己设置request的属性值):

//观察者接收通知,通过实现下面的方法,完成对属性改变的响应。将新的属性存储在一个名为 mutableObservedChangedKeyPaths的集合中
//change: 属性值,根据- addObserver: forKeyPath: options: context:的Options设置,给出对应的属性值
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(__unused id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == AFHTTPRequestSerializerObserverContext) {
        if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
            [self.mutableObservedChangedKeyPaths removeObject:keyPath];
        } else {
            [self.mutableObservedChangedKeyPaths addObject:keyPath];
        }
    }
}

这些被监听的属性值改变时是这样通知他们的观察者对象的:

/*
 willChangeValueForKey通知观察到的对象,给定属性的值即将更改。在手动实现KVO时,使用此方法通知观察对象,键值即将更改。
 值更改后,必须使用相同的参数调用相应的didChangeValueForKey:
 */
- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess {
    [self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
    _allowsCellularAccess = allowsCellularAccess;
    [self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
}

2.3最后用KVC给request设置这些属性值。
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];

3.对网络请求参数进行编码

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(request);
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    //设置请求头 不会覆盖原有的header
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];

    NSString *query = nil;//格式化的请求参数
    if (parameters) {
        //如果有自定义block
        if (self.queryStringSerialization) {
            NSError *serializationError;
            //用自定义block来格式化请求参数
            query = self.queryStringSerialization(request, parameters, &serializationError);

            if (serializationError) {
                if (error) {
                    *error = serializationError;
                }

                return nil;
            }
        } else {
            switch (self.queryStringSerializationStyle) {
                case AFHTTPRequestQueryStringDefaultStyle:
                    //调用 AFQueryStringFromParameters 将参数转换为查询参数
                    query = AFQueryStringFromParameters(parameters);
                    break;
            }
        }
    }
    //将参数 parameters 添加到 URL 或者 HTTP body 中
    //GET HEAD DELETE,参数拼接到url
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        if (query && query.length > 0) {
            mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];//根据是否已有查询字符串进行拼接?已有就用‘&’,没有就用‘?’
        }
    }
    //参数添加到httpbody中 ,比如POST PUT
    else {
        if (!query) {
            query = @"";
        }
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
        }
        [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
    }

    return mutableRequest;
}

3.1设置请求头。从self.HTTPRequestHeaders中拿到header,赋值到请求的request中去,如果原先的header已经存在就不进行设置。
3.2对网络请求参数进行编码。
如果有自定的block来格式化(转码)请求参数就用自定义block。

if (self.queryStringSerialization) {
            NSError *serializationError;
            //用自定义block来格式化请求参数
            query = self.queryStringSerialization(request, parameters, &serializationError);

如果没有自定义block来处理就使用AF的转码方式:

//把dictionary参数转换、拼接成字符串参数
/*
 NSDictionary *info = @{@"account":@"zhangsan",@"password":@"123456"};
AFQueryStringFromParameters(info)的结果是:account=zhangsan&password=123456 (没有百分比编码)
 
  NSDictionary *info = @{@"student":@{@"name":@"zhangsan",@"age":@"15"}};
 AFQueryStringFromParameters(info)的结果是:student[name]=zhangsan&student[age]=15 (没有百分比编码)
 */
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }
    //拆分数组返回的参数字符串
    return [mutablePairs componentsJoinedByString:@"&"];
}

//网络请求参数拼接处理入口。
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}

//递归处理value。如果当前的 value 是一个集合类型的话,那么它就会不断地递归调用自己。
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
    //排序。根据需要排序的对象的description来进行升序排列,
    //description返回的是NSString,compare:使用的是NSString的compare:方法
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    if ([value isKindOfClass:[NSDictionary class]]) {
        NSDictionary *dictionary = value;
        // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }
    } else if ([value isKindOfClass:[NSArray class]]) {
        NSArray *array = value;
        for (id nestedValue in array) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
    } else if ([value isKindOfClass:[NSSet class]]) {
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }

    return mutableQueryStringComponents;
}

主要是根据value的类型来用AFQueryStringPairsFromKeyAndValue这个函数递归处理value参数,直到解析的类型不是array\dictionary\set。

  • 这里涉及到一个类AFQueryStringPair
//参数转化的中间模型
 @interface AFQueryStringPair : NSObject
 @property (readwrite, nonatomic, strong) id field;
 @property (readwrite, nonatomic, strong) id value;
 - (instancetype)initWithField:(id)field value:(id)value;
 - (NSString *)URLEncodedStringValue;
 @end

 @implementation AFQueryStringPair
 - (instancetype)initWithField:(id)field value:(id)value {
    self = [super init];
    if (!self) {
        return nil;
    }
    self.field = field;
    self.value = value;
    return self;
}

 //百分号编码后,用"="拼接field value值
 - (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}
@end

AFQueryStringPair这个类相当于是一个参数转化的中间模型,在AFQueryStringPairsFromKeyAndValue函数递归处理的最后:

[mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];

就是这样把一对field-value值保存起来。再通过-URLEncodedStringValue方法对field-value百分比编码、"="拼接。
举个例子理解一下这个参数格式化:

 NSDictionary *info = @{@"account":@"zhangsan",@"password":@"123456"};
AFQueryStringFromParameters(info)的结果是:account=zhangsan&password=123456 (没有百分比编码)
 
  NSDictionary *info = @{@"student":@{@"name":@"zhangsan",@"age":@"15"}};
 AFQueryStringFromParameters(info)的结果是:student[name]=zhangsan&student[age]=15 (没有百分比编码)
  • 关于参数百分比编码:
    根据RFC 3986的规定:URL百分比编码的保留字段分为:
    1.':' '#' '[' ']' '@' '?' '/'
    2.'!' '$' '&' ''' '(' ')' '*' '+' ',' ';' '='
    在对查询字段百分比编码时,'?'和'/'可以不用编码,其他的都要进行编码。下面这段代码结合注释也很好理解,就不过多展开了。
//对字符串进行百分比编码
NSString * AFPercentEscapedStringFromString(NSString *string) {
    //过滤需要编码的字符
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
    //?和/不需要被编码,所以除了?和/之外的字符要从URLQueryAllowedCharacterSet中剔除
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

//    为了处理类似emoji这样的字符串,rangeOfComposedCharacterSequencesForRange 使用了while循环来处理,也就是把字符串按照batchSize分割处理完再拼回。
    static NSUInteger const batchSize = 50;
    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    while (index < string.length) {
        NSUInteger length = MIN(string.length - index, batchSize);
        NSRange range = NSMakeRange(index, length);

        // To avoid breaking up character sequences such as 👴🏻👮🏽
        //对emoji这类特殊字符的处理。分开一个字符串时保证我们不会分开被称为代理对的东西。
        range = [string rangeOfComposedCharacterSequencesForRange:range];

        NSString *substring = [string substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];//编码
        [escaped appendString:encoded];

        index += range.length;
    }
    return escaped;
}

3.3根据请求类型,将参数字符串添加到 URL 或者 HTTP body 中
如果是GET、HEAD、DELETE,则把请求参数拼接到url后面的。而POST、PUT是把请求参数拼接到http body。

2.创建multipart NSMutableURLRequest请求对象

这一部分主要是对上传文件做的一些封装。Multipart是HTTP协议为Web表单新增的上传文件的协议,Content-Type的类型扩充了multipart/form-data用以支持向服务器发送二进制数据。它基于HTTP POST的方法,数据同样是放在body,跟普通POST方法的区别是数据不是key=value形式。更多关于multipart/form-data请求请戳:HTTP协议之multipart/form-data请求分析

请求体HTTP Body的格式大致如下:

--boundary //上边界 //“boundary”是一个边界,没有实际的意义,可以用任意字符串来替代
Content-Disposition: form-data; name=xxx; filename=xxx
Content-Type: application/octet-stream
(空一行)
文件内容的二进制数据
--boundary-- //下边界

请求体内容分为四个部分:
1.上边界
2.头部,告诉服务器要做数据上传,包含:
a. 服务器的接收字段name=xxx。xxx是负责上传文件脚本中的 字段名,开发的时候,可以咨询后端程序员,不需要自己设定。
b. 文件在服务器中保存的名称filename=xxx。xxx可以自己指定,不一定和本地原本的文件名相同
c. 上传文件的数据类型 application/octet-stream
3.上传文件的数据部分(二进制数据)
4.下边界部分,严格按照字符串格式来设置.

上边界部分和下边界部分的字符串,最后都要转换成二进制数据,和文件部分的二进制数据拼接在一起,作为请求体发送给服务器.
NSURLConnection笔记-上传文件

要构造Multipart里的数据有三种方式:

最简单的方式就是直接拼数据,要发送一个文件,就直接把文件所有内容读取出来,再按上述协议加上头部和分隔符,拼接好数据后扔给NSURLRequest的body就可以发送了,很简单。但这样做是不可用的,因为文件可能很大,这样拼数据把整个文件读进内存,很可能把内存撑爆了。

第二种方法是不把文件读出来,不在内存拼,而是新建一个临时文件,在这个文件上拼接数据,再把文件地址扔给NSURLRequest的bodyStream,这样上传的时候是分片读取这个文件,不会撑爆内存,但这样每次上传都需要新建个临时文件,对这个临时文件的管理也挺麻烦的。

第三种方法是构建自己的数据结构,只保存要上传的文件地址,边上传边拼数据,上传是分片的,拼数据也是分片的,拼到文件实体部分时直接从原来的文件分片读取。这方法没上述两种的问题,只是实现起来也没上述两种简单,AFNetworking就是实现这第三种方法,而且还更进一步,除了文件,还可以添加多个其他不同类型的数据,包括NSData,和InputStream。

在Multipart这一部分代码比较长,涉及到几个类和协议,这里先把它们的关系图放出来:


2.1AFHTTPBodyPart

AFHTTPBodyPart实际上做的是对Multipart请求体各部分(初始边界、头部、内容数据实体、结束边界)做拼接和读取的封装。

NSData \ FileUrl \ NSInputStream 类型的数据在AFHTTPBodyPart中都转换成NSInputStream。

//根据body的数据类型,NSData\NSURL\NSInputStream转换成输入流并返回
//inputStream值保存了数据实体,没有分隔符和头部
- (NSInputStream *)inputStream {
    if (!_inputStream) {
        if ([self.body isKindOfClass:[NSData class]]) {
            _inputStream = [NSInputStream inputStreamWithData:self.body];
        } else if ([self.body isKindOfClass:[NSURL class]]) {
            _inputStream = [NSInputStream inputStreamWithURL:self.body];
        } else if ([self.body isKindOfClass:[NSInputStream class]]) {
            _inputStream = self.body;
        } else {
            _inputStream = [NSInputStream inputStreamWithData:[NSData data]];
        }
    }
    return _inputStream;
}

_inputStream只保存了数据实体(body),不包含上下边界和头部信息。

AFHTTPBodyPart读取数据是边读边拼接的,用一个状态机来确定现在数据读到哪一部分,依次往后传递进行状态切换。要注意的是,在读取数据实体(body)部分是用流(NSInputStream)来处理的,读之前打开流,读完之后关闭流然后进入下一阶段:

//用状态机切换
- (BOOL)transitionToNextPhase {
    //主线程执行本方法
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self transitionToNextPhase];
        });
        return YES;
    }

    switch (_phase) {
        //读取完初始边界
        case AFEncapsulationBoundaryPhase:
            _phase = AFHeaderPhase;
            break;
        //读取完头部,准备读取body,打开流 准备接受数据
        case AFHeaderPhase:
            [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
            [self.inputStream open];
            _phase = AFBodyPhase;
            break;
        //读取完body,关闭流
        case AFBodyPhase:
            [self.inputStream close];
            _phase = AFFinalBoundaryPhase;
            break;
        //读取完结束边界
        case AFFinalBoundaryPhase:
        default:
            _phase = AFEncapsulationBoundaryPhase;
            break;
    }
    //重置
    _phaseReadOffset = 0;

    return YES;
}

结合状态机,读取数据是分块进行的,拼接数据也是分块的,边读边拼接。并且使用totalNumberOfBytesRead的局部变量来保存已经读取的字节数,以此来定位要读的数据位置:

//把请求体读到buffer中。边读取边拼接数据
- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
    NSInteger totalNumberOfBytesRead = 0;

    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)];
    }

    if (_phase == AFHeaderPhase) {
        NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
        totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
    }

    if (_phase == AFBodyPhase) {
        NSInteger numberOfBytesRead = 0;

        //读取给定缓冲区中给定的字节数。返回的结果:正数表示读取的字节数。0表示达到缓冲区的结尾。-1表示操作失败;
        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];
            }
        }
    }

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

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

通过阅读上面这两个方法,很容易猜测,- read: maxLength:这个方法会在其他的代码中的某个循环中被调用(主要是数据实体部分的读取拼接是分块进行而不是一次性的)。

2.2AFMultipartBodyStream

AFMultipartBodyStream继承NSInputStream ,遵循NSStreamDelegate协议。

AFMultipartBodyStream封装了整个multipart数据的读取。它有一个NSSArray类型的HTTPBodyParts属性,用来保存每一个AFHTTPBodyPart对象,所以很直观地就想到了是对多文件上传的封装。

对整个multipart数据的读取,主要是根据读取的位置确定当前读的是哪个AFHTTPBodyPart,然后调用AFHTTPBodyPart- read: maxLength:读取、拼接数据,最后记录读取的每一个AFHTTPBodyPart的数据长度总和。AFMultipartBodyStream重写了NSInputStream的- read: maxLength:方法:

//重写方法
- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
    if ([self streamStatus] == NSStreamStatusClosed) {
        return 0;
    }

    NSInteger totalNumberOfBytesRead = 0;
    //self.numberOfBytesInPacket用于3G网络请求优化,指定每次读取的数据包大小,建议值kAFUploadStream3GSuggestedPacketSize
    //遍历读取数据
    while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
        //self.currentHTTPBodyPart不存在,或者没有可读的字节(已经读完)
        if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
            //看看还有没有下一个。把下一个请求体赋值给当前请求体,如果下一个是nil就退出循环
            if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
                break;
            }
        } else {
            //剩余数据长度?
            //这里maxLength是进入AFHTTPBodyPart读取的maxLength
            NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
            //读到buffer中
            NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
            if (numberOfBytesRead == -1) {
                self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
                break;
            } else {
                totalNumberOfBytesRead += numberOfBytesRead;
                //延时用于3G网络请求优化,读取数据延时,建议值kAFUploadStream3GSuggestedDelay
                if (self.delay > 0.0f) {
                    [NSThread sleepForTimeInterval:self.delay];
                }
            }
        }
    }

    return totalNumberOfBytesRead;
}

对初始边界和结束边界进行设置,比如多文件上传时设置第一个文件的初始边界,和最后一个文件的结束边界。

除此之外,它还对多文件上传的初始边界和结束边界进行设置。
对于多文件上传的请求体格式:(以多文件+普通文本为例)

多文件+普通文本 上传的请求体格式如下:

--boundary\r\n           // 第一个文件参数//上边界,不过也可以写成这样:\r\n--boundary\r\n 
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:image/jpeg\r\n\r\n        
(空一行)        
上传文件的二进制数据部分    
\r\n--boundary\r\n    // 第二个文件参数//上边界 //文件一的下边界可略,在这句之前插入文件一的下边界\r\n--boundary--也可以
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:text/plain\r\n\r\n
(空一行)                
上传文件的二进制数据部分  
\r\n--boundary\r\n    //普通文本参数 //上边界
Content-Disposition: form-data; name="xxx"\r\n\r\n    //name是服务器的接收字段,不需要自己制定
(空一行)     
普通文本二进制数据     
\r\n--boundary--       // 下边界

在两个文件之间不需要把上一个文件的结束边界也拼接上去,\r\n--boundary\r\n暂且叫做“中间边界”吧。知道这一协议格式之后,那么下面这段代码也很好理解了:

//初始边界和结束边界的设置。多文件上传时设置第一个文件的上边界,和最后一个文件的下边界
- (void)setInitialAndFinalBoundaries {
    if ([self.HTTPBodyParts count] > 0) {
        for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) {
            bodyPart.hasInitialBoundary = NO;
            bodyPart.hasFinalBoundary = NO;
        }
        [[self.HTTPBodyParts firstObject] setHasInitialBoundary:YES];
        [[self.HTTPBodyParts lastObject] setHasFinalBoundary:YES];
    }
}

由于AFMultipartBodyStream继承NSInputStream ,遵循NSStreamDelegate协议,所以这个类里还重写了很多NSStream的方法:

#pragma mark - NSInputStream
//重写方法
- (BOOL)getBuffer:(__unused uint8_t **)buffer
           length:(__unused NSUInteger *)len
{
    return NO;
}

//判断数据是否已经读完了,open状态就是还有数据
- (BOOL)hasBytesAvailable {
    return [self streamStatus] == NSStreamStatusOpen;
}

#pragma mark - NSStream

- (void)open {
    if (self.streamStatus == NSStreamStatusOpen) {
        return;
    }
    self.streamStatus = NSStreamStatusOpen;
    [self setInitialAndFinalBoundaries];
    self.HTTPBodyPartEnumerator = [self.HTTPBodyParts objectEnumerator];
}

- (void)close {
    self.streamStatus = NSStreamStatusClosed;
}

- (id)propertyForKey:(__unused NSString *)key {
    return nil;
}

- (BOOL)setProperty:(__unused id)property
             forKey:(__unused NSString *)key
{
    return NO;
}

//设置runloop为了让NSStreamDelegate收到stream状态改变回调。不过这里NSURLRequest没有用到delegate处理状态改变就写成空实现了。
- (void)scheduleInRunLoop:(__unused NSRunLoop *)aRunLoop
                  forMode:(__unused NSString *)mode
{}

- (void)removeFromRunLoop:(__unused NSRunLoop *)aRunLoop
                  forMode:(__unused NSString *)mode
{}

2.3AFStreamingMultipartFormData

AFStreamingMultipartFormData遵循AFMultipartFormData协议。是对AFMultipartBodyStream更上一层的封装。

AFStreamingMultipartFormData管理了一个AFMultipartBodyStream类型的属性bodyStream。调用AFStreamingMultipartFormData对象的几种append方法就可以添加 FileURL/NSData/NSInputStream几种不同类型的数据,AFStreamingMultipartFormData内部把这些数据转换成一个个AFHTTPBodyPart,并添加到AFMultipartBodyStream里(用AFMultipartBodyStream的HTTPBodyParts数组把它们一个个保存起来)。最后把AFMultipartBodyStream赋给原来NSMutableURLRequest的bodyStream:

//通过本地文件url获取数据
- (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);

    //url不是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) {
        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;
    }
    //设置 http请求体的header
    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];

    //生成AFHTTPBodyPart对象,拼接到AFMultipartBodyStream对象数组中
    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = mutableHeaders;
    bodyPart.boundary = self.boundary;
    bodyPart.body = fileURL;
    bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];//获取文件大小
    [self.bodyStream appendHTTPBodyPart:bodyPart];

    return YES;
}
//把数据跟请求建立联系的核心方法
//数据最终通过setHTTPBodyStream:传递给request
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {
        return self.request;
    }

    // Reset the initial and final boundaries to ensure correct Content-Length
    [self.bodyStream setInitialAndFinalBoundaries];
    //将输入流作为请求体
    [self.request setHTTPBodyStream:self.bodyStream];
    //设置请求头
    [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;
}

NSURLSession发送请求时会读取这个bodyStream,在读取数据是会调用bodyStream的- read: maxLength:方法,也即AFMultipartBodyStream重写的- read: maxLength:方法,不断读取之前append的AFHTTPBodyPart数据直到读完。

2.4创建multipart NSMutableURLRequest请求对象

//multipart传数据
//GET和HEAD不能用multipart传数据,一般都是用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"]);

    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];

    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];

    if (parameters) {
        //把请求参数也放在multipart里
        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]];
            }
        }
    }

    //执行对外暴露的block接口。
//比如可以在block里拼接其他一些文件数据。调用AFStreamingMultipartFormData的几个append方法
    if (block) {
        block(formData);
    }
    //把stream跟request建立联系的核心方法
    //数据最终通过setHTTPBodyStream:传递给request
    return [formData requestByFinalizingMultipartFormData];
}

2.5其他

AFMultipartBodyStream中有以下这么几个方法看得不太懂,不知道为什么要这样写:

#pragma mark - Undocumented CFReadStream Bridged Methods
- (void)_scheduleInCFRunLoop:(__unused CFRunLoopRef)aRunLoop                     
                     forMode:(__unused CFStringRef)aMode
{}

- (void)_unscheduleFromCFRunLoop:(__unused CFRunLoopRef)aRunLoop
                         forMode:(__unused CFStringRef)aMode
{}

- (BOOL)_setCFClientFlags:(__unused CFOptionFlags)inFlags                
                 callback:(__unused CFReadStreamClientCallBack)inCallback                  
                  context:(__unused CFStreamClientContext *)inContext {    
    return NO;
}

AFNetworking2.0源码解析<二> 中提到:

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。

3.其他

AFJSONRequestSerializer和AFPropertyListRequestSerializer这两个AFHTTPRequestSerializer的子类的实现都比较简单,主要是对这个协议方法进行重写。具体代码阅读都没什么难度,就不展开讲了。

- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(nullable id)parameters
                                        error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;

详细源码注释请戳:https://github.com/huixinHu/AFNetworking-

参考文章:
AFNetworking到底做了什么
AFNetworking2.0源码解析<二>
http://www.cnblogs.com/chenxianming/p/5674652.html
通读AFN②--AFN的上传和下载功能分析、SessionTask及相应的session代理方法的使用细节

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

推荐阅读更多精彩内容