美文网首页AFN源码iOS Developer
AFNetworking源码阅读4——请求序列化

AFNetworking源码阅读4——请求序列化

作者: Wang66 | 来源:发表于2016-10-17 23:11 被阅读356次

前言

序列化,百度百科的解释摘抄如下:

序列化 (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-LanguageUser-Agent等字段;
HTTPMethodsEncodingParametersInURI是个集合对象,它表示序列化时参数被追加在url里的请求方式的集合,想想也知道,它的元素应该是GETHead
其他几个属性都比较好理解,而且注释写得很清楚了,不解释了。

接着看在头文件中暴露的前几个方法:


+ (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 methodURLStringparameters返回一个请求request

下面代码注释非常占篇幅,但是注释的很好,舍不得删。

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.

/**
 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-LanguageUser-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-Typeapplication/x-www-form-urlencoded,这个代表什么意思呢?我赶紧百度了下。收集了以下干货资料:
四种常见的 POST 提交数据方式
HTTP协议之multipart/form-data请求分析
关于 Content-Type:application/x-www-form-urlencoded 和 Content-Type:multipart/related


结尾

刚说到了Content-Type了,但时间不早了,明天把这部分补充完整。

相关文章

网友评论

    本文标题:AFNetworking源码阅读4——请求序列化

    本文链接:https://www.haomeiwen.com/subject/jbqzyttx.html