AFNetworking 源码阅读之序列化 Serialization

AFNetworking

HTTP 的请求和响应,都需要配置好响应的格式才能正常进行。比如,一个请求中,就包含了请求行、请求头、请求体三个部分,同样一个响应,也包含了响应行、响应头与响应体,而序列化的目的就是如何定义描述这些内容的。

AFNetworking (以下简称 AF )序列化分为两大类:RequestSerializationResponseSerialization 。分别代表了请求序列化和响应序列化。

1. RequestSerialization

AFRequestSerialization 所有的内容在 AFURLRequestSerialization.hAFURLRequestSerialization.m 文件中。通篇下来,包含了几个协议、类。他们的名称和关系如下图:

AFURLRequestSerialization.h

HTTP 中的请求中,对于请求体的内容编码格式有三种:

  • application/x-www-urlencoded
  • multipart/form-data
  • text-plain

默认情况下,请求的编码是 application/x-www-urlencoded,当使用 POST 进行请求时,数据会被以 x-www-urlencoded 方式编码到 Body 中来传送,而如果 GET 请求,则是附在 url 链接后面来发送。

multipart/form-data仅仅用在 POST 中,方便大文件的传输,在请求头中需要注明content-typemultipart/form-data。一般的上传我们使用这个格式编码居多。

text-plain 则表示用普通文本的方式进行格式编码。

上图中,通用方式表示一般的请求数据结构。如果是对 POST 有 multipart/form-data 数据格式的需求(比如上传文件),需要用到右边的一些协议和类。

1.1 AFURLRequestSerialization

AFURLRequestSerialization 是一个协议。它对指定HTTP请求的参数进行编码。请求序列化操作可以将参数编码为查询字符串、HTTP主体,根据需要设置适当的HTTP头部字段。

它仅仅定义了一个协议方法:

- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(nullable id)parameters
                                        error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
  • 该方法接受一个 NSURLRequest 以及请求的参数 parameters。(parameters 的类型之所以设置为ID,是因为继承该协议之后的实体,可以自定义参数类型。)

  • __autoreleasing:将对象赋值给附有 __autoreleasing 修饰符的变量等同于ARC 无效时调用对象的autorelease方法。方法中 error 是一个二级指针,ARC下,并不能监听二级指针的 release,也即是,如果一个二级指针没有被正确的释放处理,他可能会成为一个野指针!上述的 error 就是此例,__autoreleasing 的做法是将其放入自动释放池,尽管 ARC 没有自动释放它,但是在 releasePool 中能保证释放。

  • NS_SWIFT_NOTHROW: Swift 中的错误处理和 OC 并不相同,主要体现在,Swift 不会使用类似的 NSError 的二级指针,它采用向上抛出异常的方式。该字段表示,这个方法在 Swift 中不会抛出异常(实质是因为,错误会在语言设计方面处理了,这个里面是不会有异常的)。

1.2 AFHTTPRequestSerializer

AFHTTPRequestSerializer 是 AF 对于 Http 请求的基础类。他定义了主要的请求序列化的主要内容。一般情况下,我们会使用的二进制进行数据的传输 NSData,也即是最基本的数据格式。如果我需要编码为其他的格式,比如 JSON 或者 XML ,可以定义成它的子类,覆写 AFURLRequestSerialization 协议的- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error方法,在内部对parameters 数据格式转换即可实现 JSON 或者 XML 的请求序列化。

AFHTTPRequestSerializer 的属性

在看这个协议方法实现之前,先看下 AFHTTPRequestSerializer 的属性。

AFHTTPRequestSerializer 属性

特别的,AF 中对其中的六个属性进行了 KVO 监听。目的是为了能够统一的这个六个属性进行读写监听。监听的过程中,使用了一个属 AFHTTPRequestSerializerObservedKeyPaths 保存这些属性,它是一个 NSSet,如果设置为 nil 的属性,将会被移出容器。设置好的容器,会在下一次创建请求的过程中将这些属性设置到每一个请求中去。

KVO 监听属性
AFHTTPRequestSerializer 的方法
  • 初始化
    AF 的初始化方法中,会将所有拥有默认的值的属性设置成默认值,这种方式值得我们借鉴。在头文件中,已经写明的只有一个类方法:
+ (instancetype)serializer;  

+ (instancetype)serializer {
    return [[self alloc] init];
}

当然我们也可以直接使用正常的构造方法

[[AFHTTPRequestSerializer alloc] init];

来创建一个对象,因为在其内部其实已经覆写了 init 构造方法。

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }

    // 设置默认文字编码格式
    self.stringEncoding = NSUTF8StringEncoding;

    // 初始化 mutableHTTPRequestHeaders , 这个字典将用来保存请求头的字段。
    self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
    
    // 开辟一个队列,用于请求头的内容的设置和获取时的线程安全。
    self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);

    // 设置请求头中的 Accept-Language 字段的默认值。 这里 AF 利用最近用户使用过的语言的时间作为优先级来排序,将最多六个语言设置为  Accept-Language 对默认值。
    // Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
    NSMutableArray *acceptLanguagesComponents = [NSMutableArray array];
    [[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        float q = 1.0f - (idx * 0.1f);
        [acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]];
        *stop = q <= 0.5f;
    }];
    [self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"];

    // 设置字段 "User-Agent" 默认的值。这会根据不同的平台来设置。 其中,在 iOS 平台上,是一个集 kCFBundleExecutableKey、kCFBundleIdentifierKey、kCFBundleVersionKey、currentDevice、systemVersion、mainScreen.scale 的字符串。
    NSString *userAgent = nil;
#if TARGET_OS_IOS
    // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
    userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif TARGET_OS_WATCH
    // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
    userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
    // 在拼接完字符之后,对字符传进行过滤和转化。 CFStringTransform 很有效的转化函数。
    // https://www.jianshu.com/p/c03402203ae7 参考
    if (userAgent) {
        if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
            NSMutableString *mutableUserAgent = [userAgent mutableCopy];
            if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
                userAgent = mutableUserAgent;
            }
        }
        [self setValue:userAgent forHTTPHeaderField:@"User-Agent"];
    }

    // 设置将请求参数编码到连接的请求方式的集合
    // HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
    self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];

    // 对指定的请求信息进行监听。
    self.mutableObservedChangedKeyPaths = [NSMutableSet set];
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
            [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
        }
    }

    return self;
}

其中用到两个比较有效的技巧。一个是根据用户手机的 NSLocale (本地设置)来获取用户近期使用过的语言,并根据时间的前后顺序设置一个优先级(默认最高优先级显示当前使用的语言)。另一个是,使用 CFStringTransform 对文字的编码进行转换,像 AF 中就对获取的文字进行过滤和转换:

CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)
  • Any-Latin : 将任意字符装成拉丁
  • Latin-ASCII : 将拉丁文字转换成 ASCII
  • [:^ASCII:] Remove : 删除 ASCII 码中的特殊字符

CFStringTransform很强大,如果要了解详情可以查看: CFString​Transform,这是 Mattt 大神亲自写的。

  • 其他方法
    暴露在外的方法中,还有一个设置和获取请求头的字段的一对方法。这一对方法实际改动的是在类实现内部的一个可变的字典,最终获取是通过类对外暴露的 HTTPRequestHeaders 来获取。
/**
 设置请求头中的某个已经存在的字段的值,如果设置的值为 nil。 那么将会移除字段
 */
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;

/**
   获取一个指定的请求头的字段的值。 如果字段不存在,则返回 nil。
 */
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;

/**
  请求头的 Authorization 字段值设置,它的值内容为 base + base64b编码内容
 */
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
                                       password:(NSString *)password;

/**
  清空 Authorization 字段的内容。
 */
- (void)clearAuthorizationHeader;

除此之外,AF 还提供了一个自定义编码请求参数的功能。它主要体现在以下两个函数:

/**
   设置请求参数的编码类型。目前 AF 只提供了一种类型,寂寞人类型AFHTTPRequestQueryStringDefaultStyle 
 */
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;

/**
   通过设置 block ,兼容开发者自己的请求参数编码方式。
 */
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;

这两个方法的目的是提供对请求的参数进行编码的渠道。在 AF 提供的默认方法中,在 HTTPMethodsEncodingParametersInURI 包含的请求方式中,我们会将参数并入到 URL 当中;而在 POST 请求中,则会把参数放在请求体。AF 提供了一个 block 让我们自己可以选择使用怎样的方式进行编码。

AFURLRequestSerialization 协议方法

AFHTTPRequestSerializer 遵循了 AFURLRequestSerialization协议。下面是实现协议的代码,有点长,我在代码中注释说明。

#pragma mark - AFURLRequestSerialization
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    // request为空,直接中断程序
    NSParameterAssert(request);
    
    /* HTTPRequestHeaders 可以让调用者个设置。(这个调用者实际上就是 AF 内部中的其他类,我们也可以通过设置 HTTPRequestHeaders 的方式来自定义,如果不自定义,那么将会使用默认的请求头)
       将请求头 HTTPRequestHeaders 中的参数逐个转换成字典,并将所有的字典保存在一个数组中
     */
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];
  
    /*
      检验请求原文。 请求原文是用户调用者(这里的调用者一般主要是我们的使用 AF 的开发者)的请求附带参数。
     */
    NSString *query = nil;
    if (parameters) {
    
           // queryStringSerialization 是一个block,这个 block 将提供一个给我外部调用者一个自定义参数教研。如果我们设置了这个 block 才会使用。不然会使用 AF 自带的方式。
        if (self.queryStringSerialization) {
            NSError *serializationError;
            query = self.queryStringSerialization(request, parameters, &serializationError);

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

                return nil;
            }
        } else {
            // 自带的请求参数处理。 这个处理过程包含了对字符的百分号字符转化、无效字符的删除字符等操作。具体的实现后文描述。
            switch (self.queryStringSerializationStyle) {
                case AFHTTPRequestQueryStringDefaultStyle:
                    query = AFQueryStringFromParameters(parameters);
                    break;
            }
        }
    }

    // HTTPMethodsEncodingParametersInURI 表示一系列将参数放在链接后尾的请求方式。比如我我们说的 GET。
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        if (query && query.length > 0) {
            mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
        }
    } else {
        // x-www-form-urlencoded 是默认的 URL 编码,直接将 HTTP 请求体的内容按照设定的字符编码格式填充。
        
        // #2864: an empty string is a valid x-www-form-urlencoded payload
        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;
}

可以看到,基本上,在这个序列化方法中,已经将一个请求的需要序列化的东西全都涵盖进去了。当然,主要是针对 x-www-form-urlencoded 方式。

实际上除了这个方法之外,我们更容易用到的应该是对这个方法的一个封装的方法:

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error 

这个方法比之上面的协议方法,增加了对 AFHTTPRequestSerializerObservedKeyPaths 的监听过程。

    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }

之前设置的这些属性,全都会被一一设置到一个具体的请求当中。

更多的私有方法

AF 的请求序列化中,有很多个私有方法,这些方法有的很有技巧,值得我们去借鉴。这挑选有个又代表性的说下。

  1. 百分号化符号私有化方法
NSString * AFPercentEscapedStringFromString(NSString *string) {
    
    // 先提取了不需要转换的字符
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
    
    
    // 获取 URL 允许请求的字符集
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    
    // 将不需要转换的字符集从 URL 允许的字符集中删除。
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

    // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
    // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];

    // 定义一个空的字符串,用于保存最后结果。 因此,如果下放所有的转化都失败了,或者传入的参数 string 本身为空,那么将返回一个空字符串。
    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 👴🏻👮🏽
        range = [string rangeOfComposedCharacterSequencesForRange:range];

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

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

这是一个将字符串百分号符号化的方法。 所谓的百分号符号化,是因为,在 URL 中,可能包含了很多特殊字符,比如::# [ ] @。这些字符不能被正确的识别,所以应该使用百分号符号的方式进行转换。

PS:因为不需要对/?进行转换,因此叫做百分号符号?

  • 👴🏻👮🏽这种字符串跟普通的字符创并不一样,实际上是由多个字符拼接而成,因此,转换的时候需要对这个表情做处理,而不是的单个字符。 rangeOfComposedCharacterSequencesForRange 便是由此而用。
  • stringByAddingPercentEncodingWithAllowedCharacters 用于对字符进行百分号符号化。
  1. 请求参数编码
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);
}

NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

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

这个方法其实很简单,当我们对这个 AFQueryStringFromParameters 传入一个字典参数的时候,会将参数编码成以&为分割的键值对,如:key1=value1&key2=value2

1.2 AFMultipartFormData 协议

这个协议是为了定义POST 请求使用 Multipart/form-data 格式上传文件时提供了API。

Multipart/form-data 格式的特点是,请求头中,必须包含 Content-Type,且该请求头字段对应的值是Multipart/form-data,如:'Content-Type': '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}--
--${bound}  为分隔符,如果一次性上传不止一个内容,则应该使用分隔符来隔开。
--${bound}-- 用于结尾

AFMultipartFormData 协议的作用则是用于拼接每一个被隔开的小部分。所以它提供了一系列的 API :

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

// 
- (void)appendPartWithHeaders:(nullable NSDictionary <NSString *, NSString *> *)headers
                         body:(NSData *)body;

以上几个函数都是协议提供的拼接函数。实际上,任何一个有此特征的实例都可以遵循这个协议。后面将在具体的实例中,展示如何使用这个协议的。

我们通过HTTP请求发送数据的时候,实际上数据是以Packet的形式存在于一个Send Buffer中的,应用层平时感知不到这个Buffer的存在。TCP提供可靠的传输,在弱网环境下,一个Packet一次传输失败的概率会升高,即使一次失败,TCP并不会马上认为请求失败了,而是会继续重试一段时间,同时TCP还保证Packet的有序传输,意味着前面的Packet如果不被ack,后面的Packet就会继续等待,如果我们一次往Send Buffer中写入大量的数据,那么在弱网环境下,排在后面的Packet失败的概率会变高,也就意味着我们HTTP请求失败的几率会变大。AF 中,对此也保留了相关的设置接口。通过改变 PacketSize的大小溶解阻塞时间,并设置了延时的时间,减低阻塞频率。 实现在弱网的情况下虽慢却快的效果。

- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
                                  delay:(NSTimeInterval)delay;
AFStreamingMultipartFormData 实体

AFStreamingMultipartFormData 是 AF 对于AFMultipartFormData 协议的实现者。它的作用是解决在 AF 中对于Multipart/form-data格式的序列化,同时兼具流的控制。

AFStreamingMultipartFormData
AFStreamingMultipartFormData

AFStreamingMultipartFormData

  1. AFStreamingMultipartFormData 对Multipart/form-data格式的序列化

AFStreamingMultipartFormData 对Multipart/form-data格式的序列化只用在AFHTTPRequestSerializer 类的一个特殊方法中:

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error;

这个方法从字面的意思即可知道,适用于 multipartFormData 数据格式。所以重点讲一将这个方法。 multipartFormData 本身也是一种请求,只不过它的格式不一样,所以在方法的开始,就是使用以下代码初始化一个mutableRequest

// 先排除 GET和HEAD请求方式
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);

// 初始化一个请求对象
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];

这个方法的具体实现,我们在上面已经介绍过了,就是创建一个普通的请求。

然后通过下面的代码引入 AFStreamingMultipartFormData :

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

然后遍历所有的参数,将参数抽相为一个个AFQueryStringPair对象,用于拼接使用。

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

    // 如果我们需要对组装好的请求体添加自定义内容,可通过这个 block 传入。
    if (block) {
        block(formData);
    }

    // 以 --XXX-- 结尾
    return [formData requestByFinalizingMultipartFormData];

每一个 fromData的部分都使用 AFHTTPBodyPart 进行抽象,因此每个 AFHTTPBodyPart 都包含了分割符合自己保有的请求头,如果只考虑上传这个功能的话,AFStreamingMultipartFormData 只需要直接使用 AFHTTPBodyPart 组合的数据接口。但是为了能够切实的知道,每一次上传的过程变化, AF 引入了流。因此并没有直接 AFHTTPBodyPart 组合的数据,而是使用了一个 AFMultipartBodyStream 类来封装流和 AFHTTPBodyPart ,AFStreamingMultipartFormData 则进一步在 AFMultipartBodyStream 之上进行封装。

  1. AFStreamingMultipartFormData 流的使用:AFMultipartBodyStream

AFMultipartBodyStream 是 NSInputSteam 的一个子类。NSInputSteam 用于上传时候的输入,我们将服务器的地址作为目的地,将本地源文件目的地作为起点,将数据传输以流的形式进行监听,以获取实时的上传进度和上传状态。这就是 AFMultipartBodyStream 要做的工作。

AFMultipartBodyStream 有一套代理,通过代理就能获取流的实时状态,这是我们知道上传进度的关键原因。

这里发现,AF 有一个比较有技巧性的操作, 为了能够重写系统NSStream的某个属性,可以直接将 NSSteam 的接口重写一遍,并在重写的实现中覆写这个属性,但是缺点是,这个重写的接口并不能用在文件之外,只能在局部使用,尽管如此,我们依然能够借助这样的方式,实现属性的覆盖或者添加而并不需要建立子类。具体的代码如下:

覆写接口获取属性和添加属性

上图中,NSString通过覆写接口添加了两个属性,而NSStream通过覆写获取到属性,并能对属性进行更改(比如修饰等)。

2. ResponseSerialization

ResponseSerialization 响应序列化和请求序列化实现的逻辑是一样的。但是相比之下,响应序列化没有了类似 Multipart/form-data 格式的区分,会显得更加简单。我同样使用一副图来描述。

AFURLResponseSerialization

同样的定义一个协议,然后构建实例来遵循协议,实际适用则用到具体的响应格式,比如JSON或者XML,则可以通过实现子类的的方式来继承实例和获得协议的定义的接口。

2.1 AFURLResponseSerialization 协议

AFURLResponseSerialization 协议定义了一个方法,这个方法就是我们使用过程中最基本的方法,子类通过实现这个方法,对response进行对应的格式化,最终获取的是我们想要的格式的内容。

- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
                           data:(nullable NSData *)data
                          error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;

2.2 AFHTTPResponseSerializer

AFHTTPResponseSerializer 的属性不多,只有三个。

/**
 设定允许的 HTTP 状态码。
 See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
 */
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;

/**
 用于设定能够接受的 MIME 类型. 带有 MIME 类型的 `Content-Type` 字段,不会加入到这其中,因为这在验证的时候发生错误。
 */
@property (nonatomic, copy, nullable) NSSet <NSString *> *acceptableContentTypes;

/**
  编码格式,但是整个 AFHTTPResponseSerializer 不会使用到这个属性。
 */
@property (nonatomic, assign) NSStringEncoding stringEncoding DEPRECATED_MSG_ATTRIBUTE("The string encoding is never used. AFHTTPResponseSerializer only validates status codes and content types but does not try to decode the received data in any way.");

AFHTTPResponseSerializer 提供了一个检测response是否有效的函数。这是它核心函数。我在代码中的注释讲解。

- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error
{
    // 定义量个变量, 用于下面的合法性判断保存值。
    BOOL responseIsValid = YES;
    NSError *validationError = nil;

    // 返回类型必须是 NSHTTPURLResponse
    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
        
        // 必须带有认可的 MIME 类型。
        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
            !([response MIMEType] == nil && [data length] == 0)) {

            if ([data length] > 0 && [response URL]) {
                
                // 如果是 content-type 带有 MIME。 报错。
                NSMutableDictionary *mutableUserInfo = [@{
                                                          NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                                                          NSURLErrorFailingURLErrorKey:[response URL],
                                                          AFNetworkingOperationFailingURLResponseErrorKey: response,
                                                        } mutableCopy];
                if (data) {
                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
                }
   
                validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
            }

            responseIsValid = NO;
        }

        // 必须符合认可的 http 状态
        if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
            NSMutableDictionary *mutableUserInfo = [@{
                                               NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                                               NSURLErrorFailingURLErrorKey:[response URL],
                                               AFNetworkingOperationFailingURLResponseErrorKey: response,
                                       } mutableCopy];

            if (data) {
                mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
            }

            validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);

            responseIsValid = NO;
        }
    }

    if (error && !responseIsValid) {
        *error = validationError;
    }

    return responseIsValid;
}

这个认证方法,就是判断返回的 response ,是不是有效的 response 。并且对错误的原因进行了指定。

AFHTTPResponseSerializer 仅仅对 response 进行甄别,但是并没有一个返回最终结果的函数。是因为返回的最终数据默认是 NSData,大部分情况下,我们不能直接使用,因此 AF 定义了几个子类用于实现各个不同的格式转化。在上图中,已经看到了目前支持 6 种不同的数据格式。下面只针对常用的 JSON 进行分析。

PS:AFHTTPResponseSerialize 遵循了NScoping 协议,并对相关的协议方法进行了实现。

2.3 AFJSONResponseSerializer

AFJSONResponseSerializer 继承自 AFHTTPResponseSerializer ,除了拥有 AFHTTPResponseSerializer 的一切之外,另外定义了一下几个方法用户设置该子类特有的信息。

+ (instancetype)serializer {
    // 返回一个 JSONReading 序列化内容。
    return [self serializerWithReadingOptions:(NSJSONReadingOptions)0];
}

+ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions {
    AFJSONResponseSerializer *serializer = [[self alloc] init];
    serializer.readingOptions = readingOptions;

    return serializer;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }

    // 设定 JOSN 解析的默认支持的 MIME , 如果我们需要支持更多的类型,可以对这个方法进行自定义。或者直接修改 acceptableContentTypes。
    self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];

    return self;
}

另外,AFJSONResponseSerializer还有一个属性 removesKeysWithNullValues

// 默认为NO, 如果设置为YES, 将会删除 JSON 数据中的空值。
@property (nonatomic, assign) BOOL removesKeysWithNullValues;

接下来是 AFHTTPResponseSerializer 每个子类的核心方法,也即是AFURLResponseSerialization 唯一的协议方法的实现。其中 AFJSONResponseSerializer 中实现如下:


- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    // 在转化数据格式之前,先检测的 response 合法性 和 data 的有效性。
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
            return nil;
        }
    }

    // data 数据为 nil 时,直接返回nil。 或者在某些情况(Safari中),data 解析为 @“ ”。 这时候实际也是数据为空。
    // Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
    // See https://github.com/rails/rails/issues/1742
    BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
    
    if (data.length == 0 || isSpace) {
        return nil;
    }
    
    // 保存错误的变量
    NSError *serializationError = nil;
    
    // 使用 NSJSONSerialization 进行序列化。
    id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];

    if (!responseObject)
    {
        if (error) {
            *error = AFErrorWithUnderlyingError(serializationError, *error);
        }
        return nil;
    }
    
    // 如果设置 removesKeysWithNullValues 为YES,将会将 JSON 数据中为空的字段删除。
    if (self.removesKeysWithNullValues) {
        // 详情可以查看   AFJSONObjectByRemovingKeysWithNullValues ,比较简单
        return AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }

    return responseObject;
}

AFJSONResponseSerializer的实现是一个代表,更多的其他子类的实现,有兴趣的同学可以自行查看。

3. 最后

对于 AF 序列化阅读,除了吸取作者的一些设计方案之外,还能督促我们了解更多的有关 Http 的相关知识。所以,不管是出于什么目的去阅读源码,都对开发者受益匪浅。最后感谢开源!

参考:
iOS-使用CFStringTransform汉字转拼音
iOS122-移动混合开发研究院
CFString​Transform
iOS中流(NSStream)的使用

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

推荐阅读更多精彩内容