前言
序列化,百度百科的解釋摘抄如下:
序列化 (Serialization)將對象的狀態信息轉換為可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。以后,可以通過從存儲區中讀取或反序列化對象的狀態,重新創建該對象。
1、以某種存儲形式使自定義對象持久化;
2、將對象從一個地方傳遞到另一個地方。
3、使程序更具維護性。
簡單點說就是將對象轉換為二進制流,以便存儲或傳輸,且日后也能從二進制流轉換回對象。 所以我們今天要說的請求序列化,說到底就是造一個可以在網絡上傳遞(被序列化)的、合法可用的請求。這其中就包括了參數的處理和請求頭的設置等。不管怎樣的代碼,萬變不離其宗,它的核心就是這個知識點。所以理解了這個,再去看代碼就容易多了。
我們知道,GET
請求和POST
請求對于參數是不同的處理。GET
請求是多個參數之間以&
相連,且單個參數的鍵值間以=
連接,并將參數以?
開頭,經過編碼再追加在url
后面,而POST
卻是將其放入請求體HTTPBody
的。
另外一個HTTP
連接的請求頭HTTPHeaderField
設置也是非常重要的,它提供了很多字段,用于不同場景,不同特征下的使用。
生成請求request
的地方是在AFHTTPSessionManager
中,當時我們說了該類有名為序列化器的屬性requestSerializer
,然后調用該序列化器的方法生成了一個request。當時,因為這是AFHTTPRequestSerializer
類中的方法,所以對于具體實現沒有研究。今天我們的工作就是這個。
源碼
先來看頭文件AFHTTPRequestSerializer.h
。它繼承自NSObject
,實現了AFURLRequestSerialization
協議。
首先看頭文件中定義的屬性:
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, assign) BOOL allowsCellularAccess; // 是否允許使用蜂窩網絡,默認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
意為序列化參數時的字符串編碼,默認是NSUTF8StringEncoding
;
HTTPRequestHeaders
是個字典,意為即將被序列化的HTTP的請求頭。默認包括Accept-Language
和User-Agent
等字段;
HTTPMethodsEncodingParametersInURI
是個集合對象,它表示序列化時參數被追加在url里的請求方式的集合,想想也知道,它的元素應該是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一個該類的實例對象;然后是兩個對請求頭字典操作的兩個方法;再接著是兩個關于登錄認證的方法;然后是一個設置序列化類型的方法,代表遵循什么樣的規則進行queryString轉換。參數是個枚舉,但是這個枚舉只有一個值AFHTTPRequestQueryStringDefaultStyle
。最后一個方法提供了以block形式自定義queryString轉換的接口,也就是說可以通過block回調的方式讓調用者以自己的方式完成queryString的轉換。
最后就剩下三個核心方法了。其中第一個方法便是我們在前面已經接觸過的,由HTTP method
、URLString
、parameters
返回一個請求request
。
下面代碼注釋非常占篇幅,但是注釋的很好,舍不得刪。
Creates an
NSMutableURLRequest
object 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 theparameterEncoding
property, 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;
頭文件中基本就是以上東西,現在我們應該如饑似渴,迫不及待的開始看.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數組,為每一項添加觀察
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];
}
}
}
首先是初始化方法,乍看這里感覺好復雜,其實一點不復雜。初始化方法說到底就是初始化。它首先初始化了幾個屬性。stringEncoding
屬性初始化默認為NSUTF8StringEncoding
;然后初始化了用于裝HTTP請求頭屬性的字典mutableHTTPRequestHeaders
;還初始化了修改請求頭時而專門創建的隊列requestHeaderModificationQueue
。
接下來是一大串亂糟糟的代碼,仔細看就明白了:此時既然已初始化了裝置請求頭屬性的字典,那不就可以先設置一些可以設置的請求頭屬性了。即Accept-Language
和User-Agent
。
初始化方法的最后為序列化需要觀察的屬性添加了監聽。這里是指哪些需要觀察的屬性字段呢?從上面代碼可以看到,它是由一個C函數獲取的AFHTTPRequestSerializerObservedKeyPaths()
,返回了一個數組,數組便是需要觀察的屬性字段的數組。
而當我們設置了這些HTTP配置屬性的值時,就會觸發觀察回調的方法,在此方面里將該屬性字符串放入了mutableObservedChangedKeyPaths數組。代碼如下:
// 當我們設置了這些HTTP配置屬性的值時,就會觸發觀察回調方法。并將該屬性字符串放入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設置配置屬性
方法AFHTTPRequestSerializerObservedKeyPaths()返回一個數組,代表我們需要關注的HTTP配置屬性。
而mutableObservedChangedKeyPaths集合代表我們已設置的配置屬性,
*/
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
/*
將傳入的parameters進行編碼,添加到request中
*/
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
開始閱讀該方法??梢钥吹?,首先是對幾個參數的斷言。然后以URLString
生成url
,再以url
生成mutableRequest
,并為其該HTTP請求設置配置屬性。然后它是將最核心的模塊封裝在了一個本類的方法里。我們跳入該方法,準備繼續閱讀,此時發現不光本類中有這個方法,它的幾個子類中也實現了該方法。原來這個方法便是定義在AFURLRequestSerialization
協議中的方法。在不同的子類中有不同的實現,用以實現不同的功能。下面是本類中該方法的實現:
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
/*
1.為request設置請求頭
*/
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
/*
2.參數序列化處理(將參數字典解析為url query)
*/
NSString *query = nil;
if (parameters) {
// 若自定義了queryStringSerialization,那么就使用自定義的queryStringSerialization構建方式
if (self.queryStringSerialization)
{
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError); // 通過queryStringSerialization構建query字符串
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
}
else
{
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
query = AFQueryStringFromParameters(parameters); // 由參數生成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設置到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;
}
該方法里的脈絡很清晰,分為兩個步驟。第一步便是為request設置請求頭屬性;第二步是參數序列化處理:先看是否實現自定義的序列化處理block,若有,則調用自定義的block,讓使用者自己實現參數序列化;若無,則調用AFQueryStringFromParameters()
方法,由參數字典轉換為query string。關于這個函數如何將字典解析為query string的釋義,這篇文章已經解釋得很好:AFNetworking源碼閱讀(二)??傊?,現在有了query string了,但還是要根據HTTP method的不同,將其放在正確的位置上。方法里接下來的代碼就是完成這部分的。
值得注意的是在HTTP method為POST
等其他時,它設置Content-Type
為application/x-www-form-urlencoded
,這個代表什么意思呢?我趕緊百度了下。收集了以下干貨資料:
四種常見的 POST 提交數據方式
HTTP協議之multipart/form-data請求分析
關于 Content-Type:application/x-www-form-urlencoded 和 Content-Type:multipart/related
結尾
剛說到了Content-Type
了,但時間不早了,明天把這部分補充完整。