AFNetworking 源碼閱讀之序列化 Serialization

AFNetworking

HTTP 的請求和響應,都需要配置好響應的格式才能正常進行苍姜。比如牢酵,一個請求中,就包含了請求行衙猪、請求頭馍乙、請求體三個部分,同樣一個響應垫释,也包含了響應行丝格、響應頭與響應體,而序列化的目的就是如何定義描述這些內(nèi)容的棵譬。

AFNetworking (以下簡稱 AF )序列化分為兩大類:RequestSerializationResponseSerialization 显蝌。分別代表了請求序列化和響應序列化。

1. RequestSerialization

AFRequestSerialization 所有的內(nèi)容在 AFURLRequestSerialization.hAFURLRequestSerialization.m 文件中订咸。通篇下來曼尊,包含了幾個協(xié)議酬诀、類。他們的名稱和關(guān)系如下圖:

AFURLRequestSerialization.h

HTTP 中的請求中骆撇,對于請求體的內(nèi)容編碼格式有三種:

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

默認情況下,請求的編碼是 application/x-www-urlencoded瞒御,當使用 POST 進行請求時,數(shù)據(jù)會被以 x-www-urlencoded 方式編碼到 Body 中來傳送神郊,而如果 GET 請求肴裙,則是附在 url 鏈接后面來發(fā)送。

multipart/form-data僅僅用在 POST 中涌乳,方便大文件的傳輸蜻懦,在請求頭中需要注明content-typemultipart/form-data。一般的上傳我們使用這個格式編碼居多爷怀。

text-plain 則表示用普通文本的方式進行格式編碼阻肩。

上圖中,通用方式表示一般的請求數(shù)據(jù)結(jié)構(gòu)运授。如果是對 POST 有 multipart/form-data 數(shù)據(jù)格式的需求(比如上傳文件)烤惊,需要用到右邊的一些協(xié)議和類。

1.1 AFURLRequestSerialization

AFURLRequestSerialization 是一個協(xié)議吁朦。它對指定HTTP請求的參數(shù)進行編碼柒室。請求序列化操作可以將參數(shù)編碼為查詢字符串、HTTP主體逗宜,根據(jù)需要設置適當?shù)腍TTP頭部字段雄右。

它僅僅定義了一個協(xié)議方法:

- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(nullable id)parameters
                                        error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
  • 該方法接受一個 NSURLRequest 以及請求的參數(shù) parameters。(parameters 的類型之所以設置為ID纺讲,是因為繼承該協(xié)議之后的實體擂仍,可以自定義參數(shù)類型。)

  • __autoreleasing:將對象賦值給附有 __autoreleasing 修飾符的變量等同于ARC 無效時調(diào)用對象的autorelease方法熬甚。方法中 error 是一個二級指針逢渔,ARC下,并不能監(jiān)聽二級指針的 release乡括,也即是肃廓,如果一個二級指針沒有被正確的釋放處理,他可能會成為一個野指針诲泌!上述的 error 就是此例盲赊,__autoreleasing 的做法是將其放入自動釋放池,盡管 ARC 沒有自動釋放它敷扫,但是在 releasePool 中能保證釋放哀蘑。

  • NS_SWIFT_NOTHROW: Swift 中的錯誤處理和 OC 并不相同,主要體現(xiàn)在,Swift 不會使用類似的 NSError 的二級指針递礼,它采用向上拋出異常的方式惨险。該字段表示,這個方法在 Swift 中不會拋出異常(實質(zhì)是因為脊髓,錯誤會在語言設計方面處理了辫愉,這個里面是不會有異常的)。

1.2 AFHTTPRequestSerializer

AFHTTPRequestSerializer 是 AF 對于 Http 請求的基礎類将硝。他定義了主要的請求序列化的主要內(nèi)容恭朗。一般情況下,我們會使用的二進制進行數(shù)據(jù)的傳輸 NSData依疼,也即是最基本的數(shù)據(jù)格式痰腮。如果我需要編碼為其他的格式,比如 JSON 或者 XML 律罢,可以定義成它的子類膀值,覆寫 AFURLRequestSerialization 協(xié)議的- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error方法,在內(nèi)部對parameters 數(shù)據(jù)格式轉(zhuǎn)換即可實現(xiàn) JSON 或者 XML 的請求序列化误辑。

AFHTTPRequestSerializer 的屬性

在看這個協(xié)議方法實現(xiàn)之前沧踏,先看下 AFHTTPRequestSerializer 的屬性。

AFHTTPRequestSerializer 屬性

特別的巾钉,AF 中對其中的六個屬性進行了 KVO 監(jiān)聽牵啦。目的是為了能夠統(tǒng)一的這個六個屬性進行讀寫監(jiān)聽洛口。監(jiān)聽的過程中,使用了一個屬 AFHTTPRequestSerializerObservedKeyPaths 保存這些屬性,它是一個 NSSet嚼松,如果設置為 nil 的屬性黄伊,將會被移出容器柱蟀。設置好的容器尚粘,會在下一次創(chuàng)建請求的過程中將這些屬性設置到每一個請求中去卿拴。

KVO 監(jiān)聽屬性
AFHTTPRequestSerializer 的方法
  • 初始化
    AF 的初始化方法中,會將所有擁有默認的值的屬性設置成默認值吼旧,這種方式值得我們借鑒凰锡。在頭文件中,已經(jīng)寫明的只有一個類方法:
+ (instancetype)serializer;  

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

當然我們也可以直接使用正常的構(gòu)造方法

[[AFHTTPRequestSerializer alloc] init];

來創(chuàng)建一個對象黍少,因為在其內(nèi)部其實已經(jīng)覆寫了 init 構(gòu)造方法寡夹。

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

    // 設置默認文字編碼格式
    self.stringEncoding = NSUTF8StringEncoding;

    // 初始化 mutableHTTPRequestHeaders , 這個字典將用來保存請求頭的字段处面。
    self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
    
    // 開辟一個隊列厂置,用于請求頭的內(nèi)容的設置和獲取時的線程安全。
    self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);

    // 設置請求頭中的 Accept-Language 字段的默認值魂角。 這里 AF 利用最近用戶使用過的語言的時間作為優(yōu)先級來排序昵济,將最多六個語言設置為  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" 默認的值。這會根據(jù)不同的平臺來設置访忿。 其中瞧栗,在 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
    // 在拼接完字符之后珍语,對字符傳進行過濾和轉(zhuǎn)化锤岸。 CFStringTransform 很有效的轉(zhuǎn)化函數(shù)。
    // http://www.reibang.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"];
    }

    // 設置將請求參數(shù)編碼到連接的請求方式的集合
    // HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
    self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];

    // 對指定的請求信息進行監(jiān)聽板乙。
    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;
}

其中用到兩個比較有效的技巧是偷。一個是根據(jù)用戶手機的 NSLocale (本地設置)來獲取用戶近期使用過的語言,并根據(jù)時間的前后順序設置一個優(yōu)先級(默認最高優(yōu)先級顯示當前使用的語言)募逞。另一個是蛋铆,使用 CFStringTransform 對文字的編碼進行轉(zhuǎn)換,像 AF 中就對獲取的文字進行過濾和轉(zhuǎn)換:

CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)
  • Any-Latin : 將任意字符裝成拉丁
  • Latin-ASCII : 將拉丁文字轉(zhuǎn)換成 ASCII
  • [:^ASCII:] Remove : 刪除 ASCII 碼中的特殊字符

CFStringTransform很強大凡辱,如果要了解詳情可以查看: CFString?Transform戒职,這是 Mattt 大神親自寫的。

  • 其他方法
    暴露在外的方法中透乾,還有一個設置和獲取請求頭的字段的一對方法洪燥。這一對方法實際改動的是在類實現(xiàn)內(nèi)部的一個可變的字典,最終獲取是通過類對外暴露的 HTTPRequestHeaders 來獲取乳乌。
/**
 設置請求頭中的某個已經(jīng)存在的字段的值捧韵,如果設置的值為 nil。 那么將會移除字段
 */
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;

/**
   獲取一個指定的請求頭的字段的值汉操。 如果字段不存在再来,則返回 nil。
 */
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;

/**
  請求頭的 Authorization 字段值設置磷瘤,它的值內(nèi)容為 base + base64b編碼內(nèi)容
 */
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
                                       password:(NSString *)password;

/**
  清空 Authorization 字段的內(nèi)容芒篷。
 */
- (void)clearAuthorizationHeader;

除此之外,AF 還提供了一個自定義編碼請求參數(shù)的功能采缚。它主要體現(xiàn)在以下兩個函數(shù):

/**
   設置請求參數(shù)的編碼類型针炉。目前 AF 只提供了一種類型,寂寞人類型AFHTTPRequestQueryStringDefaultStyle 
 */
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;

/**
   通過設置 block 扳抽,兼容開發(fā)者自己的請求參數(shù)編碼方式篡帕。
 */
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;

這兩個方法的目的是提供對請求的參數(shù)進行編碼的渠道殖侵。在 AF 提供的默認方法中,在 HTTPMethodsEncodingParametersInURI 包含的請求方式中镰烧,我們會將參數(shù)并入到 URL 當中拢军;而在 POST 請求中,則會把參數(shù)放在請求體怔鳖。AF 提供了一個 block 讓我們自己可以選擇使用怎樣的方式進行編碼茉唉。

AFURLRequestSerialization 協(xié)議方法

AFHTTPRequestSerializer 遵循了 AFURLRequestSerialization協(xié)議。下面是實現(xiàn)協(xié)議的代碼结执,有點長赌渣,我在代碼中注釋說明。

#pragma mark - AFURLRequestSerialization
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    // request為空昌犹,直接中斷程序
    NSParameterAssert(request);
    
    /* HTTPRequestHeaders 可以讓調(diào)用者個設置坚芜。(這個調(diào)用者實際上就是 AF 內(nèi)部中的其他類,我們也可以通過設置 HTTPRequestHeaders 的方式來自定義斜姥,如果不自定義鸿竖,那么將會使用默認的請求頭)
       將請求頭 HTTPRequestHeaders 中的參數(shù)逐個轉(zhuǎn)換成字典,并將所有的字典保存在一個數(shù)組中
     */
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];
  
    /*
      檢驗請求原文铸敏。 請求原文是用戶調(diào)用者(這里的調(diào)用者一般主要是我們的使用 AF 的開發(fā)者)的請求附帶參數(shù)缚忧。
     */
    NSString *query = nil;
    if (parameters) {
    
           // queryStringSerialization 是一個block,這個 block 將提供一個給我外部調(diào)用者一個自定義參數(shù)教研杈笔。如果我們設置了這個 block 才會使用闪水。不然會使用 AF 自帶的方式。
        if (self.queryStringSerialization) {
            NSError *serializationError;
            query = self.queryStringSerialization(request, parameters, &serializationError);

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

                return nil;
            }
        } else {
            // 自帶的請求參數(shù)處理蒙具。 這個處理過程包含了對字符的百分號字符轉(zhuǎn)化球榆、無效字符的刪除字符等操作。具體的實現(xiàn)后文描述禁筏。
            switch (self.queryStringSerializationStyle) {
                case AFHTTPRequestQueryStringDefaultStyle:
                    query = AFQueryStringFromParameters(parameters);
                    break;
            }
        }
    }

    // HTTPMethodsEncodingParametersInURI 表示一系列將參數(shù)放在鏈接后尾的請求方式持钉。比如我我們說的 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 請求體的內(nèi)容按照設定的字符編碼格式填充每强。
        
        // #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;
}

可以看到,基本上州刽,在這個序列化方法中空执,已經(jīng)將一個請求的需要序列化的東西全都涵蓋進去了。當然穗椅,主要是針對 x-www-form-urlencoded 方式辨绊。

實際上除了這個方法之外,我們更容易用到的應該是對這個方法的一個封裝的方法:

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

這個方法比之上面的協(xié)議方法房待,增加了對 AFHTTPRequestSerializerObservedKeyPaths 的監(jiān)聽過程邢羔。

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

之前設置的這些屬性,全都會被一一設置到一個具體的請求當中桑孩。

更多的私有方法

AF 的請求序列化中拜鹤,有很多個私有方法,這些方法有的很有技巧流椒,值得我們?nèi)ソ梃b敏簿。這挑選有個又代表性的說下。

  1. 百分號化符號私有化方法
NSString * AFPercentEscapedStringFromString(NSString *string) {
    
    // 先提取了不需要轉(zhuǎn)換的字符
    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];
    
    // 將不需要轉(zhuǎn)換的字符集從 URL 允許的字符集中刪除宣虾。
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

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

    // 定義一個空的字符串惯裕,用于保存最后結(jié)果。 因此绣硝,如果下放所有的轉(zhuǎn)化都失敗了蜻势,或者傳入的參數(shù) 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 中挠铲,可能包含了很多特殊字符,比如::# [ ] @寂诱。這些字符不能被正確的識別拂苹,所以應該使用百分號符號的方式進行轉(zhuǎn)換。

PS:因為不需要對/?進行轉(zhuǎn)換痰洒,因此叫做百分號符號?

  • ????????這種字符串跟普通的字符創(chuàng)并不一樣瓢棒,實際上是由多個字符拼接而成,因此丘喻,轉(zhuǎn)換的時候需要對這個表情做處理音羞,而不是的單個字符。 rangeOfComposedCharacterSequencesForRange 便是由此而用仓犬。
  • stringByAddingPercentEncodingWithAllowedCharacters 用于對字符進行百分號符號化嗅绰。
  1. 請求參數(shù)編碼
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 傳入一個字典參數(shù)的時候搀继,會將參數(shù)編碼成以&為分割的鍵值對窘面,如:key1=value1&key2=value2

1.2 AFMultipartFormData 協(xié)議

這個協(xié)議是為了定義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協(xié)議詳解.pdf"
Content-Type: application/octet-stream

%PDF-1.5
file content
%%EOF

--${bound}
Content-Disposition: form-data; name="Upload"

Submit Query
--${bound}--
--${bound}  為分隔符谍夭,如果一次性上傳不止一個內(nèi)容,則應該使用分隔符來隔開憨募。
--${bound}-- 用于結(jié)尾

AFMultipartFormData 協(xié)議的作用則是用于拼接每一個被隔開的小部分紧索。所以它提供了一系列的 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;

以上幾個函數(shù)都是協(xié)議提供的拼接函數(shù)。實際上菜谣,任何一個有此特征的實例都可以遵循這個協(xié)議珠漂。后面將在具體的實例中,展示如何使用這個協(xié)議的尾膊。

我們通過HTTP請求發(fā)送數(shù)據(jù)的時候媳危,實際上數(shù)據(jù)是以Packet的形式存在于一個Send Buffer中的,應用層平時感知不到這個Buffer的存在冈敛。TCP提供可靠的傳輸待笑,在弱網(wǎng)環(huán)境下,一個Packet一次傳輸失敗的概率會升高抓谴,即使一次失敗滋觉,TCP并不會馬上認為請求失敗了,而是會繼續(xù)重試一段時間齐邦,同時TCP還保證Packet的有序傳輸椎侠,意味著前面的Packet如果不被ack,后面的Packet就會繼續(xù)等待措拇,如果我們一次往Send Buffer中寫入大量的數(shù)據(jù)我纪,那么在弱網(wǎng)環(huán)境下,排在后面的Packet失敗的概率會變高丐吓,也就意味著我們HTTP請求失敗的幾率會變大浅悉。AF 中,對此也保留了相關(guān)的設置接口。通過改變 PacketSize的大小溶解阻塞時間券犁,并設置了延時的時間术健,減低阻塞頻率。 實現(xiàn)在弱網(wǎng)的情況下雖慢卻快的效果粘衬。

- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
                                  delay:(NSTimeInterval)delay;
AFStreamingMultipartFormData 實體

AFStreamingMultipartFormData 是 AF 對于AFMultipartFormData 協(xié)議的實現(xiàn)者荞估。它的作用是解決在 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 數(shù)據(jù)格式褂删。所以重點講一將這個方法飞醉。 multipartFormData 本身也是一種請求,只不過它的格式不一樣屯阀,所以在方法的開始缅帘,就是使用以下代碼初始化一個mutableRequest轴术。

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

// 初始化一個請求對象
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];

這個方法的具體實現(xiàn),我們在上面已經(jīng)介紹過了钦无,就是創(chuàng)建一個普通的請求逗栽。

然后通過下面的代碼引入 AFStreamingMultipartFormData :

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

然后遍歷所有的參數(shù),將參數(shù)抽相為一個個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]];
            }
        }
    }

    // 如果我們需要對組裝好的請求體添加自定義內(nèi)容苍凛,可通過這個 block 傳入趣席。
    if (block) {
        block(formData);
    }

    // 以 --XXX-- 結(jié)尾
    return [formData requestByFinalizingMultipartFormData];

每一個 fromData的部分都使用 AFHTTPBodyPart 進行抽象,因此每個 AFHTTPBodyPart 都包含了分割符合自己保有的請求頭醇蝴,如果只考慮上傳這個功能的話宣肚,AFStreamingMultipartFormData 只需要直接使用 AFHTTPBodyPart 組合的數(shù)據(jù)接口。但是為了能夠切實的知道悠栓,每一次上傳的過程變化霉涨, AF 引入了流。因此并沒有直接 AFHTTPBodyPart 組合的數(shù)據(jù)惭适,而是使用了一個 AFMultipartBodyStream 類來封裝流和 AFHTTPBodyPart 笙瑟,AFStreamingMultipartFormData 則進一步在 AFMultipartBodyStream 之上進行封裝。

  1. AFStreamingMultipartFormData 流的使用:AFMultipartBodyStream

AFMultipartBodyStream 是 NSInputSteam 的一個子類癞志。NSInputSteam 用于上傳時候的輸入往枷,我們將服務器的地址作為目的地,將本地源文件目的地作為起點凄杯,將數(shù)據(jù)傳輸以流的形式進行監(jiān)聽错洁,以獲取實時的上傳進度和上傳狀態(tài)。這就是 AFMultipartBodyStream 要做的工作戒突。

AFMultipartBodyStream 有一套代理屯碴,通過代理就能獲取流的實時狀態(tài),這是我們知道上傳進度的關(guān)鍵原因膊存。

這里發(fā)現(xiàn)导而,AF 有一個比較有技巧性的操作, 為了能夠重寫系統(tǒng)NSStream的某個屬性,可以直接將 NSSteam 的接口重寫一遍隔崎,并在重寫的實現(xiàn)中覆寫這個屬性嗡载,但是缺點是,這個重寫的接口并不能用在文件之外仍稀,只能在局部使用洼滚,盡管如此,我們依然能夠借助這樣的方式技潘,實現(xiàn)屬性的覆蓋或者添加而并不需要建立子類遥巴。具體的代碼如下:

覆寫接口獲取屬性和添加屬性

上圖中千康,NSString通過覆寫接口添加了兩個屬性,而NSStream通過覆寫獲取到屬性铲掐,并能對屬性進行更改(比如修飾等)拾弃。

2. ResponseSerialization

ResponseSerialization 響應序列化和請求序列化實現(xiàn)的邏輯是一樣的。但是相比之下摆霉,響應序列化沒有了類似 Multipart/form-data 格式的區(qū)分豪椿,會顯得更加簡單。我同樣使用一副圖來描述携栋。

AFURLResponseSerialization

同樣的定義一個協(xié)議搭盾,然后構(gòu)建實例來遵循協(xié)議,實際適用則用到具體的響應格式婉支,比如JSON或者XML,則可以通過實現(xiàn)子類的的方式來繼承實例和獲得協(xié)議的定義的接口鸯隅。

2.1 AFURLResponseSerialization 協(xié)議

AFURLResponseSerialization 協(xié)議定義了一個方法,這個方法就是我們使用過程中最基本的方法向挖,子類通過實現(xiàn)這個方法蝌以,對response進行對應的格式化,最終獲取的是我們想要的格式的內(nèi)容何之。

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

2.2 AFHTTPResponseSerializer

AFHTTPResponseSerializer 的屬性不多跟畅,只有三個。

/**
 設定允許的 HTTP 狀態(tài)碼溶推。
 See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
 */
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;

/**
 用于設定能夠接受的 MIME 類型. 帶有 MIME 類型的 `Content-Type` 字段碍彭,不會加入到這其中,因為這在驗證的時候發(fā)生錯誤悼潭。
 */
@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是否有效的函數(shù)舰褪。這是它核心函數(shù)皆疹。我在代碼中的注釋講解。

- (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 狀態(tài)
        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 進行甄別,但是并沒有一個返回最終結(jié)果的函數(shù)敲茄。是因為返回的最終數(shù)據(jù)默認是 NSData位谋,大部分情況下,我們不能直接使用堰燎,因此 AF 定義了幾個子類用于實現(xiàn)各個不同的格式轉(zhuǎn)化掏父。在上圖中,已經(jīng)看到了目前支持 6 種不同的數(shù)據(jù)格式秆剪。下面只針對常用的 JSON 進行分析赊淑。

PS:AFHTTPResponseSerialize 遵循了NScoping 協(xié)議,并對相關(guān)的協(xié)議方法進行了實現(xiàn)仅讽。

2.3 AFJSONResponseSerializer

AFJSONResponseSerializer 繼承自 AFHTTPResponseSerializer 陶缺,除了擁有 AFHTTPResponseSerializer 的一切之外,另外定義了一下幾個方法用戶設置該子類特有的信息何什。

+ (instancetype)serializer {
    // 返回一個 JSONReading 序列化內(nèi)容组哩。
    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 數(shù)據(jù)中的空值。
@property (nonatomic, assign) BOOL removesKeysWithNullValues;

接下來是 AFHTTPResponseSerializer 每個子類的核心方法位仁,也即是AFURLResponseSerialization 唯一的協(xié)議方法的實現(xiàn)柑贞。其中 AFJSONResponseSerializer 中實現(xiàn)如下:


- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    // 在轉(zhuǎn)化數(shù)據(jù)格式之前,先檢測的 response 合法性 和 data 的有效性聂抢。
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
            return nil;
        }
    }

    // data 數(shù)據(jù)為 nil 時钧嘶,直接返回nil。 或者在某些情況(Safari中)琳疏,data 解析為 @“ ”有决。 這時候?qū)嶋H也是數(shù)據(jù)為空。
    // 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 數(shù)據(jù)中為空的字段刪除。
    if (self.removesKeysWithNullValues) {
        // 詳情可以查看   AFJSONObjectByRemovingKeysWithNullValues 揽趾,比較簡單
        return AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }

    return responseObject;
}

AFJSONResponseSerializer的實現(xiàn)是一個代表台汇,更多的其他子類的實現(xiàn),有興趣的同學可以自行查看。

3. 最后

對于 AF 序列化閱讀励七,除了吸取作者的一些設計方案之外智袭,還能督促我們了解更多的有關(guān) Http 的相關(guān)知識。所以掠抬,不管是出于什么目的去閱讀源碼吼野,都對開發(fā)者受益匪淺。最后感謝開源两波!

參考:
iOS-使用CFStringTransform漢字轉(zhuǎn)拼音
iOS122-移動混合開發(fā)研究院
CFString?Transform
iOS中流(NSStream)的使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞳步,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子腰奋,更是在濱河造成了極大的恐慌单起,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劣坊,死亡現(xiàn)場離奇詭異嘀倒,居然都是意外死亡,警方通過查閱死者的電腦和手機局冰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門测蘑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人康二,你說我怎么就攤上這事碳胳。” “怎么了沫勿?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵挨约,是天一觀的道長。 經(jīng)常有香客問我产雹,道長诫惭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任蔓挖,我火速辦了婚禮夕土,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘时甚。我一直安慰自己隘弊,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布荒适。 她就那樣靜靜地躺著梨熙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刀诬。 梳的紋絲不亂的頭發(fā)上咽扇,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天邪财,我揣著相機與錄音,去河邊找鬼质欲。 笑死树埠,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嘶伟。 我是一名探鬼主播怎憋,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼九昧!你這毒婦竟也來了绊袋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铸鹰,失蹤者是張志新(化名)和其女友劉穎癌别,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蹋笼,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡展姐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了剖毯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圾笨。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖速兔,靈堂內(nèi)的尸體忽然破棺而出墅拭,到底是詐尸還是另有隱情活玲,我是刑警寧澤涣狗,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站舒憾,受9級特大地震影響镀钓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜镀迂,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一丁溅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧探遵,春花似錦窟赏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至藏雏,卻和暖如春拷况,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工赚瘦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粟誓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓起意,卻偏偏與公主長得像鹰服,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子揽咕,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容