当前位置: 首页 > 编程日记 > 正文

iOS架构设计-URL缓存(下)

本文转载自崔江涛(KenshinCui)


缓存设计


从前面对于URL Loading System的分析可以看出利用NSURLProtocol或者NSURLCache都可以做客户端缓存,但是NSURLProtocol更多的用于拦截处理,而且如果使用它来做缓存的话需要自己发起请求。而选择URLSession配合NSURLCache的话,则对于接口调用方有更多灵活的控制,而且默认情况下NSURLCache就有缓存,我们只要操作缓存响应的Cache headers即可,因此后者作为我们优先考虑的设计方案。鉴于本文代码使用Swift编写,因此结合目前Swift中流行的网络库Alamofire实现一种相对简单的缓存方案。


根据前面的思路,最早还是想从URLSessionDataDelegate的缓存设置方法入手,而且Alamofire确实对于每个URLSessionDataTask都留有缓存代理方法的回调入口,但查看源码发现这个入口dataTaskWillCacheResponse并未对外开发,而如果直接在SessionDelegate的回调入口dataTaskWillCacheResponseWithCompletion上进行回调又无法控制每个请求的缓存情况(NSURLSession是多个请求共用的)。当然如果沿着这个思路可以再扩展一个DataTaskDelegate对象以暴漏缓存入口,但是这么一来必须实现URLSessionDataDelegate,而且要想办法Swizzle NSURLSession的缓存代理(或者继承SessionDelegate切换代理),在代理中根据不同的NSURLDataTask进行缓存处理,整个过程对于调用方并不是太友好。


另一个思路就是等Response请求结束后获取缓存的响应CachedURLResponse并且修改(事实上只要是同一个NSURLRequest存储进去默认会更新原有缓存),而且NSURLCache本身就是有内存缓存的,过程并不会太耗时。当然这个方案最重要的是得保证响应完成,所以这里通过Alamofire链式调用使用response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler重新请求以保证及时掌握回调时机。主要的代码片段如下:


public func cache(maxAge:Int,isPrivate:Bool = false,ignoreServer:Bool = true)

    -> Self

{

    var useServerButRefresh = false

    if let newRequest = self.request {

        if !ignoreServer {

            if newRequest.allHTTPHeaderFields?[AlamofireURLCache.refreshCacheKey] == AlamofireURLCache.RefreshCacheValue.refreshCache.rawValue {

                useServerButRefresh = true

            }

        }

        

        if newRequest.allHTTPHeaderFields?[AlamofireURLCache.refreshCacheKey] != AlamofireURLCache.RefreshCacheValue.refreshCache.rawValue {

            if let urlCache = self.session.configuration.urlCache {

                if let value = (urlCache.cachedResponse(for: newRequest)?.response as? HTTPURLResponse)?.allHeaderFields[AlamofireURLCache.refreshCacheKey] as? String {

                    if value == AlamofireURLCache.RefreshCacheValue.useCache.rawValue {

                        return self

                    }

                }

            }

        }

        

    }

    

    return response { [unowned self](defaultResponse) in

        

        if defaultResponse.request?.httpMethod != "GET" {

            debugPrint("Non-GET requests do not support caching!")

            return

        }

        


        if defaultResponse.error != nil {

            debugPrint(defaultResponse.error!.localizedDescription)

            return

        }


        if let httpResponse = defaultResponse.response {

            guard let newRequest = defaultResponse.request else { return }

            guard let newData = defaultResponse.data else { return }

            guard let newURL = httpResponse.url else { return }

            guard let urlCache = self.session.configuration.urlCache else { return }

            guard let newHeaders = (httpResponse.allHeaderFields as NSDictionary).mutableCopy() as? NSMutableDictionary else { return }

            

            if AlamofireURLCache.isCanUseCacheControl {

                if httpResponse.allHeaderFields["Cache-Control"] == nil || httpResponse.allHeaderFields.keys.contains("no-cache") || httpResponse.allHeaderFields.keys.contains("no-store") || ignoreServer || useServerButRefresh {

                    DataRequest.addCacheControlHeaderField(headers: newHeaders, maxAge: maxAge, isPrivate: isPrivate)

                } else {

                    return

                }

            } else {

                if httpResponse.allHeaderFields["Expires"] == nil || ignoreServer || useServerButRefresh {

                    DataRequest.addExpiresHeaderField(headers: newHeaders, maxAge: maxAge)

                    if ignoreServer && httpResponse.allHeaderFields["Pragma"] != nil {

                        newHeaders["Pragma"] = "cache"

                    }

                } else {

                    return

                }

            }

            newHeaders[AlamofireURLCache.refreshCacheKey] = AlamofireURLCache.RefreshCacheValue.useCache.rawValue

            if let newResponse = HTTPURLResponse(url: newURL, statusCode: httpResponse.statusCode, httpVersion: AlamofireURLCache.HTTPVersion, headerFields: newHeaders as? [String : String]) {

                

                let newCacheResponse = CachedURLResponse(response: newResponse, data: newData, userInfo: ["framework":AlamofireURLCache.frameworkName], storagePolicy: URLCache.StoragePolicy.allowed)

                

                urlCache.storeCachedResponse(newCacheResponse, for: newRequest)

            }

        }

        

    }

    

}


要完成整个缓存处理自然还包括缓存刷新、缓存清理等操作,关于缓存清理本身NSURLCache是提供了remove方法的,不过缓存清理并不及时,调用并不会立即生效,具体参见NSURLCache does not clear stored responses in iOS8。因此,这里借助了上面提到的Cache-Control进行缓存过期控制,一方面可以快速清理缓存,另一方面缓存控制可以更加精确。


AlamofireURLCache




为了更好的配合Alamofire使用,此代码以AlamofireURLCache类库形式在github开源,所有接口API尽量和原有接口保持一致,便于对Alamofire二次封装。此外还提供了手动清理缓存、出错之后自动清理缓存、覆盖服务器端缓存配置等方便的功能,可以满足多数情况下缓存需求细节。


AlamofireURLCache在request方法添加了refreshCache参数用于缓存刷新,设为false或者不提供此参数则不会刷新缓存,只有等到上次缓存数据过了有效期才会再次发起请求。


Alamofire.request("https://myapi.applinzi.com/url-cache/no-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

    if response.value != nil {

        self.textView.text = (response.value as! [String:Any]).debugDescription

    } else {

        self.textView.text = "Error!"

    }

    

}).cache(maxAge: 10)


服务器端缓存headers设置并不都是最优选择,某些情况下客户端必须自行控制缓存策略,此时可以使用AlamofireURLCache的ignoreServer参数忽略服务器端配置,通过maxAge参数自行控制缓存时长。


Alamofire.request("https://myapi.applinzi.com/url-cache/default-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

    if response.value != nil {

        self.textView.text = (response.value as! [String:Any]).debugDescription

    } else {

        self.textView.text = "Error!"

    }

    

}).cache(maxAge: 10,isPrivate: false,ignoreServer: true)


另外,有些情况下未必需要刷新缓存而是要清空缓存保证下次访问时再使用最新数据,此时就需要使用AlamofireURLCache提供的缓存清理API来完成。需要特别说明的是,对于请求出错、序列化出错等情况如果调用了cache(maxAge)方法进行缓存后,那么下次请求会使用错误的缓存数据,需要开发人员根据返回情况自行调用API清理缓存。但更好的选择是使用AlamofireURLCache提供的autoClearCache参数来自动处理此种情况,所以任何时候都推荐将autoClearCache参数设为true以保证不会缓存出错数据。


Alamofire.clearCache(dataRequest: dataRequest) // clear cache by DataRequest

Alamofire.clearCache(request: urlRequest) // clear cache by URLRequest


// ignore data cache when request error

Alamofire.request("https://myapi.applinzi.com/url-cache/no-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

    if response.value != nil {

        self.textView.text = (response.value as! [String:Any]).debugDescription

    } else {

        self.textView.text = "Error!"

    }

    

},autoClearCache:true).cache(maxAge: 10)


如果阅读本文让你有所收获,欢迎推荐点赞,最后再次附上代码下载!


代码下载

相关文章:

pyqt4+chatterbot实现简单聊天机器人程序

环境window10python3 代码:github.com/xie233/text_mining转载于:https://www.cnblogs.com/who-a/p/5641738.html

OpenGL进阶(十三) - GLSL光照(Lighting)

提要 在上一篇文章中,我们介绍了简单的Shading,同时提出了一个光照模型,模拟了一个点光源,但是,关于光的故事还没有结束... 今天要学习的是方向光源(Directional Light),聚光灯&…

Web漏洞扫描(二:Windows server2008 R2操作系统(部署dvwa))

在Windows server 2008 R2系统中部署dvwa; 1、在Windows server 2008虚拟机中配置IIS; 1.1、打开服务器管理器,角色,添加角色,然后点击下一步; 1.2、选择安装“Web 服务器(IIS)”,…

[iOS]调和 pop 手势导致 AVPlayer 播放卡顿

作者 NewPan 关注 2017.07.15 14:24* 字数 3110 阅读 749评论 8喜欢 17声明:我为这个框架写了四篇文章: 第一篇:[iOS]UINavigationController全屏pop之为每个控制器自定义UINavigationBar 第二篇:[iOS]UINavigationController全屏…

Cocos2d-x学习笔记(三十)之 游戏存档

游戏中的存档功能可以保证玩家在游戏过程中有足够的延续性,这点在单机游戏开发中尤为重要。Cocos2D-x中支持的游戏存档类CCUserDefault可以作为一个轻量化的数据库来使用。它支持存储的数据类型包括bool(布尔型)、int(整型&#x…

github删除文件夹

git rm -rf dirgit add .git commit -m remove dirgit push origin master //dir是要删除的文件夹路径转载于:https://www.cnblogs.com/xulei1992/p/5650399.html

Web漏洞扫描(三:Burp Suite的基本操作)

任务二、Burp Suite基础Proxy功能; 2.1、在Kali虚拟机中打开Burp Suite工具并设置,打开“Proxy”选项卡,选中“Options”子选项卡,单机“Add”按钮,增加一个监听代理,设置为127.0.0.1:8080; 2.…

UITableView嵌套WKWebView的那些坑

最近项目中遇到了一个需求,TableView中需要嵌套Web页面,我的解决办法是在系统的UITableViewCell中添加WKWebView。开发的过程中,遇到了些坑,写出来分享一下。 1.首先说一下WKWebView的代理方法中,页面加载完成后会走的…

深入了解line-height

1.定义 行高:两行文字baseline(基线)之间的距离 示意图: 2.为何line-height可以让单行文本垂直居中 其实并没有垂直居中,除非将font-size:0; 3.line-height的高度原理(可以先看看行内盒子的原理) * 行内元素的高度是lin…

实现一个简单的投票功能

实现一个简单的投票功能 最近项目中需要用到一个投票功能,当时觉得简单,向都没想就动手开始做,没想到走了不少弯路。 后来才发现,是想的太过简单了。来看看改进后的功能。 第一步:数据库设计 两个表:一个主…

Web漏洞扫描(四:知识点及错误总结)

WVS软件: WVS(Web Vulnerability Scanner)是一个自动化的Web应用程序安全测试工具,它可以扫描任何可通过Web浏览器访问的和遵循HTTP/HTTPS规则的Web站点和Web应用程序。适用于任何中小型和大型企业的内联网、外延网和面向客户、雇员、厂商和其它人员的W…

【VS开发】【智能语音处理】Windows下麦克风语音采集

简介 这是我很早以前的大学毕业设计,忽然间找到贴出来以纪念自己的纯真年代...但是因为CSDN不给面子所以导致短短的一篇文章贴了足足7次..他老提时说文章超过了64K,老大,拜托,那是算上了里面的图片大小吧...:-( 本文简单介绍了声卡的工作原理 , 录音的原理以及数字音频的基本知…

iOS音频——AudioToolbox

一、前言 二、音频文件Audio File Services 三、音频文件转换Extended Audio File Services 四、音频流Audio File Stream Services 五、音频队列Audio Queue Services 一、前言 AudioToolbox提供的API主要是C 使用起来相对晦涩,针对本文提供了简单的代码示例减小学…

【WA】九度OJ题目1435:迷瘴

题目描述: 通过悬崖的yifenfei,又面临着幽谷的考验——幽谷周围瘴气弥漫,静的可怕,隐约可见地上堆满了骷髅。由于此处长年不见天日,导致空气中布满了毒素,一旦吸入体内,便会全身溃烂而死。幸好y…

云端应用SQL注入攻击

实验目的及要求: 完成VMware Workstations14平台安装,会应用相关操作;完成Windows server2008R2操作系统及Kali Linux操作系统的安装;掌握SQLmap攻击工具的使用;使用SQLmap对目标站点进行渗透攻击; 实验环…

SPOJ375(树链剖分)

题目:Query on a tree 题意:给定一棵树,告诉了每条边的权值,然后给出两种操作: (1)把第i条边的权值改为val (2)询问a,b路径上权值最大的边 分析:本题与HDU3966差不多,区别就是&#…

简单的python服务器程序

一个接受telnet输入的服务器端小程序 #!/usr/local/bin/python3.5 #coding:utf-8 import sockethost port 51423s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(1)print(S…

iOS:一句代码实现文本输入的限制

前言 实际开发中,往往需要处理UITextView、UITextField输入的限制。比如输入必须是价格格式(一个小数点、小数点后面最多两位);输入最大长度限制;对输入内容的实时回调。处理这些的时候,我们通常需要做一些…

Linux操作系统(一:基本操作)

1、创建一个以自己姓名拼音简写一致的用户名( useradd -d /home/abc abc) 2、在linux中使用打印命令,在命令行中输入“当前用户名Hello world !” 3、显示现在天年月日,显示后一天的日期,显示上一月日期,显…

Core Text 学习笔记-基础

前言 最近在学习YYKit框架,看到关于CoreText相关的知识的时候感到非常吃力,于是乎就恶补了一下Core Text相关的基础知识。 Glyphs(字形) 字符的图形形式, 则是文字中字母 (character) 的视觉表现。(字形&am…

虚拟机下CentOS 6.5配置IP地址的三种方法

实验软件环境:虚拟机Vmware Workstation10.0 、CentOS 6.5 32位 1、自动获取IP地址 虚拟机使用桥接模式,相当于连接到物理机的网络里,物理机网络有DHCP服务器自动分配IP地址。 dhclient 自动获取ip地址命令 ifconfig 查询系统里网卡信息&…

GIS 相关知识扫盲

1、什么是GIS GIS:地理信息系统,它是一种特定的十分重要的空间信息系统。它是在计算机硬、软件系统支持下,对整个或部分地球表层(包括大气层)空间中的有关地理分布数据进行采集、储存、管理、运算、分析、显示和描述的技术系统。2…

Linux操作系统(二:shell脚本)

练习一:编写shell脚本,计算1-100的和; 练习二:将一目录下所有的文件的扩展名改为bak 练习三:写一个脚本,统计。/etc/ 目录下共有多少个目录文件 练习四:写一个脚本,依次向/etc/passw…

用OpenGLES实现yuv420p视频播放界面

背景 例子TFLive这个项目里,是我按着ijkPlayer写的直播播放器,要运行需要编译ffmpeg的库,网盘里存了一份, 提取码:vjce。OpenGL ES播放相关的在在OpenGLES的文件夹里。 learnOpenGL学到会使用纹理就可以了。 播放视频,就是把画面一…

C++类的静态成员详细讲解

在C中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。 静态成员的定义或声明要…

jenkins2 multibranch

通过multibranch类型的pipeline job使得对于多个branch的支持更加简单。只需要创建一个multibranch job,jenkins将自动地为所有的branch创建job。 文章来自:http://www.ciandcd.com文中的代码来自可以从github下载: https://github.com/ciand…

Nagios的安装和基本配置(一:知识点总结及环境准备)

实验目的及要求 掌握Nagios监控的基本使用;掌握Nagios监控服务的搭建和配置; 实验环境: 1、满足实验要求的PC端; Host-name OS IP sofaware Nagios-server Centos7 192.168.1.119 Apache,php,Nagios,Nagios-plguins Nag…

浅谈Android四大组件之Service

一:Service简介 Android开发中,当需要创建在后台运行的程序的时候,就要使用到Service。 1:Service(服务)是一个没有用户界面的在后台运行执行耗时操作的应用组件。其他应用组件能够启动Service,并且当用户切…

使用 fastlane 实现 iOS 持续集成(二)

本文接上篇文章主要说下怎样使用 fastlane 上传到fir和蒲公英,下面先介绍下 plugin 命令。 plugin命令介绍: 列出所有可用插件 fastlane search_plugins 搜索指定名称的插件: fastlane search_plugins [query] 添加插件: fastlane add_plugin [name] 安装插件: fast…

Nagios的安装和基本配置(二:Nagios-Server的安装)

任务二、Nagios-server的安装 2.1、创建Nagios用户和组 注: #useradd Nagios -s /bin/nologin #groundadd nagcmd #usermod -a -G nagcmd Nagios #usermod -a G nagcmd apache 2.2、安装Nagios 2.2.1、上传软件包至操作系统中; 2.2.2、解压软件并…