前言
序列化,百度百科的解釋摘抄如下:
序列化 (Serialization)將對象的狀態(tài)信息轉(zhuǎn)換為可以存儲或傳輸?shù)男问降倪^程。在序列化期間,對象將其當(dāng)前狀態(tài)寫入到臨時或持久性存儲區(qū)。以后,可以通過從存儲區(qū)中讀取或反序列化對象的狀態(tài),重新創(chuàng)建該對象。
1、以某種存儲形式使自定義對象持久化;
2、將對象從一個地方傳遞到另一個地方。
3、使程序更具維護(hù)性。
簡單點說就是將對象轉(zhuǎn)換為二進(jìn)制流,以便存儲或傳輸,且日后也能從二進(jìn)制流轉(zhuǎn)換回對象。 所以我們今天要說的請求序列化,說到底就是造一個可以在網(wǎng)絡(luò)上傳遞(被序列化)的、合法可用的請求。這其中就包括了參數(shù)的處理和請求頭的設(shè)置等。不管怎樣的代碼,萬變不離其宗,它的核心就是這個知識點。所以理解了這個,再去看代碼就容易多了。
我們知道,GET請求和POST請求對于參數(shù)是不同的處理。GET請求是多個參數(shù)之間以&相連,且單個參數(shù)的鍵值間以=連接,并將參數(shù)以?開頭,經(jīng)過編碼再追加在url后面,而POST卻是將其放入請求體HTTPBody的。
另外一個HTTP連接的請求頭HTTPHeaderField設(shè)置也是非常重要的,它提供了很多字段,用于不同場景,不同特征下的使用。
生成請求request的地方是在AFHTTPSessionManager中,當(dāng)時我們說了該類有名為序列化器的屬性requestSerializer,然后調(diào)用該序列化器的方法生成了一個request。當(dāng)時,因為這是AFHTTPRequestSerializer類中的方法,所以對于具體實現(xiàn)沒有研究。今天我們的工作就是這個。
源碼
先來看頭文件AFHTTPRequestSerializer.h。它繼承自NSObject,實現(xiàn)了AFURLRequestSerialization協(xié)議。
首先看頭文件中定義的屬性:
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, assign) BOOL allowsCellularAccess; // 是否允許使用蜂窩網(wǎng)絡(luò),默認(rèn)YES
@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy; // 緩存策略
@property (nonatomic, assign) BOOL HTTPShouldHandleCookies; // 是否處理Cookie
@property (nonatomic, assign) BOOL HTTPShouldUsePipelining; // 是否開啟管線化
@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
@property (readonly, nonatomic, strong) NSDictionary <NSString *, NSString *> *HTTPRequestHeaders;
@property (nonatomic, strong) NSSet <NSString *> *HTTPMethodsEncodingParametersInURI;
stringEncoding意為序列化參數(shù)時的字符串編碼,默認(rèn)是NSUTF8StringEncoding;
HTTPRequestHeaders是個字典,意為即將被序列化的HTTP的請求頭。默認(rèn)包括Accept-Language和User-Agent等字段;
HTTPMethodsEncodingParametersInURI是個集合對象,它表示序列化時參數(shù)被追加在url里的請求方式的集合,想想也知道,它的元素應(yīng)該是GET、Head。
其他幾個屬性都比較好理解,而且注釋寫得很清楚了,不解釋了。
接著看在頭文件中暴露的前幾個方法:
+ (instancetype)serializer;
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
- (void)clearAuthorizationHeader;
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
首先是初始化方法,return一個該類的實例對象;然后是兩個對請求頭字典操作的兩個方法;再接著是兩個關(guān)于登錄認(rèn)證的方法;然后是一個設(shè)置序列化類型的方法,代表遵循什么樣的規(guī)則進(jìn)行queryString轉(zhuǎn)換。參數(shù)是個枚舉,但是這個枚舉只有一個值AFHTTPRequestQueryStringDefaultStyle。最后一個方法提供了以block形式自定義queryString轉(zhuǎn)換的接口,也就是說可以通過block回調(diào)的方式讓調(diào)用者以自己的方式完成queryString的轉(zhuǎn)換。
最后就剩下三個核心方法了。其中第一個方法便是我們在前面已經(jīng)接觸過的,由HTTP method、URLString、parameters返回一個請求request。
下面代碼注釋非常占篇幅,但是注釋的很好,舍不得刪。
Creates an
NSMutableURLRequestobject with the specified HTTP method and URL string.
If the HTTP method is
GET,HEAD, orDELETE, the parameters will be used to construct a url-encoded query string that is appended to the request's URL. Otherwise, the parameters will be encoded according to the value of theparameterEncodingproperty, and set as the request body.
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URL string.
If the HTTP method is `GET`, `HEAD`, or `DELETE`, the parameters will be used to construct a url-encoded query string that is appended to the request's URL. Otherwise, the parameters will be encoded according to the value of the `parameterEncoding` property, and set as the request body.
@param method The HTTP method for the request, such as `GET`, `POST`, `PUT`, or `DELETE`. This parameter must not be `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be either set as a query string for `GET` requests, or the request HTTP body.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object.
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error;
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URLString, and constructs a `multipart/form-data` HTTP body, using the specified parameters and multipart form data block. See http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2
Multipart form requests are automatically streamed, reading files directly from disk along with in-memory data in a single HTTP body. The resulting `NSMutableURLRequest` object has an `HTTPBodyStream` property, so refrain from setting `HTTPBodyStream` or `HTTPBody` on this request object, as it will clear out the multipart form body stream.
@param method The HTTP method for the request. This parameter must not be `GET` or `HEAD`, or `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded and set in the request HTTP body.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object
*/
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable NSDictionary <NSString *, id> *)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error;
/**
Creates an `NSMutableURLRequest` by removing the `HTTPBodyStream` from a request, and asynchronously writing its contents into the specified file, invoking the completion handler when finished.
@param request The multipart form request. The `HTTPBodyStream` property of `request` must not be `nil`.
@param fileURL The file URL to write multipart form contents to.
@param handler A handler block to execute.
@discussion There is a bug in `NSURLSessionTask` that causes requests to not send a `Content-Length` header when streaming contents from an HTTP body, which is notably problematic when interacting with the Amazon S3 webservice. As a workaround, this method takes a request constructed with `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:`, or any other request with an `HTTPBodyStream`, writes the contents to the specified file and returns a copy of the original request with the `HTTPBodyStream` property set to `nil`. From here, the file can either be passed to `AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler:`, or have its contents read into an `NSData` that's assigned to the `HTTPBody` property of the request.
@see https://github.com/AFNetworking/AFNetworking/issues/1398
*/
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(nullable void (^)(NSError * _Nullable error))handler;
頭文件中基本就是以上東西,現(xiàn)在我們應(yīng)該如饑似渴,迫不及待的開始看.m文件了。
@interface AFHTTPRequestSerializer ()
@property (readwrite, nonatomic, strong) NSMutableSet *mutableObservedChangedKeyPaths;
@property (readwrite, nonatomic, strong) NSMutableDictionary *mutableHTTPRequestHeaders;
@property (readwrite, nonatomic, strong) dispatch_queue_t requestHeaderModificationQueue;
@property (readwrite, nonatomic, assign) AFHTTPRequestQueryStringSerializationStyle queryStringSerializationStyle;
@property (readwrite, nonatomic, copy) AFQueryStringSerializationBlock queryStringSerialization;
@end
@implementation AFHTTPRequestSerializer
+ (instancetype)serializer {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.stringEncoding = NSUTF8StringEncoding;
self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);
// 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"];
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
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];
// 在AFHTTPRequestSerializer的初始化方法init中就初始化了集合mutableObservedChangedKeyPaths。并且遍歷AFHTTPRequestSerializerObservedKeyPaths數(shù)組,為每一項添加觀察
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;
}
- (void)dealloc {
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
[self removeObserver:self forKeyPath:keyPath context:AFHTTPRequestSerializerObserverContext];
}
}
}
首先是初始化方法,乍看這里感覺好復(fù)雜,其實一點不復(fù)雜。初始化方法說到底就是初始化。它首先初始化了幾個屬性。stringEncoding屬性初始化默認(rèn)為NSUTF8StringEncoding;然后初始化了用于裝HTTP請求頭屬性的字典mutableHTTPRequestHeaders;還初始化了修改請求頭時而專門創(chuàng)建的隊列requestHeaderModificationQueue。
接下來是一大串亂糟糟的代碼,仔細(xì)看就明白了:此時既然已初始化了裝置請求頭屬性的字典,那不就可以先設(shè)置一些可以設(shè)置的請求頭屬性了。即Accept-Language和User-Agent。
初始化方法的最后為序列化需要觀察的屬性添加了監(jiān)聽。這里是指哪些需要觀察的屬性字段呢?從上面代碼可以看到,它是由一個C函數(shù)獲取的AFHTTPRequestSerializerObservedKeyPaths(),返回了一個數(shù)組,數(shù)組便是需要觀察的屬性字段的數(shù)組。
而當(dāng)我們設(shè)置了這些HTTP配置屬性的值時,就會觸發(fā)觀察回調(diào)的方法,在此方面里將該屬性字符串放入了mutableObservedChangedKeyPaths數(shù)組。代碼如下:
// 當(dāng)我們設(shè)置了這些HTTP配置屬性的值時,就會觸發(fā)觀察回調(diào)方法。并將該屬性字符串放入mutableObservedChangedKeyPaths集合
- (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];
}
}
}
接著往下看:
- (NSDictionary *)HTTPRequestHeaders {
NSDictionary __block *value;
dispatch_sync(self.requestHeaderModificationQueue, ^{
value = [NSDictionary dictionaryWithDictionary:self.mutableHTTPRequestHeaders];
});
return value;
}
- (void)setValue:(NSString *)value
forHTTPHeaderField:(NSString *)field
{
dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
[self.mutableHTTPRequestHeaders setValue:value forKey:field];
});
}
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
NSString __block *value;
dispatch_sync(self.requestHeaderModificationQueue, ^{
value = [self.mutableHTTPRequestHeaders valueForKey:field];
});
return value;
}
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password
{
NSData *basicAuthCredentials = [[NSString stringWithFormat:@"%@:%@", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64AuthCredentials = [basicAuthCredentials base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0];
[self setValue:[NSString stringWithFormat:@"Basic %@", base64AuthCredentials] forHTTPHeaderField:@"Authorization"];
}
- (void)clearAuthorizationHeader {
dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
[self.mutableHTTPRequestHeaders removeObjectForKey:@"Authorization"];
});
}
#pragma mark -
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style {
self.queryStringSerializationStyle = style;
self.queryStringSerialization = nil;
}
- (void)setQueryStringSerializationWithBlock:(NSString *(^)(NSURLRequest *, id, NSError *__autoreleasing *))block {
self.queryStringSerialization = block;
}
上面這些代碼沒什么可說的,接著往下看,就到了最核心的方法了:
- (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);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
/*
為該HTTP的request設(shè)置配置屬性
方法AFHTTPRequestSerializerObservedKeyPaths()返回一個數(shù)組,代表我們需要關(guān)注的HTTP配置屬性。
而mutableObservedChangedKeyPaths集合代表我們已設(shè)置的配置屬性,
*/
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
/*
將傳入的parameters進(jìn)行編碼,添加到request中
*/
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
開始閱讀該方法。可以看到,首先是對幾個參數(shù)的斷言。然后以URLString生成url,再以url生成mutableRequest,并為其該HTTP請求設(shè)置配置屬性。然后它是將最核心的模塊封裝在了一個本類的方法里。我們跳入該方法,準(zhǔn)備繼續(xù)閱讀,此時發(fā)現(xiàn)不光本類中有這個方法,它的幾個子類中也實現(xiàn)了該方法。原來這個方法便是定義在AFURLRequestSerialization協(xié)議中的方法。在不同的子類中有不同的實現(xiàn),用以實現(xiàn)不同的功能。下面是本類中該方法的實現(xiàn):
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
/*
1.為request設(shè)置請求頭
*/
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
/*
2.參數(shù)序列化處理(將參數(shù)字典解析為url query)
*/
NSString *query = nil;
if (parameters) {
// 若自定義了queryStringSerialization,那么就使用自定義的queryStringSerialization構(gòu)建方式
if (self.queryStringSerialization)
{
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError); // 通過queryStringSerialization構(gòu)建query字符串
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
}
else
{
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
query = AFQueryStringFromParameters(parameters); // 由參數(shù)生成url的query部分
break;
}
}
}
// HTTPMethodsEncodingParametersInURI是個NSSet,裝了"GET"、"HEAD"、"DELETE"請求方式(在該類的初始化方法里初始化了),因為這幾個請求方式的query是直接拼接在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]];
}
}
else // 若請求方式為"POST"、"PUT"等,則需要將query設(shè)置到http body上
{
// #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]]; // 將query string編碼
}
return mutableRequest;
}
該方法里的脈絡(luò)很清晰,分為兩個步驟。第一步便是為request設(shè)置請求頭屬性;第二步是參數(shù)序列化處理:先看是否實現(xiàn)自定義的序列化處理block,若有,則調(diào)用自定義的block,讓使用者自己實現(xiàn)參數(shù)序列化;若無,則調(diào)用AFQueryStringFromParameters()方法,由參數(shù)字典轉(zhuǎn)換為query string。關(guān)于這個函數(shù)如何將字典解析為query string的釋義,這篇文章已經(jīng)解釋得很好:AFNetworking源碼閱讀(二)。總之,現(xiàn)在有了query string了,但還是要根據(jù)HTTP method的不同,將其放在正確的位置上。方法里接下來的代碼就是完成這部分的。
值得注意的是在HTTP method為POST等其他時,它設(shè)置Content-Type為application/x-www-form-urlencoded,這個代表什么意思呢?我趕緊百度了下。收集了以下干貨資料:
四種常見的 POST 提交數(shù)據(jù)方式
HTTP協(xié)議之multipart/form-data請求分析
關(guān)于 Content-Type:application/x-www-form-urlencoded 和 Content-Type:multipart/related
結(jié)尾
剛說到了Content-Type了,但時間不早了,明天把這部分補充完整。