AFNetworking 3.0 源码解读 总结

终于写完了 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就会产生新的响应,对之前的请求和响应的保温信息不做任何存储。这也是为了快速的处理事务,保持良好的可伸展性而特意设计成这样的。
  1. 请求URI定位资源
  • URI算是一个位置的索引,这样就能很方便的访问到互联网上的各种资源。
  1. 告知服务器意图的HTTP方法
  • ①GET: 直接访问URI识别的资源,也就是说根据URI来获取资源。
  • ②POST: 用来传输实体的主体。
  • ③PUT: 用来传输文件。
  • ④HEAD: 用来获取报文首部,和GET方法差不多,只是响应部分不会返回主体内容。
  • ⑤DELETE: 删除文件,和PUT恰恰相反。按照请求的URI来删除指定位置的资源。
  • ⑥OPTIONS: 询问支持的方法,用来查询针对请求URI指定的资源支持的方法。
  • ⑦TRACE: 追踪路径,返回服务器端之前的请求通信环信息。
  • ⑧CONNECT: 要求用隧道协议连接代理,要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。SSL(Secure Sockets Layer)和TLS(Transport Layer Security)就是把通信内容加密后进行隧道传输的。
  1. 管线化让服务器具备了相应多个请求的能力
  2. 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连接完成任务,因此能够大大提交请求的时间。但是响应要和请求的顺 序 保持一致才行。使用场景也有,比如说首页要发送很多请求,可以考虑这种技术。但前提是建立连接成功后才可以使用。

21.网络服务类型NSURLRequestNetworkServiceType

示例代码:

typedef NS_ENUM(NSUInteger, NSURLRequestNetworkServiceType)
{
    NSURLNetworkServiceTypeDefault = 0,    // Standard internet traffic
    NSURLNetworkServiceTypeVoIP = 1,    // Voice over IP control traffic
    NSURLNetworkServiceTypeVideo = 2,    // Video traffic
    NSURLNetworkServiceTypeBackground = 3, // Background traffic
    NSURLNetworkServiceTypeVoice = 4       // Voice data
};

可以通过这个值来指定当前的网络类型,系统会跟据制定的网络类型对很多方面进行优化,这个就设计到很细微的编程技巧了,可作为一个优化的点备用。

22.Authorization字段

在请求头中可以添加Authorization字段。

Authorization: Basic YWRtaW46YWRtaW4= 其中Basic表示基础认证,当然还有其他认证,如果感兴趣,可以看看本文开始提出的那本书。后边的YWRtaW46YWRtaW4= 是根据username:password 拼接后然后在经过Base64编码后的结果。

如果header中有 Authorization这个字段,那么服务器会验证用户名和密码,如果不正确的话会返回401错误。

23.使用流写数据

下边的代码可以说是使用流写数据的经典案例。可直接拿来使用。

示例代码:

- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
                             writingStreamContentsToFile:(NSURL *)fileURL
                                       completionHandler:(void (^)(NSError *error))handler
{
    NSParameterAssert(request.HTTPBodyStream);
    NSParameterAssert([fileURL isFileURL]);

    // 加上上边的两个判断,下边的这些代码就是把文件写到另一个地方的典型使用方法了
    NSInputStream *inputStream = request.HTTPBodyStream;
    NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
    __block NSError *error = nil;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

        [inputStream open];
        [outputStream open];

        // 读取数据
        while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
            uint8_t buffer[1024];

            NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
            if (inputStream.streamError || bytesRead < 0) {
                error = inputStream.streamError;
                break;
            }

            NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
            if (outputStream.streamError || bytesWritten < 0) {
                error = outputStream.streamError;
                break;
            }

            if (bytesRead == 0 && bytesWritten == 0) {
                break;
            }
        }

        [outputStream close];
        [inputStream close];

        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(error);
            });
        }
    });

    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    mutableRequest.HTTPBodyStream = nil;

    return mutableRequest;
}

24.NSIndexSet

定义:NSIndexSet是一个有序的,唯一的,无符号整数的集合。

我们先看个例子:

NSMutableIndexSet *indexSetM = [NSMutableIndexSet indexSet];
    [indexSetM addIndex:19];
    [indexSetM addIndex:4];
    [indexSetM addIndex:6];
    [indexSetM addIndex:8];
    [indexSetM addIndexesInRange:NSMakeRange(20, 10)];

    [indexSetM enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%lu",idx);
    }];

打印结果如下:

2016-08-10 11:39:00.826 xxxx[3765:100078] 4
2016-08-10 11:39:00.827 xxxx[3765:100078] 6
2016-08-10 11:39:00.827 xxxx[3765:100078] 8
2016-08-10 11:39:00.827 xxxx[3765:100078] 19
2016-08-10 11:39:00.827 xxxx[3765:100078] 20
2016-08-10 11:39:00.828 xxxx[3765:100078] 21
2016-08-10 11:39:00.828 xxxx[3765:100078] 22
2016-08-10 11:39:00.828 xxxx[3765:100078] 23
2016-08-10 11:39:00.828 xxxx[3765:100078] 24
2016-08-10 11:39:00.828 xxxx[3765:100078] 25
2016-08-10 11:39:00.828 xxxx[3765:100078] 26
2016-08-10 11:39:00.828 xxxx[3765:100078] 27
2016-08-10 11:39:00.828 xxxx[3765:100078] 28
2016-08-10 11:39:00.829 xxxx[3765:100078] 29

这充分说明了一下几点

  • 它是一个无符号整数的集合
  • 用addIndex方法可以添加单个整数值,使用addIndexesInRange可以添加一个范围,比如NSMakeRange(20, 10) 取20包括20后边十个整数
  • 唯一性,集合内的整数不会重复出现
  • 具体用法就不再次做详细的解释了,有兴趣的朋友可以看看这篇文章: NSIndexSet 用法

25.NSUnderlyingErrorKey优先错误

当出现可能会有两个错误的情况下,可以考虑使用NSUnderlyingErrorKey处理这种情况。

示例代码:

static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) {
    if (!error) {
        return underlyingError;
    }

    if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) {
        return error;
    }

    NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy];
    mutableUserInfo[NSUnderlyingErrorKey] = underlyingError;

    return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo];
}

26.NSJSONReadingOptions

这个选项可以设置json的读取选项,我们点进去可以看到:

typedef NS_OPTIONS(NSUInteger, NSJSONReadingOptions) {
    NSJSONReadingMutableContainers = (1UL << 0),
    NSJSONReadingMutableLeaves = (1UL << 1),
    NSJSONReadingAllowFragments = (1UL << 2)
} NS_ENUM_AVAILABLE(10_7, 5_0);
  • NSJSONReadingMutableContainers 这个解析json成功后返回一个容器
  • NSJSONReadingMutableLeaves 返回中的json对象中字符串为NSMutableString
  • NSJSONReadingAllowFragments 允许JSON字符串最外层既不是NSArray也不是NSDictionary,但必须是有效的JSON Fragment。例如使用这个选项可以解析 @“123” 这样的字符串

我们举个例子说明一下NSJSONReadingMutableContainers:

NSString *str = @"{\"name\":\"zhangsan\"}";

NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
// 应用崩溃,不选用NSJSONReadingOptions,则返回的对象是不可变的,NSDictionary
[dict setObject:@"male" forKey:@"sex"];

NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
// 没问题,使用NSJSONReadingMutableContainers,则返回的对象是可变的,NSMutableDictionary
[dict setObject:@"male" forKey:@"sex"];

NSLog(@"%@", dict);

如果服务器返回的json的最外层并不是以NSArray 或者 NSDictionary ,而是一个有效的json fragment ,比如 就返回了一个@"abc"。 那么我们使用NSJSONReadingAllowFragments这个选项也能够解析出来。

27.图片解压杂谈

AFJSONResponseSerializer使用系统内置的NSJSONSerialization解析json,NSJSON只支持解析 UTF8编码的数据(还有UTF-16LE之类的,都不常用),所以要先把返回的数据转成UTF8格式。这里会尝试用HTTP返回的编码类型和自己设置的 stringEncoding去把数据解码转成字符串NSString,再把NSString用UTF8编码转成NSData,再用 NSJSONSerialization解析成对象返回。

上述过程是NSData->NSString->NSData->NSObject,这里有个问题,如果你能确定服务端返回的是 UTF8编码的json数据,那NSData->NSString->NSData这两步就是无意义的,而且这两步进行了两次编解码,很浪费 性能,所以如果确定服务端返回utf8编码数据,就建议自己再写个JSONResponseSerializer,跳过这两个步骤。

此外AFJSONResponseSerializer专门写了个方法去除NSNull,直接把对象里值是NSNull的键去掉,还蛮贴心,若不去掉,上层很容易忽略了这个数据类型,判断了数据是否nil没判断是否NSNull,进行了错误的调用导致core。

图片解压

当我们调用UIImage的方法imageWithData:方法把数据转成UIImage对象后,其实这时UIImage对象还没准备好需要渲染 到屏幕的数据,现在的网络图像PNG和JPG都是压缩格式,需要把它们解压转成bitmap后才能渲染到屏幕上,如果不做任何处理,当你把UIImage 赋给UIImageView,在渲染之前底层会判断到UIImage对象未解压,没有bitmap数据,这时会在主线程对图片进行解压操作,再渲染到屏幕 上。这个解压操作是比较耗时的,如果任由它在主线程做,可能会导致速度慢UI卡顿的问题。

AFImageResponseSerializer除了把返回数据解析成UIImage外,还会把图像数据解压,这个处理是在子线程 (AFNetworking专用的一条线程,详见AFURLConnectionOperation),处理后上层使用返回的UIImage在主线程渲染 时就不需要做解压这步操作,主线程减轻了负担,减少了UI卡顿问题。

具体实现上在AFInflatedImageFromResponseWithDataAtScale里,创建一个画布,把UIImage画在画布 上,再把这个画布保存成UIImage返回给上层。只有JPG和PNG才会尝试去做解压操作,期间如果解压失败,或者遇到CMKY颜色格式的jpg,或者 图像太大(解压后的bitmap太占内存,一个像素3-4字节,搞不好内存就爆掉了),就直接返回未解压的图像。

另外在代码里看到iOS才需要这样手动解压,MacOS上已经有封装好的对象NSBitmapImageRep可以做这个事。

关于图片解压,还有几个问题不清楚:

1.本来以为调用imageWithData方法只是持有了数据,没有做解压相关的事,后来看到调用堆栈发现已经做了一些解压操作,从调用名字看进行了huffman解码,不知还会继续做到解码jpg的哪一步。
UIImage_jpg

2.以上图片手动解压方式都是在CPU进行的,如果不进行手动解压,把图片放进layer里,让底层自动做这个事,是会用GPU进行的解压的。不知
用GPU解压与用CPU解压速度会差多少,如果GPU速度很快,就算是在主线程做解压,也变得可以接受了,就不需要手动解压这样的优化了,不过目前没找到
方法检测GPU解压的速度。

28.获取系统版本

#ifndef NSFoundationVersionNumber_iOS_8_0
#define NSFoundationVersionNumber_With_Fixed_5871104061079552_bug 1140.11
#else
#define NSFoundationVersionNumber_With_Fixed_5871104061079552_bug NSFoundationVersionNumber_iOS_8_0
#endif

上边的这个宏的目的是通过NSFoundation的版本来判断当前ios版本,关键是这个宏的调试目标是IOS,来看看系统是怎么定义的:

那么我们就能够联想到,目前我们能够判断系统版本号的方法有几种呢?最少三种:

  • [UIDevice currentDevice].systemVersion
  • 通过比较Foundation框架的版本号,iOS系统升级的同时Foundation框架的版本也会提高
  • 通过在某系版本中新出现的方法来判断,UIAlertController 这个类是iOS8之后才出现的
    NS_CLASS_AVAILABLE_IOS(8_0),如果当前系统版本没有这个类
    NSClassFromString(@"UIAlertController" == (null),从而判断当前版本是否大于等于iOS8

这篇博文写的很详细,关于获取当前版本

29.dispatch_queue_create()

AFNetworking中所有的和创建任务相关的事件都放到了一个单例的队列中,我们平时可能会使用这些方法,但还是可能会忽略一些内
容,dispatch_queue_create()这个是队列的方法,第一个参数是队列的identifier,第二个参数则表示这个队列是串行队列还
是并行队列。

如果第二个参数为DISPATCH_QUEUE_SERIAL或NULL 则表示队列为串行队列。如果为DISPATCH_QUEUE_CONCURRENT则表示是并行队列。
关于队列的小的知识点,参考了这篇文章 Objective C 高级进阶— GCD队列浅析(一).

30.dispatch_block_t

这个方法还有一个小知识点:dispatch_block_t ,点击去可以看到:

typedef void (^dispatch_block_t)(void);

关于这个Block我们应该注意几点:

  • 非ARC情况下,Block被allocated或者copied到堆后,一定要记得释放它,通过[release]或者Block_release()
  • 非ARC情况下,Block被allocated或者copied到堆后,一定要记得释放它,通过[release]或者Block_release()

31.NSKeyValueObservingOptions

  • NSKeyValueObservingOptionNew 把更改之前的值提供给处理方法
  • NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法
  • NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值
  • NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后

32.task delegate出发问题

task一共有4个delegate,只要设置了一个,就代表四个全部设置,有时候一些delegate不会被触发的原因在于这四种 delegate是针对不同的URLSession类型和URLSessionTask类型来进行响应的,也就是说不同的类型只会触发这些 delegate中的一部分,而不是触发所有的delegate。

举例说明如下:

  1. 触发NSURLSessionDataDelegate

     //使用函数dataTask来接收数据
    -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
    
    //则NSURLSession部分的代码如下
    NSURLSessionConfiguration* ephConfiguration=[NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession* session=[NSURLSession sessionWithConfiguration:ephConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURL* url=[NSURL URLWithString:@"http://www.example.com/external_links/01.png"];
    NSURLSessionDataTask* dataTask=[session dataTaskWithURL:url];
    [dataTask resume];
  2. 触发NSURLSessionDownloadDelegate
    //使用函数downloadTask来接受数据
    -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
    
    //则NSURLSession部分的代码如下
    NSURLSessionConfiguration* ephConfiguration=[NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession* session=[NSURLSession sessionWithConfiguration:ephConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURL* url=[NSURL URLWithString:@"http://www.example.com/external_links/01.png"];
    NSURLSessionDownloadTask* dataTask=[session downloadTaskWithURL:url];
    [dataTask resume];

    这两段代码的主要区别在于NSURLSessionTask的类型的不同,造成了不同的Delegate被触发.

[email protected]

 tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];

这么使用之前确实不太知道,如果是我,可能就直接赋值给数组了。那么@unionOfArrays.self又是什么意思呢?

  • @distinctUnionOfObjects 清楚重复值
  • unionOfObjects 保留重复值

34.相对路径(relative)

假如有一个基础路径NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"];我们暂时命名为baseURL.所谓 相对 肯定跟这个baseURL有关系

我们可以通过 NSURL +URLWithString:relativeToUL:这个方法来获取一个路径,至于怎么使用,我们通过一个例子来说明:

NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"];
[NSURL URLWithString:@"foo" relativeToURL:baseURL];                  // http://example.com/v1/foo
[NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL];          // http://example.com/v1/foo?bar=baz
[NSURL URLWithString:@"/foo" relativeToURL:baseURL];                 // http://example.com/foo
[NSURL URLWithString:@"foo/" relativeToURL:baseURL];                 // http://example.com/v1/foo
[NSURL URLWithString:@"/foo/" relativeToURL:baseURL];                // http://example.com/foo/
[NSURL URLWithString:@"http://example2.com/" relativeToURL:baseURL]; // http://example2.com/

至于 绝对路径相对路径 的定义,使用方法,优缺点,这里就不提了,大家可以自行了解。说一下为什么使用相对路径吧。

在真实开发中,一般都会有一个线上的服务器和一下测试服务器,当然也可能多个。在ios开发中切换开发环境有好几种方法

  • 通过target
  • 自定义一个字段HTTPURL,用它来控制路径
  • 通过相对路径来切换接口

35.dispatch_barrier_async

barrier 这个单词的意思是障碍,拦截的意思,也即是说 dispatch_barrier_async 一定是有拦截事件的作用。

看下边这段代码:

dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^(){
        NSLog(@"dispatch-1");
    });
    dispatch_async(concurrentQueue, ^(){
        NSLog(@"dispatch-2");
    });
    dispatch_barrier_async(concurrentQueue, ^(){
        NSLog(@"dispatch-barrier");
    });
    dispatch_async(concurrentQueue, ^(){
        NSLog(@"dispatch-3");
    });
    dispatch_async(concurrentQueue, ^(){
        NSLog(@"dispatch-4");
    });

打印结果:

2016-08-22 16:43:20.554 xxx[26805:271426] dispatch-1
2016-08-22 16:43:20.555 xxx[26805:271422] dispatch-2
2016-08-22 16:43:20.556 xxx[26805:271422] dispatch-barrier
2016-08-22 16:43:20.556 xxx[26805:271422] dispatch-3
2016-08-22 16:43:20.556 xxx[26805:271426] dispatch-4

这个说明了 dispatch_barrier_async 能够拦截它前边的异步事件,等待两个异步方法都完成之后,调用 dispatch_barrier_async

我们稍微改动一下:

dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-1");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-2");
});
dispatch_barrier_sync(concurrentQueue, ^(){
    NSLog(@"dispatch-barrier");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-3");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-4");
});

打印结果:

2016-08-22 16:43:20.554 xxx[26805:271426] dispatch-1
2016-08-22 16:43:20.555 xxx[26805:271422] dispatch-2
2016-08-22 16:43:20.556 xxx[26805:271422] dispatch-barrier
2016-08-22 16:43:20.556 xxx[26805:271422] dispatch-3
2016-08-22 16:43:20.556 xxx[26805:271426] dispatch-4

36.dispatch_sync() 和 dispatch_async()

大概说下 dispatch_sync() 和 dispatch_async() 这两个方法。

示例代码:

dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_sync(concurrentQueue, ^(){
        NSLog(@"2");
        [NSThread sleepForTimeInterval:5];
        NSLog(@"3");
    });
    NSLog(@"4");

输出为:

2016-08-25 11:50:51.601 xxxx[1353:102804] 1
2016-08-25 11:50:51.601 xxxx[1353:102804] 2
2016-08-25 11:50:56.603 xxxx[1353:102804] 3
2016-08-25 11:50:56.603 xxxx[1353:102804] 4

再看:

dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(concurrentQueue, ^(){
        NSLog(@"2");
        [NSThread sleepForTimeInterval:5];
        NSLog(@"3");
    });
    NSLog(@"4");

输出为:

2016-08-25 11:52:29.022 xxxx[1392:104246] 1
2016-08-25 11:52:29.023 xxxx[1392:104246] 4
2016-08-25 11:52:29.023 xxxx[1392:104284] 2
2016-08-25 11:52:34.029 xxxx[1392:104284] 3

通过上边的两个例子,我们可以总结出:

  • dispatch_sync(),同步添加操作。他是等待添加进队列里面的操作完成之后再继续执行
  • dispatch_async ,异步添加进任务队列,它不会做任何等待

37.NSURLCache

我们简单介绍下NSURLCache。NSURLCache 为您的应用的 URL 请求提供了内存中以及磁盘上的综合缓存机制。网络缓存减少了需要向服务器发送请求的次数,同时也提升了离线或在低速网络中使用应用的体验。当一个请求完成 下载来自服务器的回应,一个缓存的回应将在本地保存。下一次同一个请求再发起时,本地保存的回应就会马上返回,不需要连接服务器。NSURLCache 会 自动 且 透明 地返回回应。

为了好好利用 NSURLCache,你需要初始化并设置一个共享的 URL 缓存。在 iOS 中这项工作需要在 -application:didFinishLaunchingWithOptions: 完成,而 OS X 中是在 –applicationDidFinishLaunching::

示例代码:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                                       diskCapacity:20 * 1024 * 1024
                                                           diskPath:nil];
  [NSURLCache setSharedURLCache:URLCache];
}

NSURLRequest 有个 cachePolicy 属性,我们平时最常用的有四个属性:

  • NSURLRequestUseProtocolCachePolicy: 对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略
  • NSURLRequestReloadIgnoringLocalCacheData:数据需要从原始地址加载。不使用现有缓存。
  • NSURLRequestReturnCacheDataElseLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据
  • NSURLRequestReturnCacheDataDontLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。

[email protected]()锁

synchronized是一种锁,这种锁不管是在oc中还是java中用的都挺多的,而且这种锁锁得是对象。具体原理,可以看这篇文章后边的 参考 那一部分。
总结一下,锁一般用于多线程环境下对数据的操作中。在 AFNetworking 中我们见到了3种不同的锁,分别是:

  1. NSLock
  2. dispatch_semaphore_wait
  3. @synchronized

39.UIWebView+AFNetworking

UIWebView的这个分类是这几个分类中最让我惊讶的一个。让我真正认识到条条大路通罗马到底是什么意思。有时候人的思想确实会被固有的思维所束缚。这里只是用了UIWebView
的loadData:(NSData )data MIMEType:(NSString )MIMEType
textEncodingName:(NSString )textEncodingName baseURL:(NSURL )baseURL方法

你会发现使用这个分类配合UIWebView,所有的事情都变得很简单。

示例代码

- (void)loadRequest:(NSURLRequest *)request
           MIMEType:(NSString *)MIMEType
   textEncodingName:(NSString *)textEncodingName
           progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress
            success:(NSData * (^)(NSHTTPURLResponse *response, NSData *data))success
            failure:(void (^)(NSError *error))failure
{
    // 检查参数
    NSParameterAssert(request);

    // 如果正处于运行或者暂停装状态,就取消之前的任务task并设置为nil
    if (self.af_URLSessionTask.state == NSURLSessionTaskStateRunning || self.af_URLSessionTask.state == NSURLSessionTaskStateSuspended) {
        [self.af_URLSessionTask cancel];
    }
    self.af_URLSessionTask = nil;

    __weak __typeof(self)weakSelf = self;
    NSURLSessionDataTask *dataTask;
    dataTask = [self.sessionManager
            GET:request.URL.absoluteString
            parameters:nil
            progress:nil
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
                __strong __typeof(weakSelf) strongSelf = weakSelf;

                // 请求成功后,调用success block
                if (success) {
                    success((NSHTTPURLResponse *)task.response, responseObject);
                }
                // 显示数据
                [strongSelf loadData:responseObject MIMEType:MIMEType textEncodingName:textEncodingName baseURL:[task.currentRequest URL]];

                // 调用webViewDidFinishLoad
                if ([strongSelf.delegate respondsToSelector:@selector(webViewDidStartLoad:)]) {
                    [strongSelf.delegate webViewDidFinishLoad:strongSelf];
                }
            }
            failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
                if (failure) {
                    failure(error);
                }
            }];
    self.af_URLSessionTask = dataTask;

    // 设置progress,这个来自于self.sessionManager
    if (progress != nil) {
        *progress = [self.sessionManager downloadProgressForTask:dataTask];
    }

    // 开启任务
    [self.af_URLSessionTask resume];

    // 调用webViewDidStartLoad方法
    if ([self.delegate respondsToSelector:@selector(webViewDidStartLoad:)]) {
        [self.delegate webViewDidStartLoad:self];
    }
}

总结

AFNetworking 的源码解读到此就结束了。我曾经说要写一个网络框架,但随着对网络的更进一步的了解。一个好的框架不是随随便便写的。需要对使用者负责。因此我又找了几个目前比较流行的框架,对他们的思想研究研究。实属拿来主义。

时间: 2024-10-10 05:37:32

AFNetworking 3.0 源码解读 总结的相关文章

AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization

本篇是AFNetworking 3.0 源码解读的第四篇了. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager AFNetworking 3.0 源码解读(二)之 AFSecurityPolicy AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization 这次主要讲AFURLResponseSerialization(HTTP响应)这一个类的知识. 这是一个协议,只要遵守这个协议,就要实现N

AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager

做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一读这个框架的代码,我们的目标就是理解了它的思想之后,能够明白我们的请求是如何实现的,我们的代码哪里还需要进行改进,如果能够更进一步,我们能够总结出一套适合大部分应用的网络架构思想. 能够让一些人从中受益. 我们先来看看整个框架的文件系统,我们先不对每个文件的作用进行说明,在整个源码解读最后的一篇中我

AfNetworking 3.0源码解读

做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一读这个框架的代码,我们的目标就是理解了它的思想之后,能够明白我们的请求是如何实现的,我们的代码哪里还需要进行改进,如果能够更进一步,我们能够总结出一套适合大部分应用的网络架构思想. 能够让一些人从中受益. 我们先来看看整个框架的文件系统,我们先不对每个文件的作用进行说明,在整个源码解读最后的一篇中我

EventBus3.0源码解读

综述 EventBus是我们在项目当中最常用的开源框架之一.对于EventBus的使用方法也是非常的简单.然而EventBus内部的实现原理也不是很复杂.在这里便针对EventBus3.0的源码进行一下详细的分析.对于EventBus的详细使用可以参考EventBus3.0使用详解这篇文章. EventBus3.0源码解析 从下面这个图中可以看到,对于EventBus的工作过程很简单,用一句话概括为:事件被提交到EventBus后进行查找所有订阅该事件的方法并执行这些订阅方法. 获取EventB

AFNetworking 3.0源码阅读 - AFURLSessionManager

这次来说一下AFURLSessionManager 从头文件的英文注释可以看出AFURLSessionManager类创建并管理着NSURLSession对象,而NSURLSession又是基于NSURLSessionConfiguration的.同时该类也是AFHTTPSessionManager的父类,下一篇来讲. AFURLSessionManager实现了四个协议 1.NSURLSessionDelegate URLSession:didBecomeInvalidWithError: U

AFNetworking 3.0源码阅读 - AFURLRequestSerialization

AFURLRequestSerialization模块主要做的两样事情: 1.创建普通NSMUtableURLRequest请求对象2.创建multipart NSMutableURLRequest请求对象此外还有比如:处理查询的URL参数 也就是说这主要实现了请求报文构造器的功能 在AFURLRequestSerialization.h文件中声明了三个类来帮助开发者构造请求报文 1.AFHTTPRequestSerializer 2.AFJSONRequestSerializer 3.AFPr

Entity Framework 6.0 源码解读笔记(一)

internal static TResult ExecuteSingle<TResult>(IEnumerable<TResult> query, Expression queryRoot) { return GetElementFunction<TResult>(queryRoot)(query); //① } private static Func<IEnumerable<TResult>, TResult> GetElementFunct

Android事件总线(二)EventBus3.0源码解析

相关文章 Android事件总线(一)EventBus3.0用法全解析 前言 上一篇我们讲到了EventBus3.0的用法,这一篇我们来讲一下EventBus3.0的源码以及它的利与弊. 1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus getDefault() { if (defaultInstance == null) { synchroniz

vue源码解读预热-0

vueJS的源码解读 vue源码总共包含约一万行代码量(包括注释)特别感谢作者Evan You开放的源代码,访问地址为Github 代码整体介绍与函数介绍预览 代码模块分析 代码整体思路 总体的分析 从图片中可以看出的为采用IIFE(Immediately-Invoked Function Expression)立即执行的函数表达式的形式进行的代码的编写 常见的几种插件方式: (function(,){}(,))或(function(,){})(,)或!function(){}()等等,其中必有