本文只做本地緩存,只用文件緩存

知識儲備
工欲善其事必先利其器,要想封裝一個(gè)好用的本地緩存庫,首先要對本地文件目錄有個(gè)比較清晰的認(rèn)識
- 沙盒主路徑:是程序運(yùn)行期間系統(tǒng)會生成一個(gè)專屬的沙盒路徑,應(yīng)用程序在使用期間非代碼的文件都存儲在當(dāng)前的文件夾路徑里面
let homePath = NSHomeDirectory()
print(homePath)
把控制臺輸出的地址拷貝,F(xiàn)inder下前往后可以看到目錄結(jié)構(gòu)

- Documents:用來存儲永久性的數(shù)據(jù)的文件 程序運(yùn)行時(shí)所需要的必要的文件都存儲在這里(數(shù)據(jù)庫)itunes會自動備份這里面的文件
//Document 主目錄
let documentPaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentationDirectory, NSSearchPathDomainMask.AllDomainsMask, true)
let path = documentPaths.first
- Library:用于保存程序運(yùn)行期間生成的文件
//Libaray目錄
let libPaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.LibraryDirectory, NSSearchPathDomainMask.AllDomainsMask, true)
let libPath = libPaths.first
- Caches:文件夾用于保存程序運(yùn)行期間產(chǎn)生的緩存文件
//Cache目錄
let cachePaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomainMask.AllDomainsMask, true)
let cachePath = cachePaths.first
-
Preferences:主要是保存一些用戶偏好設(shè)置的信息,一般情況下,我們不直接打開這個(gè)文件夾 而是通過
NSUserDefaults進(jìn)行偏好設(shè)置的存儲
NSUserDefaults的操作非常簡單,我對它也小小的封裝了一下,寫了幾個(gè)全局方法
func setDefault(key:String,value:AnyObject?){
if value == nil{
NSUserDefaults.standardUserDefaults().removeObjectForKey(key)
}else{
NSUserDefaults.standardUserDefaults().setObject(value, forKey: key)
NSUserDefaults.standardUserDefaults().synchronize() //同步
}
}
func removeUserDefault(key:String?){
if key != nil{
NSUserDefaults.standardUserDefaults().removeObjectForKey(key!)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
func getDefault(key:String) ->AnyObject?{
return NSUserDefaults.standardUserDefaults().valueForKey(key)
}
- tmp:臨時(shí)文件夾---程序運(yùn)行期間產(chǎn)生的臨時(shí)歲騙會保存在這個(gè)文件夾中 通常文件下載完之后或者程序退出的灰自動清空此文件夾itunes不會備份這里的數(shù)據(jù)。
tips: 由于系統(tǒng)會清空此文件夾所以下載或者其他臨時(shí)文件若需要持久化請及時(shí)移走
本地緩存庫
有了這些對文件存儲的預(yù)備知識,下面來開發(fā)我們的本地緩存
首先明確我們?yōu)槭裁匆鲞@件事情,主要是為了提高用戶體驗(yàn)
比如:簡書中用戶瀏覽了主頁,點(diǎn)進(jìn)了各種詳情頁看了,然后坐在地鐵上,想瀏覽自己瀏覽過的頁面時(shí),由于連不上網(wǎng)絡(luò)或者網(wǎng)絡(luò)很差。這時(shí)候如果你把用戶瀏覽的記錄都儲存在本地,用戶體驗(yàn)就會非常舒服。而且用戶瀏覽過的頁面,再次瀏覽的時(shí)候直接從本地去,有更新再從服務(wù)器取,這樣省去了用戶的重復(fù)等待。
我現(xiàn)在做的app就有這樣的需求,每個(gè)頁面都要做本地存儲,因?yàn)樽隽肆奶?,聊天中的圖片和語音也要做本地存儲。
針對這個(gè)需求,我們來寫一個(gè)好用的能適配這些情況的庫。
因?yàn)槭蔷彺?,我選擇了存在Cache文件夾中。因?yàn)樾枰诸惞芾恚晕視贑ache地下建幾個(gè)不同的文件夾,頁面緩存的其實(shí)是對象類型。用一個(gè)文件夾管理,圖片和語音也分別用一個(gè),所以這里用了枚舉來管理這幾種類型。以后添加類型也方便
//會在cache下創(chuàng)建目錄管理
enum CacheFor:String{
case Object = "zzObject" //頁面對象緩存 (緩存的對象)
case Image = "zzImage" //圖片緩存 (緩存NSData)
case Voice = "zzVoice" //語音緩存 (緩存NSData)
}
文件管理,需要用到NSFileManager對象,這里聲明一個(gè)文件管理的私有變量。文件的寫入放在一個(gè)串行的線程中異步執(zhí)行,需要一個(gè)隊(duì)列對象。 當(dāng)然還需要一個(gè)路徑對象。為了避免文件名或者隊(duì)列名的重復(fù),都聲明了一個(gè)前綴,還有一個(gè)默認(rèn)的緩存名。最后聲明一個(gè)私有的緩存類型的變量
public class ZZDiskCache {
private let defaultCacheName = "zz_default"
private let cachePrex = "com.zz.zzdisk.cache."
private let ioQueueName = "com.zz.zzdisk.cache.ioQueue."
private var fileManager: NSFileManager!
private let ioQueue: dispatch_queue_t
var diskCachePath:String
private var storeType:CacheFor
}
然后就是初始化這些變量了,因?yàn)槲覀円凑疹愋统跏蓟?,所以初始化的時(shí)候需要傳入對應(yīng)的類型。
init(type:CacheFor) {
self.storeType = type
let cacheName = cachePrex+type.rawValue
ioQueue = dispatch_queue_create(ioQueueName+type.rawValue, DISPATCH_QUEUE_SERIAL)
//獲取緩存目錄
let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true)
//緩存目錄下創(chuàng)建一個(gè)子目錄
diskCachePath = (paths.first! as NSString).stringByAppendingPathComponent(cacheName)
dispatch_sync(ioQueue) { () -> Void in
self.fileManager = NSFileManager()
//創(chuàng)建子目錄對應(yīng)的文件夾
do {
try self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
}
根據(jù)類型創(chuàng)建好對應(yīng)的隊(duì)列名稱,目錄和文件夾。
一般我在項(xiàng)目中只用到三種類型,所以我自己聲明好三個(gè)對象,方便自己使用。
聲明三個(gè)私有的全局對象
private let page = ZZDiskCache(type:.Object)
private let image = ZZDiskCache(type:.Image)
private let voice = ZZDiskCache(type:.Voice)
對外開放調(diào)用的變量
// 針對Page
public class var sharedCacheObj: ZZDiskCache {
return page
}
// 針對Image
public class var sharedCacheImage: ZZDiskCache {
return image
}
// 針對Voice
public class var sharedCacheVoice: ZZDiskCache {
return voice
}
準(zhǔn)備工作完畢,可以真正的存儲和獲取了。
頁面的緩存一般緩存的是對象或者對象數(shù)組也有可能為nil,這里用AnyObject?
首先需要知道一點(diǎn)就是對象的緩存是通過歸檔和反歸檔 , 所有對象必須序列化和反序列化。也就是實(shí)現(xiàn)NSCoding的encodeWithCoder:和init?(coder aDecoder: NSCoder)
比如我們新建一個(gè)Student類,應(yīng)該這樣
import Foundation
class Student: NSObject,NSCoding {
var id:NSNumber?
var name:String?
//MARK: -序列化
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.name, forKey: "name")
aCoder.encodeObject(self.id, forKey: "id")
}
//MARK: -反序列化
required init?(coder aDecoder: NSCoder) {
self.id = aDecoder.decodeObjectForKey("id") as? NSNumber
self.name = aDecoder.decodeObjectForKey("name") as? String
}
}
對象存儲的時(shí)候需要一個(gè)路徑和一個(gè)key,這里寫了兩個(gè)方法來管理這個(gè)key,key既作為路徑也作為取值的key并對它進(jìn)行md5加密
extension ZZDiskCache{
func cachePathForKey(key: String) -> String {
let fileName = cacheFileNameForKey(key) //對name進(jìn)行MD5加密
return (diskCachePath as NSString).stringByAppendingPathComponent(fileName)
}
func cacheFileNameForKey(key: String) -> String {
return key.zz_MD5()
}
}
key.zz_MD5()是一個(gè)String的擴(kuò)展,后面我會把源碼地址放上,大家可以下載看。其實(shí)不加密也是可以的。
需要使用路徑的時(shí)候只需要傳入一個(gè)key進(jìn)去就行了
let path = self.cachePathForKey(key)
寫一個(gè)私有方法處理對象歸檔
/**
對象存儲 歸檔操作后寫入文件
- parameter key: 鍵
- parameter value: 值
- parameter path: 路徑
- parameter completeHandler: 完成后回調(diào)
*/
private func stroeObject(key:String,value:AnyObject?,path:String,completeHandler:(()->())? = nil){
dispatch_async(ioQueue){
let data = NSMutableData() //聲明一個(gè)可變的Data對象
//創(chuàng)建歸檔對象
let keyArchiver = NSKeyedArchiver(forWritingWithMutableData: data)
//開始?xì)w檔
keyArchiver.encodeObject(value, forKey: key.zz_MD5()) //對key進(jìn)行MD5加密
//完成歸檔
keyArchiver.finishEncoding() //歸檔完畢
do {
//寫入文件
try data.writeToFile(path, options: NSDataWritingOptions.DataWritingAtomic) //存儲
//完成回調(diào)
completeHandler?()
}catch let err{
print("err:\(err)")
}
}
}
這里的操作放在我們定義好的串行隊(duì)列中進(jìn)行,注釋很清楚了,就不再贅述。
同理寫兩個(gè)本地存儲UIImage和NSData(用來放音頻)的私有方法
/**
圖像存儲
- parameter image: image
- parameter key: 鍵
- parameter path: 路徑
- parameter completeHandler: 完成回調(diào)
*/
private func storeImage(image:UIImage,forKey key:String,path:String,completeHandler:(()->())? = nil){
dispatch_async(ioQueue) {
let data = UIImagePNGRepresentation(image.zz_normalizedImage())
if let data = data {
self.fileManager.createFileAtPath(path, contents: data, attributes: nil)
}
}
}
/**
存儲聲音
- parameter data: data
- parameter key: 鍵
- parameter path: 路徑
- parameter completeHandler: 完成回調(diào)
*/
private func storeVoice(data:NSData?,forKey key:String,path:String,completeHandler:(()->())? = nil){
dispatch_async(ioQueue) {
if let data = data {
self.fileManager.createFileAtPath(path, contents: data, attributes: nil)
}
}
}
圖像存儲中的zz_normalizedImage是擔(dān)心圖像的方向不對寫的UIImage的分類??梢韵螺d源碼查看。如果要真正用圖片緩存的話,在讀取的時(shí)候都加一層內(nèi)存的緩存,用NSCache就行了,用法很簡單 就不贅述了,因?yàn)楸疚闹攸c(diǎn)是本地緩存
然后寫一個(gè)公開的存儲方法,根據(jù)當(dāng)前的類型調(diào)用不同的私有方法。
/**
存儲
- parameter key: 鍵
- parameter value: 值
- parameter image: 圖像
- parameter data: data
- parameter completeHandler: 完成回調(diào)
*/
public func stroe(key:String,value:AnyObject? = nil,image:UIImage?,data:NSData?,completeHandler:(()->())? = nil){
let path = self.cachePathForKey(key)
switch storeType{
case .Object:
print("save Object ")
self.stroeObject(key, value: value,path:path,completeHandler:completeHandler)
case .Image:
print("save Image ")
if let image = image{
self.storeImage(image, forKey: key, path: path, completeHandler: completeHandler)
}
case .Voice:
print("save Voice ")
self.storeVoice(data, forKey: key, path: path, completeHandler: completeHandler)
}
}
用同樣的方式寫出獲取的方法
/**
獲取數(shù)據(jù)的方法
- parameter key: 鍵
- parameter objectGetHandler: 對象完成回調(diào)
- parameter imageGetHandler: 圖像完成回調(diào)
- parameter voiceGetHandler: 音頻完成回調(diào)
*/
public func retrieve(key:String,objectGetHandler:((obj:AnyObject?)->())? = nil,imageGetHandler:((image:UIImage?)->())? = nil,voiceGetHandler:((data:NSData?)->())?){
let path = self.cachePathForKey(key)
switch storeType{
case .Object:
self.retrieveObject(key.zz_MD5(), path: path, objectGetHandler: objectGetHandler)
case .Image:
self.retrieveImage(path,imageGetHandler:imageGetHandler)
case .Voice:
self.retrieveVoice(path, voiceGetHandler: voiceGetHandler)
}
}
/**
獲取文件歸檔對象
- parameter key: 鍵
- parameter path: 路徑
- parameter objectGetHandler: 獲得后回調(diào)閉包
*/
private func retrieveObject(key:String,path:String,objectGetHandler:((obj:AnyObject?)->())?){
//反歸檔 獲取
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
if self.fileManager.fileExistsAtPath(path){
let mdata = NSMutableData(contentsOfFile:path) //聲明可變Data
let unArchiver = NSKeyedUnarchiver(forReadingWithData: mdata!) //反歸檔對象
let obj = unArchiver.decodeObjectForKey(key) //反歸檔
objectGetHandler?(obj:obj) //完成回調(diào)
}
objectGetHandler?(obj:nil)
}
}
/**
獲取圖片
- parameter path: 路徑
- parameter imageGetHandler: 獲得后回調(diào)閉包
*/
private func retrieveImage(path:String,imageGetHandler:((image:UIImage?)->())?){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
if let data = NSData(contentsOfFile: path){
if let image = UIImage(data: data){
imageGetHandler?(image: image)
}
}
imageGetHandler?(image: nil)
}
}
/**
獲取音頻數(shù)據(jù)
- parameter path: 路徑
- parameter voiceGetHandler: 獲得后回調(diào)閉包
*/
private func retrieveVoice(path:String,voiceGetHandler:((data:NSData?)->())?){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
if let data = NSData(contentsOfFile: path){
voiceGetHandler?(data: data)
}
voiceGetHandler?(data: nil)
}
}
這樣一個(gè)針對對象、圖片、NSData進(jìn)行本地讀取并對其分目錄管理的類就寫完了,但是現(xiàn)在要調(diào)用非常麻煩,還需要進(jìn)一步封裝。
創(chuàng)建一個(gè)結(jié)構(gòu)體,對存儲和獲取方法進(jìn)行封裝
public struct ZZDiskCacheHelper {
/**
本地緩存對象
*/
static func saveObj(key:String,value:AnyObject?,completeHandler:(()->())? = nil){
ZZDiskCache.sharedCacheObj.stroe(key, value: value, image: nil, data: nil, completeHandler: completeHandler)
}
/**
本地緩存圖片
*/
static func saveImg(key:String,image:UIImage?,completeHandler:(()->())? = nil){
ZZDiskCache.sharedCacheImage.stroe(key, value: nil, image: image, data: nil, completeHandler: completeHandler)
}
/**
本地緩存音頻 或者其他 NSData類型
*/
static func saveVoc(key:String,data:NSData?,completeHandler:(()->())? = nil){
ZZDiskCache.sharedCacheVoice.stroe(key, value: nil, image: nil, data: data, completeHandler: completeHandler)
}
/**
獲得本地緩存的對象
*/
static func getObj(key:String,compelete:((obj:AnyObject?)->())){
ZZDiskCache.sharedCacheObj.retrieve(key, objectGetHandler: compelete, imageGetHandler: nil, voiceGetHandler: nil)
}
/**
獲得本地緩存的圖像
*/
static func getImg(key:String,compelete:((image:UIImage?)->())){
ZZDiskCache.sharedCacheImage.retrieve(key, objectGetHandler: nil, imageGetHandler: compelete, voiceGetHandler: nil)
}
/**
獲得本地緩存的音頻數(shù)據(jù)文件
*/
static func getVoc(key:String,compelete:((data:NSData?)->())){
ZZDiskCache.sharedCacheVoice.retrieve(key, objectGetHandler: nil, imageGetHandler: nil, voiceGetHandler: compelete)
}
}
經(jīng)過封裝,我們現(xiàn)在使用已經(jīng)很方便了,只需要這樣

但是每次還要輸入ZZDiskCacheHelper好麻煩 。
再加一句代碼
typealias $ = ZZDiskCacheHelper
這時(shí)候就很方便了


在任何想要存儲和獲取的地方只需要簡單的save和get就行了,文件夾,隊(duì)列異步等都在那個(gè)簡單的類中寫好了。
測試下,對象的。 此類我在項(xiàng)目中親測可用。歡迎下載。
空項(xiàng)目Caches下只有屏幕截圖

我們在viewDidLoad中加入這段代碼
let stu = Student()
stu.name = "小王"
stu.id = 1
$.saveObj("xxxx", value: stu)

我們創(chuàng)建的文件夾和文件都在了。
獲取更簡單。
$.getObj("xxxx") { (obj) -> () in
if let obj = obj as? Student{
print("\(obj.id) , \(obj.name)")
}
}
輸出:Optional(1) , Optional("小王")
圖片和NSData就不再這里演示了大家可以下載代碼看看。其實(shí)平時(shí)Coding的時(shí)候有很多可以封裝的東西,一次動手后面就輕松多了。
github地址:https://github.com/smalldu/ZZDiskCache
租房的伙伴可以試試:http://zuber.im/ 或者下載app--zuber(我們公司產(chǎn)品,專注解決租房問題)