终于写完了 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;
- 第一个参数接受一个signed long 的参数
- 第二个参数接受一个void * 类型的值,相当于oc的id类型,void * 可以指向任何类型的参数
- 第三个参数 是一个函数 目的是对info做retain操作
- 第四个参数是一个函数,目的是对info做release操作
- 第五个参数是 一个函数,根据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
- HTTP协议用于客户端和服务器端之间的通信
- 通过请求和相应的交换达成通信
- HTTP是不保存状态的协议
- HTTP自身不会对请求和相应之间的通信状态进行保存。什么意思呢?就是说,当有新的请求到来的时候,HTTP就会产生新的响应,对之前的请求和响应的保温信息不做任何存储。这也是为了快速的处理事务,保持良好的可伸展性而特意设计成这样的。
- 请求URI定位资源
- URI算是一个位置的索引,这样就能很方便的访问到互联网上的各种资源。
- 告知服务器意图的HTTP方法
- ①GET: 直接访问URI识别的资源,也就是说根据URI来获取资源。
- ②POST: 用来传输实体的主体。
- ③PUT: 用来传输文件。
- ④HEAD: 用来获取报文首部,和GET方法差不多,只是响应部分不会返回主体内容。
- ⑤DELETE: 删除文件,和PUT恰恰相反。按照请求的URI来删除指定位置的资源。
- ⑥OPTIONS: 询问支持的方法,用来查询针对请求URI指定的资源支持的方法。
- ⑦TRACE: 追踪路径,返回服务器端之前的请求通信环信息。
- ⑧CONNECT: 要求用隧道协议连接代理,要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。SSL(Secure Sockets Layer)和TLS(Transport Layer Security)就是把通信内容加密后进行隧道传输的。
- 管线化让服务器具备了相应多个请求的能力
- 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 。
实际中的加密算法比这个要复杂的多。
介绍两种常用加密方法:
-
共享密钥加密
-
公开密钥加密
共享密钥加密就是加密和解密通用一个密钥,也称为对称加密。优点是加密解密速度快,缺点是一旦密钥泄露,别人也能解密数据。
公开密钥加密恰恰能解决共享密钥加密的困难,过程是这样的:
-
①发文方使用对方的公开密钥进行加密
-
②接受方在使用自己的私有密钥进行解密
关于公开密钥,也就是非对称加密 可以看看这篇文章 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时,执行标记以后的代码
可以看出这个宏的用途是:当条件抛出异常时,执行标记以后的代码
这样就有很多使用场景了。当必须要对条件进行判断的时候,我们有下边几种方案了
-
#ifdef
这个是编译特性 -
if else
代码层次的判断 -
__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.对文件的操作
- NSParameterAssert() 用来判断参数是否为空,如果为空就抛出异常
- 使用isFileURL 判断一个URL是否为fileURL 使用checkResourceIsReachableAndReturnError判断路径能够到达
- 使用 [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error] 获取本地文件属性
- lastPathComponent ,https://www.baidu.com/abc.html 结果就是abc.html
- 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。
举例说明如下:
-
触发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];
-
触发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被触发.
33.@unionOfArrays
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:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。
38.@synchronized()锁
synchronized是一种锁,这种锁不管是在oc中还是java中用的都挺多的,而且这种锁锁得是对象。具体原理,可以看这篇文章后边的 参考 那一部分。
总结一下,锁一般用于多线程环境下对数据的操作中。在 AFNetworking 中我们见到了3种不同的锁,分别是:
NSLock
dispatch_semaphore_wait
@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 的源码解读到此就结束了。我曾经说要写一个网络框架,但随着对网络的更进一步的了解。一个好的框架不是随随便便写的。需要对使用者负责。因此我又找了几个目前比较流行的框架,对他们的思想研究研究。实属拿来主义。