美文网首页暂时没看却需要看的iOS老司机第三方框架
AFNetworking 3.0 源码解读 总结(干货)(上)

AFNetworking 3.0 源码解读 总结(干货)(上)

作者: 老马的春天 | 来源:发表于2016-09-02 11:24 被阅读2499次

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要。记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点。它就像时间一样,会自己消散。

    前言

    终于写完了 AFNetworking 的源码解读。这一过程耗时数天。当我回过头又重头到尾的读了一篇,又有所收获。不禁让我想起了当初上学时的种种情景。我们应该对知识进行反复的记忆和理解。下边是我总结的 AFNetworking 中能够学到的知识点。

    1.枚举(enum)

    使用原则:当满足一个有限的并具有统一主题的集合的时候,我们就考虑使用枚举。这在很多框架中都验证了这个原则。最重要的是能够增加程序的可读性

    示例代码:

    /**
     *  网络类型 (需要封装为一个自己的枚举)
     */
    typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) {
        /**
         *  未知
         */
        AFNetworkReachabilityStatusUnknown          = -1,
        /**
         *  无网络
         */
        AFNetworkReachabilityStatusNotReachable     = 0,
        /**
         *  WWAN 手机自带网络
         */
        AFNetworkReachabilityStatusReachableViaWWAN = 1,
        /**
         *  WiFi
         */
        AFNetworkReachabilityStatusReachableViaWiFi = 2,
    };
    

    2.注释

    我们必须知道一个事实,注释的代码是不会编译到目标文件的,因此放心大胆的注释吧。在平日里的开发中,应该经常问问自己是否把每段代码都当成写API那样对待?

    曾经看过两种不同的说辞,一种是说把代码注释尽量少些,要求代码简介可读性强。另一种是说注释要详细,着重考虑他人读代码的感受。个人感觉还是写详细一点比较好,因为可能过一段时间之后,自己再去看自己当时写的代码可能就不记得了。很有可能在写这些繁琐的注释的过程中,能够想到些什么,比如如何合并掉一些没必要的方法等等。

    示例代码:

    /*!
        @header SCNetworkReachability
        @discussion The SCNetworkReachability API allows an application to
            determine the status of a system's current network
            configuration and the reachability of a target host.
            In addition, reachability can be monitored with notifications
            that are sent when the status has changed.
    
            "Reachability" reflects whether a data packet, sent by
            an application into the network stack, can leave the local
            computer.
            Note that reachability does <i>not</i> guarantee that the data
            packet will actually be received by the host.
     */
    
    /*!
        @typedef SCNetworkReachabilityRef
        @discussion This is the handle to a network address or name.
     */
    typedef const struct CF_BRIDGED_TYPE(id) __SCNetworkReachability * SCNetworkReachabilityRef;
    

    3.BOOL属性的property书写规则

    通常我们在定义一个BOOL属性的时候,要自定义getter方法,这样做的目的是为了增加程序的可读性。Apple中的代码也是这么写的。

    示例代码:

    /**
     Whether or not the network is currently reachable.
     */
    @property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;
    
    // setter
    self.reachable = YES;
    // getter
    if (self.isReachable) {}
    

    4.按功能区分代码

    假如我们写的一个控制器中大概有500行代码,我们应该保证能够快速的找到我们需要查找的内容,这就需要把代码按照功能来分隔。

    通常在.h中 我们可以使用一个自定义的特殊的注释来分隔,在.m中使用#pragma mark -来分隔。

    示例代码:

    ///---------------------
    /// @name Initialization
    ///---------------------
    
    ///------------------------------
    /// @name Evaluating Server Trust
    ///------------------------------
    
    #pragma mark - UI
    ...设置UI相关
    #pragma mark - Data
    ...处理数据
    #pragma mark - Action
    ...点击事件
    

    5.通知

    我们都知道通知可以用来传递事件和数据,但要想用好它,也不太容易。在 AFNetworking 事件和数据的传递使用的是通知和Block,按照AFNetworking对通知的使用习惯。我总结了几点:

    • 原则:如果我们需要传递事件或数据,可采用代理和Block,同时额外增加一个通知。因为通知具有跨多个界面的优点。
    • 释放问题:在接收通知的页面,一定要记得移除监听。
    • 使用方法:在.h中 FOUNDATION_EXPORT + NSString * const +通知名 在.m中赋值。如果在别的页面用到这个通知,使用extern + NSString * const +通知名就可以了。

    ps: FOUNDATION_EXPORT 和#define 都能定义常量。FOUNDATION_EXPORT 能够使用==进行判断,效率略高。而且能够隐藏定义细节(就是实现部分不在.中)

    示例代码:

    FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityDidChangeNotification;
    FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityNotificationStatusItem;
    /**
     *  网络环境发生改变的时候接受的通知
     */
    NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change";
    /**
     *  网络环境发生变化是会发送一个通知,同时携带一组状态数据,根据这个key来去除网络status
     */
    NSString * const AFNetworkingReachabilityNotificationStatusItem = @"AFNetworkingReachabilityNotificationStatusItem";
    

    6.国际化的问题

    我个人认为在开发一个APP之初,就应该考虑国际化的问题,不管日后会不会用到这个功能。当你有了国际化的思想之后,在对控件进行布局的时候,就会比只在一种语言下考虑的更多,这会让一个人对控件布局的视野更加宽阔。好了,这个问题就说这么多。有兴趣的朋友请自行查找相关内容。

    7.私有方法

    在开发中,难免会使用私有方法来协助我们达到某种目的或获取某个数据。在oc中,我看到很多人都会这样写:- (void)funName {}。个人是不赞成这样写了,除非方法内部使用了self。总之,类似于这样的方法,其实跟我们的业务并没有太大的关系。我进入一个控制器的文件中,目光应该集中在业务代码上才对。

    AFNetworking 中,一般都会把私有方法,也可以叫函数,放到头部,你即使不看这些代码,对于整个业务的理解也不会受到影响。所以,这种写法值得推荐。可以适当的使用内联函数,提高效率.

    示例代码:

    /**
     *  把枚举的值转换成字符串
     */
    NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusReachableViaWWAN:
                return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusReachableViaWiFi:
                return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusUnknown:
            default:
                return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil);
        }
    }
    
    - (NSString *)AFStringFromNetworkReachabilityStatus:(AFNetworkReachabilityStatus)status {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusReachableViaWWAN:
                return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusReachableViaWiFi:
                return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil);
            case AFNetworkReachabilityStatusUnknown:
            default:
                return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil);
        }
    }
    

    8.SCNetworkReachabilityRef(网络监控核心实现)

    SCNetworkReachabilityRef 是获取网络状态的核心对象,创建这个对象有两个方法:

    • SCNetworkReachabilityCreateWithName
    • SCNetworkReachabilityCreateWithAddress

    我们看看实现网络监控的核心代码:

    示例代码:

    - (void)startMonitoring {
        [self stopMonitoring];
    
        if (!self.networkReachability) {
            return;
        }
    
        __weak __typeof(self)weakSelf = self;
        AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
            __strong __typeof(weakSelf)strongSelf = weakSelf;
    
            strongSelf.networkReachabilityStatus = status;
            if (strongSelf.networkReachabilityStatusBlock) {
                strongSelf.networkReachabilityStatusBlock(status);
            }
    
        };
    
        SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
        SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
        SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
    
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
            SCNetworkReachabilityFlags flags;
            if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
                AFPostReachabilityStatusChange(flags, callback);
            }
        });
    }
    

    上边的方法中涉及了一些 CoreFoundation 的知识,我们来看看:

    SCNetworkReachabilityContext点进去,会发现这是一个结构体,一般c语言的结构体是对要保存的数据的一种描述

    示例代码:

    typedef struct {
        CFIndex     version;
        void *      __nullable info;
        const void  * __nonnull (* __nullable retain)(const void *info);
        void        (* __nullable release)(const void *info);
        CFStringRef __nonnull (* __nullable copyDescription)(const void *info);
    } SCNetworkReachabilityContext;
    
    1. 第一个参数接受一个signed long 的参数
    2. 第二个参数接受一个void * 类型的值,相当于oc的id类型,void * 可以指向任何类型的参数
    3. 第三个参数 是一个函数 目的是对info做retain操作
    4. 第四个参数是一个函数,目的是对info做release操作
    5. 第五个参数是 一个函数,根据info获取Description字符串

    设置网络监控分为下边几个步骤:

    1.我们先新建上下文

       SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
    

    2.设置回调

    SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
    

    3.加入RunLoop池

    SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
    

    9.键值依赖

    注册键值依赖,这个可能大家平时用的比较少。可以了解一下。举个例子:

    比如说一个类User中有两个属性
    还有一个卡片的类card
    我们写一个info的setter 和 getter 方法,

    这么做的目的是,如果我监听info这个属性,当user中的name或者age有一个改变了,能够出发info的这个监听事件。

    示例代码:

    @interface User :NSObject
    @property (nonatomic,copy)NSString *name;
    @property (nonatomic,assign)NSUInteger age;
    @end
    
    
    
    @interface card :NSObject
    @property (nonatomic,copy)NSString *info;
    @property (nonatomic,strong)User *user;
    @end
    @implementation card
    
    - (NSString *)info {
        return [NSString stringWithFormat:@"%@/%lu",_user.name,(unsigned long)_user.age];
    }
    - (void)setInfo:(NSString *)info {
        
        NSArray *array = [info componentsSeparatedByString:@"/"];
        _user.name = array[0];
        _user.age = [array[1] integerValue];
        
    }
    
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        NSArray * moreKeyPaths = nil;
    
        if ([key isEqualToString:@"info"])
        {
            moreKeyPaths = [NSArray arrayWithObjects:@"user.name", @"user.age", nil];
        }
    
        if (moreKeyPaths)
        {
            keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
        }
        
        return keyPaths;
    }
    
    @end
    

    10.HTTP

    1. HTTP协议用于客户端和服务器端之间的通信
    2. 通过请求和相应的交换达成通信
    3. HTTP是不保存状态的协议
      • HTTP自身不会对请求和相应之间的通信状态进行保存。什么意思呢?就是说,当有新的请求到来的时候,HTTP就会产生新的响应,对之前的请求和响应的保温信息不做任何存储。这也是为了快速的处理事务,保持良好的可伸展性而特意设计成这样的。
    4. 请求URI定位资源
      • URI算是一个位置的索引,这样就能很方便的访问到互联网上的各种资源。
    5. 告知服务器意图的HTTP方法
      • ①GET: 直接访问URI识别的资源,也就是说根据URI来获取资源。
      • ②POST: 用来传输实体的主体。
      • ③PUT: 用来传输文件。
      • ④HEAD: 用来获取报文首部,和GET方法差不多,只是响应部分不会返回主体内容。
      • ⑤DELETE: 删除文件,和PUT恰恰相反。按照请求的URI来删除指定位置的资源。
      • ⑥OPTIONS: 询问支持的方法,用来查询针对请求URI指定的资源支持的方法。
      • ⑦TRACE: 追踪路径,返回服务器端之前的请求通信环信息。
      • ⑧CONNECT: 要求用隧道协议连接代理,要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。SSL(Secure Sockets Layer)和TLS(Transport Layer Security)就是把通信内容加密后进行隧道传输的。
    6. 管线化让服务器具备了相应多个请求的能力
    7. Cookie让HTTP有迹可循

    11.HTTPS

    HTTPS是一个通信安全的解决方案,可以说相对已经非常安全。为什么它会是一个很安全的协议呢?下边会做出解释。大家可以看看这篇文章,解释的很有意思 。《简单粗暴系列之HTTPS原理》.

    HTTP + 加密 + 认证 + 完整性保护 = HTTPS

    其实HTTPS是身披SSL外壳的HTTP,这句话怎么理解呢?

    大家应该都知道HTTP是应用层的协议,但HTTPS并非是应用层的一种新协议,只是HTTP通信接口部分用SSL或TLS协议代替而已。

    通常 HTTP 直接和TCP通信,当使用SSL时就不同了。要先和SSL通信,再由SSL和TCP通信。

    这里再说一些关于加密的题外话:

    现如今,通常加密和解密的算法都是公开的。举个例子: a * b = 200,加入a是你知道的密码,b是需要被加密的数据,200 是加密后的结果。那么这里这个*号就是一个很简单的加密算法。这个算法是如此简单。但是如果想要在不知道a和b其中一个的情况下进行破解也是很困难的。就算我们知道了200 然后得到a b 这个也很难。假设知道了密码a 那么b就很容易算出b = 200 / a 。

    实际中的加密算法比这个要复杂的多。

    介绍两种常用加密方法:

    1. 共享密钥加密

    2. 公开密钥加密

    共享密钥加密就是加密和解密通用一个密钥,也称为对称加密。优点是加密解密速度快,缺点是一旦密钥泄露,别人也能解密数据。

    公开密钥加密恰恰能解决共享密钥加密的困难,过程是这样的:

    • ①发文方使用对方的公开密钥进行加密

    • ②接受方在使用自己的私有密钥进行解密

    关于公开密钥,也就是非对称加密 可以看看这篇文章 RSA算法原理

    原理都是一样的,这个不同于刚才举得a和b的例子,就算知道了结果和公钥,破解出被机密的数据是非常难的。这里边主要涉及到了复杂的数学理论。

    HTTPS采用混合加密机制

    HTTPS采用共享密钥加密和公开密钥加密两者并用的混合加密机制。

    注意黄色的部分,这个指明了,我们平时使用的一个场景。这篇文章会很长,不仅仅是为了解释HTTPS,还为了能够增加记忆,当日后想看看的时候,就能通过读这边文章想起大部分的HTTPS的知识。下边解释一些更加详细的HTTPS过程。

    12.如何获取证书中的PublicKey

    // 在证书中获取公钥
    static id AFPublicKeyForCertificate(NSData *certificate) {
        id allowedPublicKey = nil;
        SecCertificateRef allowedCertificate;
        SecCertificateRef allowedCertificates[1];
        CFArrayRef tempCertificates = nil;
        SecPolicyRef policy = nil;
        SecTrustRef allowedTrust = nil;
        SecTrustResultType result;
    
        // 1. 根据二进制的certificate生成SecCertificateRef类型的证书
        // NSData *certificate 通过CoreFoundation (__bridge CFDataRef)转换成 CFDataRef
        // 看下边的这个方法就可以知道需要传递参数的类型
        /* 
         SecCertificateRef SecCertificateCreateWithData(CFAllocatorRef __nullable allocator,
         CFDataRef data) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
         */
        allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
        
        // 2.如果allowedCertificate为空,则执行标记_out后边的代码
        __Require_Quiet(allowedCertificate != NULL, _out);
    
        // 3.给allowedCertificates赋值
        allowedCertificates[0] = allowedCertificate;
        
        // 4.新建CFArra: tempCertificates
        tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
    
        // 5. 新建policy为X.509
        policy = SecPolicyCreateBasicX509();
        
        // 6.创建SecTrustRef对象,如果出错就跳到_out标记处
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
        // 7.校验证书的过程,这个不是异步的。
        __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    
        // 8.在SecTrustRef对象中取出公钥
        allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
    
    _out:
        if (allowedTrust) {
            CFRelease(allowedTrust);
        }
    
        if (policy) {
            CFRelease(policy);
        }
    
        if (tempCertificates) {
            CFRelease(tempCertificates);
        }
    
        if (allowedCertificate) {
            CFRelease(allowedCertificate);
        }
    
        return allowedPublicKey;
    }
    

    在二进制的文件中获取公钥的过程是这样

    • ① NSData *certificate -> CFDataRef -> (SecCertificateCreateWithData) -> SecCertificateRef allowedCertificate
    • ②判断SecCertificateRef allowedCertificate 是不是空,如果为空,直接跳转到后边的代码
    • ③allowedCertificate 保存在allowedCertificates数组中
    • ④allowedCertificates -> (CFArrayCreate) -> SecCertificateRef allowedCertificates[1]
    • ⑤根据函数SecPolicyCreateBasicX509() -> SecPolicyRef policy
    • ⑥SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust) -> 生成SecTrustRef allowedTrust
    • ⑦SecTrustEvaluate(allowedTrust, &result) 校验证书
    • ⑧(__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust) -> 得到公钥id allowedPublicKey

    这个过程我们平时也不怎么用,了解下就行了,真需要的时候知道去哪里找资料就行了。

    这里边值得学习的地方是:

    __Require_Quiet 和 __Require_noErr_Quiet 这两个宏定义。

    我们看看他们内部是怎么定义的


    可以看出这个宏的用途是:当条件返回false时,执行标记以后的代码


    可以看出这个宏的用途是:当条件抛出异常时,执行标记以后的代码

    这样就有很多使用场景了。当必须要对条件进行判断的时候,我们有下边几种方案了

    1. #ifdef 这个是编译特性

    2. if else 代码层次的判断

    3. __Require_XXX


    _out 就是一个标记,这段代码__Require_Quiet 到_out之间的代码不会执行

    13.URL编码

    关于什么叫URI编码和为什么要编码,请看我转载的这篇文章url 编码(percentcode 百分号编码)

    根据RFC 3986的规定:URL百分比编码的保留字段分为:
    • ':' '#' '[' ']' '@' '?' '/'
    • '!' '$' '&' ''' '(' ')' '*' '+' ',' ';' '='

    在对查询字段百分比编码时,'?'和'/'可以不用编码,其他的都要进行编码。我记得在使用支付宝支付时,在对数据进行URL编码时要求编码'/'.

    NSString * AFPercentEscapedStringFromString(NSString *string) {
        static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
        static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
        // '?'和'/'在query查询允许不被转译,因此!$&'()*+,;=和:#[]@都要被转译,也就是在URLQueryAllowedCharacterSet中删除掉这些字符
        NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
        [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];
    
        // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
        // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        static NSUInteger const batchSize = 50;
        NSUInteger index = 0;
        NSMutableString *escaped = @"".mutableCopy;
    
        while (index < string.length) {
            //http://www.jianshu.com/p/eb03e20f7b1c
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wgnu"
            NSUInteger length = MIN(string.length - index, batchSize);
    #pragma GCC diagnostic pop
            NSRange range = NSMakeRange(index, length);
            // To avoid breaking up character sequences such as 👴🏻👮🏽
            range = [string rangeOfComposedCharacterSequencesForRange:range];
    
            NSString *substring = [string substringWithRange:range];
            NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
            [escaped appendString:encoded];
    
            index += range.length;
        }
        return escaped;
    }
    

    上边的这个方法可以作为URL编码的通用方法,可以直接使用,也可以写到NSString的分类中。YYModel就有这个方法。

    这里值得注意的是:

    • 字符串需要经过过滤 ,过滤法则通过 NSMutableCharacterSet 实现。添加规则后,只对规则内的因子进行编码。
    • 为了处理类似emoji这样的字符串,rangeOfComposedCharacterSequencesForRange 使用了while循环来处理,也就是把字符串按照batchSize分割处理完再拼回。

    14.HTTPBody

    我们有必要了解下请求提body的组成部分。先看下一个HTTTP请求是什么样的?

    某app的一个登录POST请求:

    POST / HTTP/1.1
    Host: log.nuomi.com
    Content-Type: multipart/form-data; boundary=Boundary+6D3E56AA6EAA83B7
    Cookie: access_log=7bde65268e2260bb0a85c7de2c67c468; BAIDUID=428D86FDBA6028DE2A5496BE3E7FC308:FG=1; BAINUOCUID=4368e1b7499c455dcd437da336ca1ca9feb8f57d; BDUSS=Ecwa3NvN1NjNWhsVGxWZktFfkc2bzJxQjZ3RFJpTFBiUzZqZUJZU0ZTSmZsN0ZXQVFBQUFBJCQAAAAAAAAAAAEAAABxbLRYWXV1dXV3dXV1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF8KilZfCopWR; bn_na_copid=60139b4b2ba75706fc384d987c2e4007; bn_na_ctag=W3siayI6Imljb25fMSIsInMiOiJ0dWFuIiwidiI6IjMyNiIsInQiOiIxNDUxODg2OTE0In1d; channel=user_center%7C%7C; channel_content=; channel_webapp=webapp; condition=6.0.3; domainUrl=sh; na_qab=6be39bfce918bb7b51887412e009faa6; UID=1488219249
    Connection: keep-alive
    Accept: */*
    User-Agent: Bainuo/6.1.0 (iPhone; iOS 9.0; Scale/2.00)
    Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9
    Content-Length: 22207
    Accept-Encoding: gzip, deflate
    
    --Boundary+6D3E56AA6EAA83B7 /// 开始
    Content-Disposition: form-data; name="app_version"
    
    6.1.0
    --Boundary+6D3E56AA6EAA83B7
    

    HTTP请求头我们就暂时不说了,看这个body的内容

    --Boundary+6D3E56AA6EAA83B7 /// 开始
    Content-Disposition: form-data; name="app_version"
    
    6.1.0
    --Boundary+6D3E56AA6EAA83B7
    

    组成分为4个部分: 1.初始边界 2.body头 3.body 4.结束边界。 下边就会用着这些知识。

    15.保证方法在主线程执行

    有时候我们必须要确保某个方法在主线程调用,就可以使用下边的思路来做。

    - (BOOL)transitionToNextPhase {
        
        // 保证代码在主线程
        if (![[NSThread currentThread] isMainThread]) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self transitionToNextPhase];
            });
            return YES;
        }
    }
    

    16.代码跟思想的碰撞

    示例代码:

    - (BOOL)transitionToNextPhase {
        
        // 保证代码在主线程
        if (![[NSThread currentThread] isMainThread]) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self transitionToNextPhase];
            });
            return YES;
        }
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wcovered-switch-default"
        switch (_phase) {
            case AFEncapsulationBoundaryPhase:
                _phase = AFHeaderPhase;
                break;
            case AFHeaderPhase:  // 打开流,准备接受数据
                [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
                [self.inputStream open];
                _phase = AFBodyPhase;
                break;
            case AFBodyPhase: // 关闭流
                [self.inputStream close];
                _phase = AFFinalBoundaryPhase;
                break;
            case AFFinalBoundaryPhase:
            default:
                _phase = AFEncapsulationBoundaryPhase;
                break;
        }
        // 重置offset
        _phaseReadOffset = 0;
    #pragma clang diagnostic pop
    
        return YES;
    }
    

    回过头来看这段代码,我又有新的想法。原本对数据的操作,对body的操作,是一件很复杂的事情。但作者的思路非常清晰。就像上边这个方法一样,它只实现一个功能,就是切换body组成部分。它只做了这一件事,我们在开发中,如遇到有些复杂的功能,在写方法的时候,可能考虑了很多东西,当时所有的考虑可能都写到一个方法中了。

    能不能写出一个思路图,先不管思路的实现如何,先一一列出来,最后在一一实现,一一拼接起来。

    17.NSInputStream

    NSInputStream有好几种类型,根据不同的类型返回不同方法创建的NSInputStream

    示例代码:

    - (NSInputStream *)inputStream {
        if (!_inputStream) {
            if ([self.body isKindOfClass:[NSData class]]) {
                _inputStream = [NSInputStream inputStreamWithData:self.body];
            } else if ([self.body isKindOfClass:[NSURL class]]) {
                _inputStream = [NSInputStream inputStreamWithURL:self.body];
            } else if ([self.body isKindOfClass:[NSInputStream class]]) {
                _inputStream = self.body;
            } else {
                _inputStream = [NSInputStream inputStreamWithData:[NSData data]];
            }
        }
    
        return _inputStream;
    }
    

    18.对文件的操作

    1. NSParameterAssert() 用来判断参数是否为空,如果为空就抛出异常
    2. 使用isFileURL 判断一个URL是否为fileURL 使用checkResourceIsReachableAndReturnError判断路径能够到达
    3. 使用 [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error] 获取本地文件属性
    4. lastPathComponent ,https://www.baidu.com/abc.html 结果就是abc.html
    5. pathExtension https://www.baidu.com/abc.html 结果就是html

    19.NSURLRequestCachePolicy缓存策略

    这个要仔细介绍下,在某些特殊的场景下还是能用到的。我们点开NSURLRequestCachePolicy 可以看到是一个枚举值

    typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
    {
        NSURLRequestUseProtocolCachePolicy = 0,
    
        NSURLRequestReloadIgnoringLocalCacheData = 1,
        NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
        NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
    
        NSURLRequestReturnCacheDataElseLoad = 2,
        NSURLRequestReturnCacheDataDontLoad = 3,
    
        NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
    };
    

    NSURLRequestUseProtocolCachePolicy 这个是默认的缓存策略,缓存不存在,就请求服务器,缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端。

    • NSURLRequestReloadIgnoringLocalCacheData 这个策略是不管有没有本地缓存,都请求服务器。
    • NSURLRequestReloadIgnoringLocalAndRemoteCacheData 这个策略会忽略本地缓存和中间代理 直接访问源server
    • NSURLRequestReturnCacheDataElseLoad 这个策略指,有缓存就是用,不管其有效性,即Cache-Control字段 ,没有就访问源server
    • NSURLRequestReturnCacheDataDontLoad 这个策略只加载本地数据,不做其他操作,适用于没有网路的情况
    • NSURLRequestReloadRevalidatingCacheData 这个策略标示缓存数据必须得到服务器确认才能使用,未实现。

    20.管线化

    在HTTP连接中,一般都是一个请求对应一个连接,每次建立tcp连接是需要一定时间的。管线化,允许一次发送一组请求而不必等到相应。但由于目前并不是所有的服务器都支持这项功能,因此这个属性默认是不开启的。管线化使用同一tcp连接完成任务,因此能够大大提交请求的时间。但是响应要和请求的顺序 保持一致才行。使用场景也有,比如说首页要发送很多请求,可以考虑这种技术。但前提是建立连接成功后才可以使用。

    相关文章

      网友评论

      本文标题:AFNetworking 3.0 源码解读 总结(干货)(上)

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