终于写完了 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被触发.
[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种不同的锁,分别是:
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 的源码解读到此就结束了。我曾经说要写一个网络框架,但随着对网络的更进一步的了解。一个好的框架不是随随便便写的。需要对使用者负责。因此我又找了几个目前比较流行的框架,对他们的思想研究研究。实属拿来主义。