iOS开发缓存之NSURLCache

转载:https://blog.csdn.net/jason_chen13/article/details/51984823

两行代码就能完成80%的缓存需求

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    
    NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
    [NSURLCache setSharedURLCache:URLCache];
}

剩下20%的网络缓存需求

NSURLCache 不是帮我们做了硬盘缓存么?那我们为什么要自己用数据库做本地缓存啊。为啥不直接用NSURLCache 不是更方便?

真的有NSURLCache 不能满足的需求?

有人可能要问:

系统帮我们做的缓存,好处是自动,无需我们进行复杂的设置。坏处也恰恰是这个:不够灵活,不能自定义。只能指定一个缓存的总文件夹,不能分别指定每一个文件缓存的位置,更不能为每个文件创建一个文件夹,也不能指定文件夹的名称。缓存的对象也是固定的:只能是 GET请求的返回值

目录

  1. 当我们在谈论缓存的时候,我们在谈论什么?

  2. GET网络请求缓存

    1. 80%的缓存需求:两行代码就可满足

    2. 控制缓存的有效性

    3. 文件缓存:借助ETag或Last-Modified判断文件缓存是否有效

      1. Last-Modified
      2. ETag
      3. 总结
    4. 一般数据类型借助 Last-Modified 与 ETag 进行缓存

  3. 剩下20%的网络缓存需求–真的有NSURLCache 不能满足的需求?

由于微信、QQ、微博、这类的应用使用缓存很“重”,使一般的用户也对缓存也非常习惯。缓存已然成为必备。

缓存的目的的以空间换时间

这句话在动辄就是 300M、600M 的大应用上,得到了很好的诠释。但能有缓存意识的公司,还在少数。

只有你真正感受到痛的时候,你才会考虑使用缓存。

这个痛可能是:

服务器压力、客户端网络优化、用户体验等等。

当我们在谈论缓存的时候,我们在谈论什么?

我们今天将站在小白用户的角度,给缓存这个概念进行重新的定义。

缓存有不同的分类方法:

这里所指的缓存,是一个宽泛的概念。

我们这里主要按照功能进行划分:

enter image description here
- 第一种 第二种
目的 优化型缓存 功能型缓存
具体描述 出于优化考虑:服务器压力、用户体验、为用户剩流量等等。同时优化型缓存也有内存缓存和磁盘缓存之分。 App离线也能查看,出于功能考虑,属于存储范畴
常见概念 GET网络请求缓存、WEB缓存 离线存储
典型应用 微信首页的会话列表、微信头像、朋友圈、网易新闻新闻列表、 微信聊天记录、
Parse对应的类 PFCachedQueryController PFOfflineStore

重度使用缓存的App: 微信、微博、网易新闻、携程、去哪儿等等。

GET网络请求缓存

概述

首先要知道,POST请求不能被缓存,只有 GET 请求能被缓存。因为从数学的角度来讲,GET 的结果是 幂等 的,就好像字典里的 key 与 value 就是幂等的,而 POST 不 幂等 。缓存的思路就是将查询的参数组成的值作为 key ,对应结果作为value。从这个意义上说,一个文件的资源链接,也叫 GET 请求,下文也会这样看待。

80%的缓存需求:两行代码就可满足

设置缓存只需要三个步骤:

第一个步骤:请使用 GET 请求。

第二个步骤:

如果你已经使用 了 GET 请求,iOS 系统 SDK 已经帮你做好了缓存。你需要的仅仅是设置下内存缓存大小、磁盘缓存大小、以及缓存路径。甚至这两行代码不设置也是可以的,会有一个默认值。代码如下:

第三个步骤:没有第三步!

你只要设置了这两行代码,基本就可满足80%的缓存需求。AFNetworking 的作者 Mattt曾经说过:

无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存的功能,殊不知 NSURLCache 只要两行代码就能搞定且好上 100 倍。

(AFN 是不是在暗讽 SDWebImage 复杂又蹩脚的缓存机制??)

要注意

  • iOS 5.0开始,支持磁盘缓存,但仅支持 HTTP
  • iOS 6.0开始,支持 HTTPS 缓存

控制缓存的有效性

我们知道:

  • 只要是缓存,总会过期。

那么缓存的过期时间如何控制?

上文中的两行代码,已经给出了一个方法,指定超时时间。但这并也许不能满足我们的需求,如果我们对数据的一致性,时效性要求很高,即使1秒钟后数据更改了,客户端也必须展示更改后的数据。这种情况如何处理?

下面我们将对这种需求,进行解决方案的介绍。顺序是这样的:先从文件类型的缓存入手,引入两个概念。然后再谈下,一般数据类型比如 JSON 返回值的缓存处理。

文件缓存:借助ETag或Last-Modified判断文件缓存是否有效

Last-Modified

服务器的文件存贮,大多采用资源变动后就重新生成一个链接的做法。而且如果你的文件存储采用的是第三方的服务,比如七牛、青云等服务,则一定是如此。

这种做法虽然是推荐做法,但同时也不排除不同文件使用同一个链接。那么如果服务端的file更改了,本地已经有了缓存。如何更新缓存?

这种情况下需要借助 ETagLast-Modified 判断图片缓存是否有效。

Last-Modified 顾名思义,是资源最后修改的时间戳,往往与缓存时间进行对比来判断缓存是否过期。

在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:

客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过:

总结下来它的结构如下:

请求 HeaderValue 响应 HeaderValue
Last-Modified If-Modified-Since

如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

判断方法用伪代码表示:

之所以使用

而非使用:

原因是考虑到可能出现类似下面的情况:服务端可能对资源文件,废除其新版,回滚启用旧版本,此时的情况是:

但我们依然要更新本地缓存。

参考链接: What takes precedence: the ETag or Last-Modified HTTP header?

Demo10和 Demo11 给出了一个完整的校验步骤:

并给出了 NSURLConnectionNSURLSession 两个版本:

ETag

ETag 是什么?

HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。它是一个 hash 值,用作 Request 缓存请求头,每一个资源文件都对应一个唯一的 ETag 值,
服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,以下是服务器端返回的格式:

其中:

  • If-None-Match - 与响应头的 Etag 相对应,可以判断本地缓存数据是否发生变化

总结下来它的结构如下:

请求 HeaderValue 响应 HeaderValue
ETag If-None-Match

ETag 是的功能与 Last-Modified 类似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的 ETag 值,服务器会根据客户端与服务端的 ETag 值是否相等,来决定是否返回 data,同时总是返回对应的 HTTP 状态码。客户端通过 HTTP 状态码来决定是否使用缓存。比如:服务端与客户端的 ETag 值相等,则 HTTP 状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag 值不等,并且状态值会变为200,同时返回 data。

因为修改资源文件后该值会立即变更。这也决定了 ETag 在断点下载时非常有用。
比如 AFNetworking 在进行断点下载时,就是借助它来检验数据的。详见在 AFHTTPRequestOperation 类中的用法:

七牛等第三方文件存储商现在都已经支持ETag,Demo8和9 中给出的演示图片就是使用的七牛的服务,见:

下面使用一个 Demo 来进行演示用法,

NSURLConnection 搭配 ETag 为例,步骤如下:

  • 请求的缓存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地缓存
  • 服务器响应结束后,要记录 Etag,服务器内容和本地缓存对比是否变化的重要依据
  • 在发送请求时,设置 If-None-Match,并且传入 Etag
  • 连接结束后,要判断响应头的状态码,如果是 304,说明本地缓存内容没有发生变化

以下代码详见 Demo08 :

相应的 NSURLSession 搭配 ETag 的版本见 Demo09:

运行效果:

enter image description here

总结

在官方给出的文档中提出 ETag 是首选的方式,优于 Last-Modified 方式。因为 ETag 是基于 hash ,hash 的规则可以自己设置,而且是基于一致性,是“强校验”。 Last-Modified 是基于时间,是弱校验,弱在哪里?比如说:如果服务端的资源回滚客户端的 Last-Modified 反而会比服务端还要新。

虽然 ETag 优于 Last-Modified ,但并非所有服务端都会支持,而 Last-Modified 则一般都会有该字段。 大多数情况下需要与服务端进行协调支持 ETag ,如果协商无果就只能退而求其次。

Demo 也给出了一个不支持 ETag 的链接,基本随便找一张图片都行:

作为通用型的网络请求工具 AFNetworking 对该现状的处理方式是,判断服务端是否包含 ETag ,然后再进行相应处理。可见 AFHTTPRequestOperation 类中的用法,也就是上文中已经给出的断点下载的代码。

在回顾下思路:

  • 为资源分派 hash 值,然后对比服务端与本地缓存是否一致来决定是否需要更新缓存。

这种思路,在开发中经常使用,比如:处于安全考虑,登陆操作一般不会传输账号密码,而是传输对应的 hash 值– token ,这里的 token 就可以看做一个 file 资源,如果想让一个用户登陆超时时间是三天,只需要在服务端每隔三天更改下 token 值,客户端与服务端值不一致,然后服务端返回 token 过期的提示。

值得注意的一点是:

  • 如果借助了 Last-ModifiedETag,那么缓存策略则必须使用 NSURLRequestReloadIgnoringCacheData 策略,忽略缓存,每次都要向服务端进行校验。

如果 GET 中包含有版本号信息

众多的应用都会在 GET 请求后加上版本号:

这种情况下,
?v1.0?v2.0 两个不同版本,请求到的 Last-ModifiedETag 会如预期吗?

这完全取决于公司服务端同事的实现, Last-ModifiedETag 仅仅是一个协议,并没有统一的实现方法,而服务端的处理逻辑完全取决于需求。

你完全可以要求服务端同事,仅仅判断资源的异同,而忽略掉 ?v1.0?v2.0 两个版本的区别。

参考链接:if-modified-since vs if-none-match

一般数据类型借助 Last-ModifiedETag 进行缓存

以上的讨论是基于文件资源,那么对一般的网络请求是否也能应用?

控制缓存过期时间,无非两种:设置一个过期时间;校验缓存与服务端一致性,只在不一致时才更新。

一般情况下是不会对 api 层面做这种校验,只在有业务需求时才会考虑做,比如:

  1. 数据更新频率较低,“万不得已不会更新”—只在服务器有更新时才更新,以此来保证2G 等恶略网络环境下,有较好的体验。比如网易新闻栏目,但相反微博列表、新闻列表就不适合。
  2. 业务数据一致性要求高,数据更新后需要服务端立刻展示给用户。客户端显示的数据必须是服务端最新的数据
  3. 有离线展示需求,必须实现缓存策略,保证弱网情况下的数据展示的速度。但不考虑使用缓存过期时间来控制缓存的有效性。
  4. 尽量减少数据传输,节省用户流量

一些建议:

  1. 如果是 file 文件类型,用 Last-Modified 就够了。即使 ETag 是首选,但此时两者效果一致。九成以上的需求,效果都一致。

  2. 如果是一般的数据类型–基于查询的 get 请求,比如返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified 服务端支持起来就会困难一点。因为比如
    你做了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified 指的是10条中任意一个博客的更改。那么服务端需要在你发出请求后,遍历下10条数据,得到“10条中是否至少一个被修改了”。而且要保证每一条博客表数据都有一个类似于记录 Last-Modified 的字段,这显然不太现实。

    如果更新频率较高,比如最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,然后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。

参考链接: 《(慕课网)imooc iPhone3.3 接口数据缓存》

URL加载系统 UIWebView URL拦截

转自:http://prolove10.blog.163.com/blog/static/138411843201443111235812/

转自:http://www.cocoachina.com/ios/20150626/12161.html

iOS根本离不开网络——不论是从服务端读写数据、向系统分发计算任务,还是从云端加载图片、音频、视频等。

当应用程序面临处理问题的抉择时,通常会选择最高级别的框架来解决这个问题。所以如果给定的任务是通过http://,
https:// 或 ftp://进行通讯,那么与NSURLConnection 相关的方法就是最好的选择了。
苹果关于网络的类涵盖甚广,包括从URL加载、还存管理到认证与存储cookie等多个领域,完全可以满足现代Objective-C应用开发的需要:

URL加载
NSURLConnection
NSURLRequest NSMutableURLRequest
NSURLResponse NSHTTPURLResponse
缓存管理
NSURLCache
NSCacheURLRequest
NSCachedURLResponse
认证 & 证书
NSURLCredential
NSURLCredentialStorage
NSURLAuthenticationChallenge
NSURLProtectionSpace
Cookie存储
NSHTTPCookie
NSHTTPCookieStorage
协议支持
NSURLProtocol

虽然URL加载系统包含的内容众多,但代码的设计上却非常良好,没有把复杂的操作暴露出来,开发者只需要在用到的时候进行设置。
任何通过NSURLConnection 进行的请求都会被系统的其他部分所拦截,这也使得当可用时显式地从硬盘加载缓存成为了可能。
说到这里,我们就说说:NSURLProtocol。
NSURLProtocol
NSURLProtocol或许是URL加载系统中最功能强大但同时也是最晦涩的部分了。它是一个抽象类,
你可以通过子类化来定义新的或已经存在的URL加载行为。
听了我说了这些乱七八糟的如果你还没有抓狂,这里有一些关于希望加载请求时不用改变其他部分代码的例子,供你参考:
拦截图片加载请求,转为从本地文件加载
为了测试对HTTP返回内容进行mock和stub
对发出请求的header进行格式化
对发出的媒体请求进行签名
创建本地代理服务,用于数据变化时对URL请求的更改
故意制造畸形或非法返回数据来测试程序的鲁棒性
过滤请求和返回中的敏感信息
在既有协议基础上完成对 NSURLConnection的实现且与原逻辑不产生矛盾
再次强调 NSURLProtocol
核心思想最重要的一点:用了它,你不必改动应用在网络调用上的其他部分,就可以改变URL加载行为的全部细节。
或者这么说吧: NSURLProtocol 就是一个苹果允许的中间人攻击。
子类化NSURLProtocol
之前提到过 NSURLProtocol是一个抽象类,所以不能够直接使用必须被子类化之后才能使用。
让子类识别并控制请求
子类化 NSURLProtocol 的第一个任务就是告诉它要控制什么类型的网络请求。比如说如果你想要当本地有资源的时候请求直接使用本地资源文件,
那么相关的请求应该对应已有资源的文件名。
这部分逻辑定义在 +canInitWithRequest: 中,如果返回 YES,该请求就会被其控制。返回 NO 则直接跳入下一Protocol。
提供请求规范
如果你想要用特定的某个方式来修改一个请求,应该使用 +canonicalRequestForRequest: 方法。每一个subclass都应该依据某一个规范,
也就是说,一个protocol应该保证只有唯一的规范格式(虽然很多不同的请求可能是同一种规范格式)。
获取和设置请求的属性
NSURLProtocol提供方法允许你来添加、获取、删除一个request对象的任意metadata,而且不需要私有扩展或者方法欺骗(swizzle):
+propertyForKey:inRequest:
+setProperty:forKey:inRequest:
+removePropertyForKey:inRequest:
在操作protocol时对尚未赋予特定信息的 NSURLRequest
进行操作时,上述方法都是特别重要的。这些对于和其他方法之间的状态传递也非常有用。
加载请求
你的子类中最重要的方法就是 -startLoading 和 -stopLoading。不同的自定义子类在调用这两个方法是会传入不同的内容,
但共同点都是要围绕protocol客户端进行操作。
每个 NSURLProtocol 的子类实例都有一个 client 属性,该属性对URL加载系统进行相关操作。它不是NSURLConnection,
但看起来和一个实现了NSURLConnectionDelegate 协议的对象非常相似。
<NSURLProtocolClient>
-URLProtocol:cachedResponseIsValid:
-URLProtocol:didCancelAuthenticationChallenge:
-URLProtocol:didFailWithError:
-URLProtocol:didLoadData:
-URLProtocol:didReceiveAuthenticationChallenge:
-URLProtocol:didReceiveResponse:cacheStoragePolicy:
-URLProtocol:wasRedirectedToRequest:redirectResponse:
-URLProtocolDidFinishLoading:
在对-startLoading 和-stopLoading的实现中,你需要在恰当的时候让client调用每一个delegate方法。
简单来说就是连续调用那些方法,不过这是至关重要的。
向URL加载系统注册子类
最后,为了使用 NSURLProtocol 子类,需要向URL加载系统进行注册。
当请求被加载时,系统会向每一个注册过的protocol询问:“Hey你能控制这个请求吗?”第一个通过
+canInitWithRequest: 回答为 YES 的protocol就会控制该请求。URLprotocol会被以注册顺序的反序访问,所以当在
-application:didFinishLoadingWithOptions:方法中调用 [NSURLProtocol registerClass:[MyURLProtocol class]]; 时,
你自己写的protocol比其他内建的protocol拥有更高的优先级。
就像控制请求的URL加载系统一样, NSURLProtocol也一样的无比强大,可以通过各种灵活的方式使用。它作为一个相对晦涩难解的类,
我们挖掘出了它的潜力来让我们的代码更清爽健壮。

NSURLCache
NSURLCache 为您的应用的 URL 请求提供了内存中以及磁盘上的综合缓存机制。 作为基础类库 URL 加载系统 的一部分,
任何通过 NSURLConnection加载的请求都将被 NSURLCache 处理。
网络缓存减少了需要向服务器发送请求的次数,同时也提升了离线或在低速网络中使用应用的体验。
当一个请求完成下载来自服务器的回应,一个缓存的回应将在本地保存。下一次同一个请求再发起时,本地保存的回应就会马上返回,
不需要连接服务器。NSURLCache会 自动 且 透明 地返回回应。
初始化并设置一个共享的 URL 缓存
为了好好利用 NSURLCache,你需要初始化并设置一个共享的 URL 缓存。在 iOS 中这项工作需要在
-application:didFinishLaunchingWithOptions: 完成,而 Mac 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];
}

缓存策略由请求(客户端)和回应(服务端)分别指定。理解这些策略以及它们如何相互影响,是为您的应用程序找到最佳行为的关键。
NSURLRequestCachePolicy NSURLRequest 有个 cachePolicy属性,它根据以下常量指定了请求的缓存行为:
NSURLRequestUseProtocolCachePolicy:对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略。
NSURLRequestReloadIgnoringLocalCacheData:数据需要从原始地址加载。不使用现有缓存。
NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存。
NSURLRequestReturnCacheDataElseLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
NSURLRequestReturnCacheDataDontLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,
请求视为失败(即:“离线”模式)。
NSURLRequestReloadRevalidatingCacheData:从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
你并不会惊奇于这些值不被透彻理解且经常搞混淆。
NSURLRequestReloadIgnoringLocalAndRemoteCacheData 和NSURLRequestReloadRevalidatingCacheData

根本没有实现(Link to Radar)更加加深了混乱程度!
关于NSURLRequestCachePolicy,以下才是你实际 需要了解的东西:
UseProtocolCachePolicy 默认行为
ReloadIgnoringLocalCacheData 不使用缓存
ReloadIgnoringLocalAndRemoteCacheData 我是认真地,不使用任何缓存
ReturnCacheDataElseLoad 使用缓存(不管它是否过期),如果缓存中没有,那从网络加载吧
ReturnCacheDataDontLoad 离线模式:使用缓存(不管它是否过期),但是不从网络加载
ReloadRevalidatingCacheData 在使用前去服务器验证

HTTP 缓存语义
因为 NSURLConnection 被设计成支持多种协议——包括 FTP、HTTP、HTTPS——所以
URL加载系统用一种协议无关的方式指定缓存。为了本文的目的,缓存用术语 HTTP 语义来解释。
HTTP 请求和回应用
headers 来交换元数据,如字符编码、MIME 类型和缓存指令等。
Request Cache Headers
在默认情况下,NSURLRequest会用当前时间决定是否返回缓存的数据。为了更精确地控制,允许使用以下请求头:
If-Modified-Since - 这个请求头与 Last-Modified回应头相对应。把这个值设为同一终端最后一次请求时返回的 Last-Modified 字段的值。
If-None-Match - 这个请求头与与Etag 回应头相对应。使用同一终端最后一次请求的Etag 值。
Response Cache Headers
NSHTTPURLResponse 包含多个 HTTP 头,当然也包括以下指令来说明回应应当如何缓存:
Cache-Control - 这个头必须由服务器端指定以开启客户端的HTTP 缓存功能。这个头的值可能包含 max-age(缓存多久),
是公共 public 还是私有 private,或者不缓存 no-cache 等信息。详情请参阅 Cache-Control section of RFC 2616。
除了 Cache-Control 以外,服务器也可能发送一些附加的头用于根据需要有条件地请求(如上一节所提到的):
Last-Modified -
这个头的值表明所请求的资源上次修改的时间。例如,一个客户端请求最近照片的时间线,/photos/timeline,
Last-Modified的值可以是最近一张照片的拍摄时间。
Etag - 这是 “entity tag”的缩写,它是一个表示所请求资源的内容的标识符。在实践中,Etag
的值可以是类似于资源的 MD5 之类的东西。这对于那些动态生成的、可能没有明显的 Last-Modified
值的资源非常有用。
NSURLConnectionDelegate
一旦收到了服务器的回应,NSURLConnection 的代理就有机会在 -connection:willCacheResponse: 中指定缓存数据。
NSCachedURLResponse 是个包含NSURLResponse 以及它对应的缓存中的 NSData 的类。
在 -connection:willCacheResponse:中,cachedResponse 对象会根据 URL连接返回的结果自动创建。因为 NSCachedURLResponse没有可变部分,
为了改变 cachedResponse 中的值必须构造一个新的对象,把改变过的值传入
–initWithResponse:data:userInfo:storagePolicy:
例如:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy];
    NSMutableData *mutableData = [[cachedResponse data] mutableCopy];
    NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowedInMemoryOnly;
   // ...
    return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response]
                                                              data:mutableData
                                                     userInfo:mutableUserInfo
                                                 storagePolicy:storagePolicy];
}

如果 -connection:willCacheResponse: 返回 nil,回应将不会缓存。

  • (NSCachedURLResponse *)connection:(NSURLConnection *)connection
    willCacheResponse:(NSCachedURLResponse *)cachedResponse
    {
    return nil;
    }
    如果不实现此方法,NSURLConnection 就简单地使用本来要传入 -connection:willCacheResponse:的那个缓存对象,
    所以除非你需要改变一些值或者阻止缓存,否则这个代理方法不必实现。
    注意事项
    正如它那个毫无关系但是名字相近的小伙伴 NSCache 一样,NSURLCache 也是有一些特别的。在 iOS 5,磁盘缓存开始支持,
    但仅支持 HTTP,非 HTTPS(iOS 6中增加了此支持)。Peter Steinberger关于这个主题写了一篇优秀的文章
    (http://petersteinberger.com/blog/2012/nsurlcache-uses-a-disk-cache-as-of-ios5/),
    在深入研究内部细节后实现他自己的 NSURLCache 子类(https://github.com/steipete/SDURLCache)。
    Daniel Pasco 在 Black Pixel 上的另一篇文章
    http://blackpixel.com/blog/2012/05/caching-and-nsurlconnection.html
    描述了一些与服务器通信时不设置缓存头的意外的默认行为。
    NSURLCache 提醒着我们熟悉我们正在操作的系统是多么地重要。开发 iOS 或 Mac OS X 程序时,
    这些系统中的重中之重,非 URL Loading System莫属。
    无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存的功能,殊不知NSURLCache只要两行代码就能搞定且好上100倍。
    甚至更多开发者根本不知道网络缓存的好处,也从未尝试过,导致他们的应用向服务器作了无数不必要的网络请求。
    所以如果你想看到世界的变化,你想确保你有程序总以正确的方式开启,在
    -application:didFinishLaunchingWithOptions:设置一个共享的 NSURLCache 吧。

本文译者:candeladiao,原文:URL filtering for UIWebView on the iPhone
说明:译者在做app开发时,因为页面的javascript文件比较大导致加载速度很慢,所以想把javascript文件打包在app里,当UIWebView需要加载该脚本时就从app本地读取,但UIWebView并不支持加载本地资源。最后从下文中找到了解决方法,第一次翻译,难免有误,大家多多指教。

iCab Mobile(一款iOS平台的网页浏览器)要实现一个拦截管理器来过滤页面上的广告及其他东西。它有一个简单的基于URL过滤规则的列表(通常由用户维护),当页面包含的资源(图片、js以及css等),文件的URL存在于规则列表中时,资源就不会被加载。

但看一下UIWebView类的API,会发现我们没有办法知道UIWebView正在加载什么资源,更糟的是,当你希望过滤掉某些资源文件的时候,没有方法可以强制UIWebView不去加载这些文件,

拦截器看起来貌似没有可能实现。

当然还是有解决方案的,否则这篇文件就没什么卵用。

正如上面所说,实现拦截器不能靠UIWebView,因为UIWebView没有提供任何有用的API。

对UIWebView的所有请求,要找到一个能中断所有HTTP 请求的切入点,我们需要先了解一下Cocoa的URL Loading System,因为UIWebView是使用URL Loading System从web端取数据的。我们需要的切入点NSURLCache类就是URL Loading System的一部分。虽然目前iOS系统不会在磁盘上缓存任何数据(后面的iOS系统版本或许会有不同),因此在UIWebView开始加载前,NSURLCache管理的缓存数据通常为空,但UIWebView仍然会检测所请求资源文件是否存在于缓存。所以我们需要做的只是继承NSURLCache并重载其方法:

`- (NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request`

 |

UIWebView请求所有资源时都会调用这个方法。因为我们只需要在这个方法里判断请求的URL是否是我们想拦截的。如果是则创建一个没有内容的假response,否则只需调用super方法即可。

如下是实现细节:

1.继承NSURLCache:

FilteredWebCache.h:

`@interface FilteredWebCache : NSURLCache`

`{ `

`} `

`@end`

子类的主要代码

FilteredWebCache.m:

`#import "FilteredWebCache.h"`

`#import "FilterManager.h"`

`@implementation FilteredWebCache`

`- (NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request`

`{`

`NSURL *url = [request URL];`

`BOOL blockURL = [[FilterMgr sharedFilterMgr] shouldBlockURL:url];`

`if` `(blockURL) {`

`NSURLResponse *response =`

`[[NSURLResponse alloc] initWithURL:url`

`MIMEType:@``"text/plain"`

`expectedContentLength:1`

`textEncodingName:nil];`

`NSCachedURLResponse *cachedResponse =`

`[[NSCachedURLResponse alloc] initWithResponse:response`

`data:[NSData dataWithBytes:``" "` `length:1]];`

`[``super` `storeCachedResponse:cachedResponse forRequest:request];`

`[cachedResponse release];`

`[response release];`

`}`

`return` `[``super` `cachedResponseForRequest:request];`

`}`

`@end`

首先判断URL是否需拦截(判断通过FilterManager类实现,类实现在此不列出)。如果需要,创建一个无内容的响应对象并把它存在cache中。有人可能会认为只需要返回假的响应对象就够了,没必要缓存它。但这样会因响应对象被系统释放而导致app crash。不知道为何为会这样,可能是iOS的bug(Mac OS X 10.5.x也存在同样问题,而10.4.x及更早的系统上没有问题),也可能是URL Loading System内部类之间的依赖所致。所以我们先缓存响应对象。确保所有响应都是真实存在于缓存中,这也iOS希望的,最重要的是不会crash.

更新:因为假的响应是以大于0的大小来初始化的,看起来结缓存它也是必要的。

2.创建新的缓存:

接下来需要创建一个新的缓存并告诉iOS系统使用新的缓存代替默认的,这样当URL Loading System检测资源缓存时才会调用上面的代码。这要在任意UIWebView开始加载页面前做,显然应该放在app启动的时候:


`NSString *path = ...``// the path to the cache file`

`NSUInteger discCapacity = 10*1024*1024;`

`NSUInteger memoryCapacity = 512*1024;`

`FilteredWebCache *cache =`

`[[FilteredWebCache alloc] initWithMemoryCapacity: memoryCapacity`

`diskCapacity: discCapacity diskPath:path];`

`[NSURLCache setSharedURLCache:cache];`

`[cache release];`

这里需要提供一个缓存存储路径。缓存文件由NSURLCache对象自动生成,我们无需事先创建文件,但要定义缓存文件所存位置(必须是应用程序“沙盒”内,如“tmp”目录或是“Document”目录)

这就是实现UIWebView基于URL进行请求过滤的所有内容,看起来其实并不复杂

注:如果过滤规则在app运行过程中会改变,你需要从缓存中删除假的响应。NSURLCache提供了删除方法,所以这不是问题。如果过滤规则不会改变,则无需关心

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容