zoukankan      html  css  js  c++  java
  • 【原】AFNetworking源码阅读(二)

    【原】AFNetworking源码阅读(二)

    本文转载请注明出处 —— polobymulberry-博客园

    1. 前言


    上一篇中我们在iOS Example代码中提到了AFHTTPSessionManager中的一个函数:

    - (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
                                parameters:(nullable id)parameters
                                  progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                                   success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                   failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

    这个函数作用其实看函数名就明白了- 使用GET类型的Request创建并运行一个NSURLSessionDataTask

    2. dataTaskWithHTTPMethod

    具体我们看函数实现:

    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
                                                        URLString:URLString
                                                       parameters:parameters
                                                   uploadProgress:nil
                                                 downloadProgress:downloadProgress
                                                          success:success
                                                          failure:failure];
    
    [dataTask resume];
    
    return dataTask;
    • 使用dataTaskWithHTTPMethod方法创建了一个NSURLSessionDataTask
    • 调用NSURLSessionDataTask的resume来开启这个session task

      • 知识点:session task的几种状态的操作函数
        • suspend -- 可以让当前的任务暂停
        • resume ---- 方法不仅可以启动任务,还可以唤醒suspend状态的任务
        • cancel ----- 方法可以取消当前的任务,你也可以向处于suspend状态的任务发送cancel消息,任务如果被取消便不能再恢复到之前的状态.

    • 最后返回这个task

    我们很自然想到了,所有的关键都在dataTaskWithHTTPMethod这个函数。我们先不慌看这个函数的具体实现,先穷尽到这个函数的所有调用。我们已经知道这个函数是创建一个NSURLSessionDataTask,而系统提供给我们创建NSURLSessionDataTask的方法,有两个:

    1. 1.–dataTaskWithRequest:
    2. 2.–dataTaskWithRequest:completionHandler:

    好,那我们就沿着这个线索一直找下去,一直找到有这两个函数使用的地方。追踪溯源,还真找到了这样一条函数调用栈。

    QQ20160116-0

    上图可以看出GET、HEAD、POST、PUT、PATCH、DELETE这些方法实现的不同之处只在于调用dataTaskWithHTTPMethod:传递的method名称不同。另外在调用dataTaskWithRequest:时候,其实已经在上一级函数dataTaskWithHTTPMethod:中构建好了一个NSMutableURLRequest类型的request。所以我们主要研究dataTaskWithHTTPMethod:函数实现。

    dataTaskWithHTTPMethod函数的实现主要分两部分,一部分是构建NSMutableURLRequest,另一部分是根据已构建好的Request来构建NSURLSessionDataTask

    2.1 构建NSMutableURLRequest

    此处构建request分为两个部分:

    1. 1.先调用AFHTTPRequestSerializer的requestWithMethod函数构建request
    2. 2.处理request构建产生的错误 – serializationError

    2.1.1 requestWithMethod构建request

    先直接暴力列出requestWithMethod的函数声明(注:requestWithMethod是AFHTTPRequestSerializer的一个成员函数,并且AFHTTPRequestSerializer遵循AFURLRequestSerialization协议)

    /**
     使用指定的HTTP method和URLString来构建一个NSMutableURLRequest对象实例
    
     如果method是GET、HEAD、DELETE,那parameter将会被用来构建一个基于url编码的查询字符串(query url)
     ,并且这个字符串会直接加到request的url后面。对于其他的Method,比如POST/PUT,它们会根
     据parameterEncoding属性进行编码,而后加到request的http body上。
     @param method request的HTTP methodt,比如 `GET`, `POST`, `PUT`, or `DELETE`. 该参数不能为空
     @param URLString 用来创建request的URL
     @param parameters 既可以对method为GET的request设置一个查询字符串(query string),也可以设置到request的HTTP body上
     @param error 构建request时发生的错误
    
     @return  一个NSMutableURLRequest的对象
     */
    - (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                     URLString:(NSString *)URLString
                                    parameters:(nullable id)parameters
                                         error:(NSError * _Nullable __autoreleasing *)error;

    接着我们来看requestWithMethod的具体实现:

    • 第一步:进行url转化和参数化断言
    NSParameterAssert(method);
    NSParameterAssert(URLString);
    
    NSURL *url = [NSURL URLWithString:URLString];
    
    NSParameterAssert(url);

    其中NSParameterAssert(method) <=> NSParameterAssert(method != nil),同理NSParameterAssert(URLString)和NSParameterAssert(url)也一样。这里NShipster给出了一个金科玉律:

    方法或函数应当在代码最开始处使用 NSParameterAssert / NSCParameterAssert 来强制输入的值满足先验条件,这是一条金科玉律;其他情况下使用 NSAssert / NSCAssert

    • 第二步:使用url构建并初始化NSMutableURLRequest,然后设置HTTPMethod
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;
    • 第三步:给NSMutableURLRequest自带的属性赋值

    NSURLRequest/NSMutableURLRequest需要赋值的属性可以在AFHTTPRequestSerializerObservedKeyPaths()中找到,我们可以进去看一下:

    // 定义了一个static的方法,表示该方法只能在本文件中使用
    // 函数整体上使用了单例模式
    static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
        static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
        static dispatch_once_t onceToken;
        // 此处需要observer的keypath为allowsCellularAccesscachePolicyHTTPShouldHandleCookies
        // HTTPShouldUsePipeliningnetworkServiceTypetimeoutInterval
        dispatch_once(&onceToken, ^{
            _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
        });
    
        return _AFHTTPRequestSerializerObservedKeyPaths;
    }

    简单介绍下上面添加的keypath:

    /**
     是否允许使用设备的蜂窝移动网络来创建request,默认为允许:
     */
    @property (nonatomic, assign) BOOL allowsCellularAccess;
    
    /**
     创建的request所使用的缓存策略,默认使用`NSURLRequestUseProtocolCachePolicy`,该策略表示 
     如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断
     下一步操作,如: Cache-Control字段为must-revalidata, 则 询问服务端该数据是否有更新,无更新话
     直接返回给用户缓存数据,若已更新,则请求服务端.
     */
    @property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy;
    
    /**
     如果设置HTTPShouldHandleCookies为YES,就处理存储在NSHTTPCookieStore中的cookies
     HTTPShouldHandleCookies表示是否应该给request设置cookie并随request一起发送出去
     */
    @property (nonatomic, assign) BOOL HTTPShouldHandleCookies;
    
    /**
     HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送。
     如果为YES表示可以,NO表示必须等receiver收到先前的回复才能发送下个信息。
     */
    @property (nonatomic, assign) BOOL HTTPShouldUsePipelining;
    
    /**
     设定request的network service类型. 默认是`NSURLNetworkServiceTypeDefault`.
     这个network service是为了告诉系统网络层这个request使用的目的
     比如NSURLNetworkServiceTypeVoIP表示的就这个request是用来请求网际协议通话技术(Voice over IP)。
     系统能根据提供的信息来优化网络处理,从而优化电池寿命,网络性能等等
     */
    @property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;
    
    /**
     超时机制,默认60秒
     */
    @property (nonatomic, assign) NSTimeInterval timeoutInterval;

    然后通过判断mutableObservedChangedKeyPaths(NSMutableSet)中是否有这个keyPath,来设定mutableRequest对应的keyPath值。

    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }

    至于mutableObservedChangedKeyPaths是什么,我们可以在AFURLRequestSerialization文件中的observeValueForKeyPath函数中得到答案。整个过程是这样的

    image

    关键就是在哪里会产生keypath的值变化了的消息

    image

    也就是说你只要使用了keyPath对应的的setter方法,就会响应observerValueForKeyPath这个方法,从而将对应的keyPath添加到了mutableObservedChangedKeyPaths。至于添加keyPath到observer中,那是在AFHTTPRequestSerializer的init中干的活:

    image

    1. 第四步:将传入的parameters进行编码,并添加到request中

    此过程主要集中在requestBySerializingRequest这个函数中。在介绍requestBySerializingRequest之前,先简单介绍下,为什么会有这个函数的存在?


    引用自AFNetworking2.0源码解析<二>

    一般我们请求都会按key=value的方式带上各种参数,GET方法参数直接加在URL上,POST方法放在body上,NSURLRequest没有封装好这个参数的解析,只能我们自己拼好字符串。AFNetworking提供了接口,让参数可以是NSDictionary, NSArray, NSSet这些类型,再由内部解析成字符串后赋给NSURLRequest。

    转化过程大致是这样的:

    @{ 
         @"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

    或者看下面这段解释:

    比如说我定义了下面这个parameter:

    NSString *URLString = @"http://example.com";
    NSDictionary *parameters = @{@"foo": @"bar", @"baz": @[@1, @2, @3]};

    使用GET方式,最后得到的request是这样的:

    [[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:URLString parameters:parameters error:nil];
    
    GET http://example.com?foo=bar&baz[]=1&baz[]=2&baz[]=3

    或者使用POST方式,最后得到的request是这样的:

    [[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

    requestBySerializingRequest也分为三个部分:

    • requestBySerializingRequest函数 - 第一部分

    设置request的http header field:

    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];

    这里关于http header field的值都存放在了HTTPRequestHeaders中了。至于HTTPRequestHeaders的设置,是在多个函数中都有设置的。此处就不一一赘述,后面遇到会详解。

    • requestBySerializingRequest函数 - 第二部分

    根据parameter来构建查询字符串,这里一开始parameter如下:

    Printing description of parameters:
    {
        baz =     (
            1,
            2,
            3
        );
        foo = bar;
    }

    经过构建后,得到query为(这个例子中的构建方式使用的是AFQueryStringFromParameters()函数):

    Printing description of query:
    baz[]=1&baz[]=2&baz[]=3&foo=bar

    事实上代码中有两种构建query的方式:

    其中一种就是,如果自定义了queryStringSerialization(AFQueryStringSerializationBlock的block变量)。那么就使用自定义的queryStringSerialization构建方式(此方法在AFNetworking的test中用的比较多)

    还有一种就是上面的那个AFQueryStringFromParameters()函数,我们可以看到AFQueryStringFromParameters的调用结构是下图这样的:

    image

    讲解的话,我觉得根据上图从后往前讲比较好:

    首先看NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value)这个函数

    该函数首先定义了一个NSSortDescriptor *sortDescriptor:

    // 根据需要排列的对象的description来进行升序排列,并且selector使用的是compare:
    // 因为对象的description返回的是NSString,所以此处compare:使用的是NSString的compare函数
    // 即@[@"foo", @"bar", @"bae"] ----> @[@"bae", @"bar",@"foo"]
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    接着会对value的类型进行判断,有NSDictionary、NSArray、NSSet类型。不过有人就会问了,在AFQueryStringPairsFromDictionary中给AFQueryStringPairsFromKeyAndValue函数传入的value不是NSDictionary嘛?还要判断那么多类型干啥?对,问得很好,这就是AFQueryStringPairsFromKeyAndValue的核心----递归调用并解析,你不能保证NSDictionary的value中存放的是一个NSArray、NSSet。

    既然是递归,那么就要有结束递归的情况,比如解析到最后,对应value是一个NSString,那么就得调用函数中最后的else语句:

    else {
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }

    注意此处定义了一个AFQueryStringPair:

    @interface AFQueryStringPair : NSObject
    @property (readwrite, nonatomic, strong) id field;
    @property (readwrite, nonatomic, strong) id value;
    // ...
    @end

    而initWithField做的就是将key赋给field,value赋值给value。大家可以回头看一下最开始举的那个例子,就产生了对应的field-value。

    接着回到AFQueryStringPairsFromDictionary函数,好像没啥好说的。再回到AFQueryStringFromParameters函数,这个函数就是把这些构建好的AFQueryStringPair一个个用&连接好。这里注意一点就是,此处会对AFQueryStringPair使用其URLEncodedStringValue函数做一定的处理,其实就是Percent-encoding(百分号编码)。


    知识点:百分号编码

    根据RFC 3986,以下字符为保留字:

    image

    另外,在RFC 3989 – Section 3.4部分,“?”和“/”当作为URL中的query string的时候,不再当做保留字。

    此处主要是通过stringByAddingPercentEncodingWithAllowedCharacters函数来给我们的string进行百分号编码的。其中stringByAddingPercentEncodingWithAllowedCharacters函数需要传入不需要百分号编码的字符集(也就是不包括上面说的保留字,即函数中构建的allowedCharacterSet)。另外,为了防止image字符造成的问题,此处还需要使用rangeOfComposedCharacterSequencesForRange函数来处理字符长度。

    举个例子,如果我传入的字符串为image,那么最终得到的百分号编码的字符串为

    poloby%3A%23mulberry%5B%5D%40%F0%9F%91%B4%F0%9F%8F%BB%F0%9F%91%AE%F0%9F%8F%BD

    下图是保留字的百分号编码:

    image


    所以事实上,上面最终生成的query url中,[]都会被%5B%5D所代替。

    • requestBySerializingRequest函数 - 第三部分

    最后判断该request中是否包含了GET、HEAD、DELETE(都包含在HTTPMethodsEncodingParametersInURI)。因为这几个method的quey是拼接到url后面的。而POST、PUT是把query拼接到http body中的。

    如果method是GET、HEAD、DELETE等。最后将query合并到mutbleRequest的query url上。不过这里还是要分情况讨论,如果request的query url不为空,就在生成的query前拼接&字符,再拼接到原先的query url上,如果request的query url为空,就将生成的的query前拼接?字符,再拼接到request的url上

    Printing description of mutableRequest:
    <NSMutableURLRequest: 0x7f9a63f237c0> { URL: https://api.app.net/stream/0/posts/stream/global?baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3&foo=bar }

    如果method是POST、PUT等。最后将query设置到http body上。另外,在此之前,函数会判断request的Content-Type是否设置了,如果没有,就默认设置为application/x-www-form-urlencoded。

    2.1.2 处理serializationError

    生成request出错了怎么办?这里冒出了一个completionQueue,暂时不管它,因为我并不知道这个东西是怎么用的。一般的话,我们都是在main queue来执行自定义的failure函数处理error。

    2.2 构建NSURLSessionDataTask

    有了request后,就可以调用-[AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:]来构建session data task。

    同样地,dataTaskWithRequest函数也分为两个部分。第一部分是创建一个dataTask,第二个部分是调用addDelegateForDataTask这个函数,具体这个函数是做什么的,目前我也不是很清楚。

    2.2.1 创建dataTask

    使用了url_session_manager_create_task_safely(dispatch_block_t block)这个函数。这个函数主要的目的是为了解决iOS8之前的一个bug,详见https://github.com/AFNetworking/AFNetworking/issues/2093。在这个issue中,提问者建议版本小于iOS8的使用QUEUE_SERIAL的dispatch。所以才有了url_session_manager_create_task_safely这个函数,注意函数名中的create task和safely。由于在iOS8之后,这个bug被修复了,所以直接调用block()即可。

    static void url_session_manager_create_task_safely(dispatch_block_t block) {
        if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) {
            // Fix of bug
            // Open Radar:http://openradar.appspot.com/radar?id=5871104061079552 (status: Fixed in iOS8)
            // Issue about:https://github.com/AFNetworking/AFNetworking/issues/2093
            dispatch_sync(url_session_manager_creation_queue(), block);
        } else {
            block();
        }
    }

    2.2.2 addDelegateForDataTask

    字面上理解的话,就是给data task添加了一个delegate,而这个delegate的类型为AFURLSessionManagerTaskDelegate。为什么要给task加一个delegate?

    我们看看AFURLSessionManagerTaskDelegate的定义:

    @interface AFURLSessionManagerTaskDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>

    这里我比较疑惑,NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate这三个delegate应该NSURLSession的delegate,你这边出现了一个AFURLSessionManagerTaskDelegate也来实现这三个delegate是几个意思?我猜测这里是不是一种分离的代码的方式,就是说把NSURLSession的delegate的实现分离出来给AFURLSessionManagerTaskDelegate实现。但是搜索了一下AFURLSessionManager中的session属性的构建:

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

    这里的delegate并不是使用了AFURLSessionManagerTaskDelegate的那个delegate,所以上述猜测错误。不过我还是找到了点蛛丝马迹:

    AFURLSessionManager中session(NSURLSession)的delegate设置为了AFURLSessionManager的self,并且AFURLSessionManager确实也遵循了NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate这三个协议,也实现了其中的方法。关键是实现这些方法时用到了AFURLSessionManagerTaskDelegate的delegate中实现的方法。至于为什么要这么做,话说我也是刚看,所以还需要消化一下。

    这一篇就到此为止,下面一篇会详细介绍实现的NSURLSession的delegate方法了。

    参考文章


  • 相关阅读:
    leetcode刷题笔记十四 最长公共前缀 Scala版本
    leetcode刷题笔记十三 罗马数字转数字 Scala版本
    leetcode刷题笔记十二 整数转罗马数字 Scala版本
    Maven 安装与配置
    Maven基础
    java 打jar包配置文件和jar包通级
    java 类
    java 数组
    java 重载
    java 普通项目的配置文件
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5131983.html
Copyright © 2011-2022 走看看