Swift之一步一步帶你封裝一個(gè)本地緩存庫

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

圖片來源于網(wǎng)絡(luò)
知識儲備

工欲善其事必先利其器,要想封裝一個(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)NSCodingencodeWithCoder: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)品,專注解決租房問題)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,694評論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,835評論 18 399
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,656評論 30 472
  • 白月坡 外表為27歲左右的男性,黑色長發(fā)容貌秀麗。調(diào)查局亞洲分部代理人。通稱白博士,擁有S級權(quán)限的最高長官,同時(shí)自...
    吃蘑菇喝酒看狐貍逗狗閱讀 441評論 0 0
  • 最近經(jīng)常陷入沉思:宇宙浩渺無垠,世界闊大遼遠(yuǎn),人群熙熙攘攘,你身處其中,再渺小不過。生活有什么意思?生又何歡,...
    哩吱閱讀 697評論 0 1

友情鏈接更多精彩內(nèi)容