前言
序列化,百度百科的解释摘抄如下:
序列化 (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
了,但时间不早了,明天把这部分补充完整。