前面兩篇文章已經初步介紹了AFNetworking 2.x 的基本情況以及核心類AFXXXXRequestOperation的內容。主要是關于request是如何執行的,responseData如何獲取的,然后關于requestSerialization&responseSerialization的內容直接跳過去的。對于requestSerialization的內容主要包括兩大塊:
① request的get&post參數格式化,http headers設置,請求相關其他屬性的設置
② multipart request相關內容
具體涉及到的類的結構如下
分兩篇文章分析這兩部分內容,本篇主要是第一部分——參數格式化,headers,timeout等
1 requestSerialization完成的工作
對于一個http request,一般需要知道request的method是POST/GET,HTTP headers是什么內容,請求的url中或者HTTP body中的參數是什么,user-agent內容是什么以及request其他屬性例如timeout,cache等等如何設置。總之,一切與request相關的設置都是在這個類中完成。在上述工作中,最難的地方就是傳入的parameter參數如何轉化成request可以使用的參數結構。另一個難點是構建multipart 的http body。
對于格式化請求參數,在request過程中一般會增加參數例如username=brownfeng&company=webank
,POST方法放在http body,GET方法放在URL的?
后面。AFNetworking提供了方法,讓我們將dict,array,set表示的參數轉化成key=value
形式,框架中用AFQueryStringPair表示,然后拼接成key1=value1&key2=value2
根據http request method的不同,放到url中(進行過url encoded)或者http body中。
大致的流程如下:參考https://github.com/AFNetworking/AFNetworking/tree/2.x
對于GET請求
NSString *URLString = @"http://example.com";
NSDictionary *parameters = @{@"foo": @"bar", @"baz": @[@1, @2, @3]};
[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:URLString parameters:parameters error:nil];
--------->
GET http://example.com?foo=bar&baz[]=1&baz[]=2&baz[]=3
對于POST請求:
[[AFHTTPRequestSerializer serializer] requestWithMethod:@"POST" URLString:URLString parameters:parameters];
--------->
POST http://example.com/
Content-Type: application/x-www-form-urlencoded
foo=bar&baz[]=1&baz[]=2&baz[]=3
另一個更加細致的parameter -> query string, 參考http://blog.cnbang.net/tech/2371/
@{
@"name" : @"bang",
@"phone": @{@"mobile": @"xx", @"home": @"xx"},
@"families": @[@"father", @"mother"],
@"nums": [NSSet setWithObjects:@"1", @"2", nil]
}
->
@[
field: @"name", value: @"bang",
field: @"phone[mobile]", value: @"xx",
field: @"phone[home]", value: @"xx",
field: @"families[]", value: @"father",
field: @"families[]", value: @"mother",
field: @"nums", value: @"1",
field: @"nums", value: @"2",
]
->
name=bang&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2
AFNetworking不僅僅支持默認的這種格式化,也支持另外兩種格式:AFJSONRequestSerializer&AFPropertyListRequestSerializer。這兩種格式的轉化方法比較簡單,直接調用的系統api,本文就不深入討論。
另外,如果這三種格式都不滿足需求,還可以模仿上述格式化方法自定義的格式——只需要繼承AFHTTPRequestSerializer,并實現AFURLRequestSerialization protocol,然后在AFHTTPRequestManager中設置requestSerializer
屬性為自定義的對象即可。
2 requestSerialization創建
如果閱讀過第一篇文章,就會有知道, AFHTTPRequestOperationManager有一個AFHTTPRequestSerializer屬性,并在designed init 方法中初始化,當我們調用manager方法時,就會觸發[AFHTTPRequestSerializer serializer]
(這里也可以設置成AFJSONRequestSerializer & AFPropertyListRequestSerializer)。這是一個類方法,AFHTTPRequestSerializer中的designed init方法如下,主要設置NSURLRequest的http headers:Accept-Language, User-Agent等等
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.stringEncoding = NSUTF8StringEncoding;//request body 的encoding,如果有的話
self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];//初始化請求的headers
// 設置HTTP請求request的Accept-Language屬性
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"];
//設置 UA
NSString *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]];
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 需要直接在uri 中 encode parameter
self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
//對allowsCellularAccess,cachePolicy,HTTPShouldHandleCookies,HTTPShouldUsePipelining,networkServiceType,timeoutInterval等屬性進行觀察
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;
}
3 默認requestSerializer請求序列化的過程
如果閱讀過第一篇文章,就會有知道,requestSerializaiton發生在 AFHTTPRequestOperationManager的
HTTPRequestOperationWithHTTPMethod:URLString:parameters:success:failure:方法中,會返回一個NSMutableURLRequest。
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
而上述方法的實現如下:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
//使用宏定義判斷是否為nil
NSParameterAssert(method);
NSParameterAssert(URLString);
//創建url
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
//創建mutableURLRequest,并設置request method
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
//十分巧妙的通過serializer的屬性設置request的部分屬性(值得學習的方法)
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
//序列化的核心message
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
方法(NSURLRequest *)requestBySerializingRequest:withParameters:error:
是所有序列化方法里面最重要的方法——根據請求是POST or GET設置請求參數到url or body,然后Post情況下設置content-type。重點是如何從paramter中取出參數然后拼成request需要的形式。
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
//通過serializer 的headers屬性 設置 request 的headers
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
NSString *query = nil;
if (parameters) {
if (self.queryStringSerialization) {//自定義的序列化的block,如果不為空
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
//調用系統的parameter -> queryString 方法,生成樣式 xxx=xx&xxx=xx,對于轉化成json&property模式替換此方法
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
//如果HTTP method 是 GET
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {//如果HTTP method 是 POST
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
//設置Content-Type
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
//將query 放到HTTPBody中
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
通過源碼可以發現如果設置了queryStringSerialization Block,AFNetworking就會調用這個block來序列化query參數,也可以將這部分參數添加到系統中,定義成一個style來選擇。
關鍵代碼是(id)paramters -> (NSString *)query 有關參數的組裝拼接主要包括以下方法&屬性
@property (readwrite, nonatomic, copy) AFQueryStringSerializationBlock queryStringSerialization
static NSString * AFQueryStringFromParameters(NSDictionary *parameters)
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary)
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value)
具體的實現如下:
//網絡框架的 parameters->query 入口方法,系統只支持paramters是NSDictionary*的參數列表,如果需要解析其他parameters的形式,比如model||array等等,需要自己實現queryStringSerialization block。也可以在AFHTTPRequestManager進行封裝,增加其他的添加參數的形式,本系列最后會給一個傳入model當做參數的實例。
static 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是否是dict,array,set,非集合類型,如果是value是集合類型,dict,set無序集合需要先排序再處理,繼續遞歸,如果是非集合類型則會返回,最后得到一個pair類型的array,返回上一層進行foreach操作,輸出pair 的URLEncodedStringValue(url編碼)值的數組,然后組裝成'&'間隔的字符串paramters。
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 {//如果非集合類,直接當做key-value pair
[mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
}
return mutableQueryStringComponents;
}
總結經驗:
① 使用#define NSParameterAssert(condition) NSAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)
宏定義來判斷參數是否為nil
② 將parameter轉化成key-value pair的方法值得借鑒,在格式化其他內容時候使用遞歸調用的方式處理。最后一篇文章會給出一個實例。
③ 使用KVO的方式設置request的其他屬性(如果沒有手動設置requestSerializer的這幾個屬性,以下幾個setter方法不會出發,生成的request相關屬性都是默認的),使用流程如下:
- 在init方法中調用方法如下方法,對需要設置的屬性進行KVO觀察
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
}
}
- 在每個觀察屬性的getter方法中willChange 和 didChange方法 send message
- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess {
[self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
_allowsCellularAccess = allowsCellularAccess;
[self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
}
- (void)setCachePolicy:(NSURLRequestCachePolicy)cachePolicy {
[self willChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
_cachePolicy = cachePolicy;
[self didChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
}
- (void)setHTTPShouldHandleCookies:(BOOL)HTTPShouldHandleCookies {
[self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
_HTTPShouldHandleCookies = HTTPShouldHandleCookies;
[self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
}
- (void)setHTTPShouldUsePipelining:(BOOL)HTTPShouldUsePipelining {
[self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))];
_HTTPShouldUsePipelining = HTTPShouldUsePipelining;
[self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))];
}
- (void)setNetworkServiceType:(NSURLRequestNetworkServiceType)networkServiceType {
[self willChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))];
_networkServiceType = networkServiceType;
[self didChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))];
}
- (void)setTimeoutInterval:(NSTimeInterval)timeoutInterval {
[self willChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))];
_timeoutInterval = timeoutInterval;
[self didChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))];
}
- 最后在屬性發生變化時,將key加入到mutableObservedChangedKeyPaths中,下次調用requestXXXXX方法生成request對象時,就可以講設置的相關屬性設置到request對象中。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([AFHTTPRequestSerializerObservedKeyPaths() containsObject:key]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (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];
}
}
}
- 在
- (NSMutableURLRequest *)requestWithMethod:URLString:parameters:error:
方法中調用如下語句,返回mutableRequest時候就會去設置前面加入到mutableObservedChangedKeyPaths的屬性
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}