iOS數據持久化設計

1、目標

瞭解移動端的數據持久化方式和對應的使用場景,提供相關技術選型作技術儲備。html

2、數據持久化的目的

  1. 快速展現,提高體驗
    • 已經加載過的數據,用戶下次查看時,不須要再次從網絡(磁盤)加載,直接展現給用戶
  2. 節省用戶流量(節省服務器資源)
    • 對於較大的資源數據進行緩存,下次展現無需下載消耗流量
    • 同時下降了服務器的訪問次數,節約服務器資源。(圖片)
  3. 離線使用。
    • 用戶瀏覽過的數據無需聯網,能夠再次查看。
    • 部分功能使用解除對網絡的依賴。(百度離線地圖、圖書閱讀器)
    • 無網絡時,容許用戶進行操做,等到下次聯網時同步到服務端。
  4. 記錄用戶操做
    • 草稿:對於用戶須要花費較大成本進行的操做,對用戶的每一個步驟進行緩存,用戶中斷操做後,下次用戶操做時直接繼續上次的操做。
    • 已讀內容標記緩存,幫助用戶識別哪些已讀。
    • 搜索記錄緩存
      ...

3、數據持久化方式分類

在移動端的數據持久化方式整體能夠分爲如下兩類:ios

一、內存緩存

  • 定義c++

    對於使用頻率比較高的數據,從網絡或者磁盤加載數據到內存之後,使用後並不立刻銷燬,下次使用時直接從內存加載。git

  • 案例github

    • iOS系統圖片加載——[UIImage imageNamed:@"imageName"]
    • 網絡圖片加載三方庫:SDWebImage

二、磁盤緩存

  • 定義算法

    將從網絡加載的、用戶操做產生的數據寫入到磁盤,用戶下次查看、繼續操做時,直接從磁盤加載使用。sql

  • 案例數據庫

    • 用戶輸入內容草稿緩存(如:評論、文本編輯)
    • 網絡圖片加載三方庫:SDWebImage
    • 搜索歷史緩存

4、緩存策略(常見緩存算法)

在緩存設計中,因爲硬件設備的存儲空間不是無限的,咱們指望存儲空間不要佔用過多,僅能緩存有限的數據,可是咱們但願得到更高的命中率。想達到這一目的。一般須要藉助緩存算法來實現。vim

一、FIFO(First in First out)

實現原理:數組

FIFO 先進先出的核心思想若是一個數據最早進入緩存中,則應該最先淘汰掉。相似實現一個按照時間前後順序的隊列來管理緩存,將淘汰最先訪問的數據緩存。

示意圖:

問題:

沒有考慮時間最近和訪問頻率對緩存命中率的影響。對於用戶較高几率訪問最近訪問數據的狀況,命中率會比較低。

二、LFU(Least Frequently Used)

實現原理:

LFU 最近最少使用算法是基於「若是一個數據在最近一段時間內使用次數不多,那麼在未來一段時間內被使用的可能性也很小」的思路。記錄用戶對數據的訪問次數,將訪問次數多的數據降序排列在一個容器中,淘汰訪問次數最少的數據。

問題:

LFU僅維護各項的被訪問頻率信息,對於某緩存項,若是該項在過去有着極高的訪問頻率而最近訪問頻率較低,當緩存空間已滿時該項很難被從緩存中替換出來,進而致使命中率降低。

三、 LRU (LeastRecentlyUsed)

實現原理:

LRU 是一種應用普遍的緩存算法。該算法維護一個緩存項隊列,隊列中的緩存項按每項的最後被訪問時間排序。當緩存空間已滿時,將處於隊尾,即刪除最後一次被訪問時間距如今最久的項,將新的區段放入隊列首部。

示意圖:

問題:

LRU算法僅維護了緩存塊的訪問時間信息,沒有考慮被訪問頻率等因素,當存在熱點數據時,LRU的效率很好,但偶發性的、週期性的批量操做會致使LRU命中率急劇降低。例如對於VoD(視頻點播)系統,用戶已經訪問過的數據不會重複訪問等場景。

四、 LRU-K (LeastRecentlyUsed)

實現原理:

相比LRU,其核心思想是將「最近使用過1次」的判斷標準擴展爲「最近使用過K次」。具體來講它多維護一個隊列,記錄全部緩存數據被訪問的歷史。僅當數據的訪問次數達到K次的時候,纔將數據放入緩存。當須要淘汰數據時,LRU-K會淘汰第K次訪問時間距當前時間最大的數據。

示意圖:

問題:

須要額外的空間來存儲訪問歷史,維護兩個隊列增長了算法的複雜度,提高了CPU等消耗。

五、2Q(Two queues)

實現原理:

2Q算法相似於LRU-2,不一樣點在於2Q將LRU-2算法中的訪問歷史隊列(注意這不是緩存數據的)改成一個FIFO緩存隊列,即:2Q算法有兩個緩存隊列,一個是FIFO隊列,一個是LRU隊列。

示意圖:

問題:

須要兩個隊列,但兩個隊列自己都比較簡單,2Q算法和LRU-2算法命中率、內存消耗都比較接近,但對於最後緩存的數據來講,2Q會減小一次從原始存儲讀取數據或者計算數據的操做。

六、MQ(Multi Queue)

實現原理:

MQ算法根據優先級(訪問頻率)將數據劃分爲多個LRU隊列,其核心思想是:優先緩存訪問次數多的數據。

示意圖:

問題:

多個隊列須要額外的空間來存儲緩存,維護多個隊列增長了算法的複雜度,提高了CPU等消耗。

5、iOS端可供選擇的數據持久化方案

1. 內存緩存

實現內存緩存的技術手段包括蘋果官方提供的NSURLCache,NSCache,還有性能和API上比較有優點的開源緩存庫YYCache、PINCache等。

2. 磁盤緩存

  • NSUserDefault

    適合小規模數據,弱業務相關數據的緩存。

  • keychain

    Keychain是蘋果提供的帶有可逆加密的存儲機制,廣泛用在各類存用戶名、密碼的需求上。另外,Keychain是系統級存儲,還能夠被iCloud同步,即便App被刪除,Keychain數據依然保留,用戶下次安裝App,能夠直接讀取,一般會用來存儲用戶惟一標識串。因此須要加密、同步iCloud的敏感小數據,通常使用Keychain存取。

  • 文件存儲

    • Plist:通常結構化的數據能夠Plist的方式去持久化
    • archive:Archive方式能夠存取遵循協議的數據,比較方便的是存取使用的都是對象,不過中間的序列化和反序列化須要花費必定的性能,能夠在想要使用對象直接進行磁盤存取時使用。
    • Stream:指文件存儲,通常用來存圖片、視頻文件等數據
  • 數據庫存儲

    數據庫適合存取一些關係型的數據;能夠在有大量的條件查詢排序類需求時使用。

    • Core Data:蘋果官方封裝的ORM(Object Relational Mapping)
    • FMDB:github最受歡迎的iOS sqlite 封裝開源庫之一
    • WCDB:微信團隊在本身使用的sqlite封裝基礎上的開源實現,具備ORM(Object Relational Mapping)的特性,支持iOS、Android。
    • Realm:由Y Combinator孵化的創業團隊開源出來的一款跨平臺(iOS、Android)移動數據庫。

3. 應該用哪一種緩存方案

根據需求選擇:

  • 簡單數據存儲直接寫文件、key-value存取便可。
  • 須要按照一些條件查找、排序等需求的,可使用sqlite等關係型存儲方式。
  • 敏感性高的數據,加密存儲。
  • 不但願App刪除後清除的小容量數據(用戶名、密碼、token)存keychain。

6、內存、磁盤數據持久化方案對比

一、可選方案詳解

1.一、NSCache

蘋果提供的一個簡單的內存緩存,它有着和 NSDictionary 相似的 API,不一樣點是它是線程安全的,而且不會 retain key,內部實現了內存警告處理(僅應用在後臺時,會移除一部分緩存)。

1.1.一、特性

  • 屬性
    • 名稱
    • delegate:obj從cache移除時,通知代理
    • countLimit:存儲數限制
    • costLimit:存儲空間開銷值限制(不精確)
    • evictsObjectsWithDiscardedContent(自動回收廢棄內容,沒看到這個屬性的使用場景)
  • 方法
    • 使用key同步存、取、刪
    • 刪除全部內容

1.1.二、實現

  • NSCacheEntry:內部類,將key-value轉換成改實體,用來實現雙向鏈表存儲結構
    • key:鍵
    • value:值
    • cost:開銷
    • prevByCost:上個節點
    • nextByCost:下個節點
  • NSCacheKey:對存取使用的key的封裝,用於實現存取使用不支持NSCopy協議的object
    • value:存取使用的key的值
  • _entries:NSDictionary,使用它以鍵值對形式存取NSCacheEntry實例
  • _head:雙向鏈表頭節點,鏈表按cost升序排序;setObject觸發costLimit/countLimit trim時,從根節點開始刪除
  • NSLock:實現讀寫線程安全

1.二、TMCache

TMCache 最初由 Tumblr 開發,但如今已經再也不維護了。TMMemoryCache 實現了不少 NSCache 並無提供的功能,好比數量限制、總容量限制、存活時間限制、內存警告或應用退到後臺時清空緩存等。TMMemoryCache 在設計時,主要目標是線程安全,它把全部讀寫操做都放到了同一個 concurrent queue 中,而後用 dispatch_barrier_async 來保證任務能順序執行。它錯誤的用了大量異步 block 回調來實現存取功能,以致於產生了很大的性能和死鎖問題。 因爲該庫好久再也不維護,不作詳細對比。

1.三、PINCache

Tumblr 宣佈不在維護 TMCache 後,由 Pinterest 維護和改進的一個緩存SDK。它的功能和接口基本和 TMCache 同樣,但修復了性能和死鎖的問題。它一樣也用 dispatch_semaphore 來保證線程安全,但去掉了dispatch_barrier_async,避免了線程切換帶來的巨大開銷,也避免了可能的死鎖。

1.3.一、特性:

  • PINCaching(protocal)

    • 屬性
      • 名稱
    • 方法
      • 同步/異步使用key存、取、刪、判斷存在、設置ttl時長、存儲空間消耗值
      • 同步/異步刪除指定日期以前的數據(磁盤緩存指建立日期)
      • 同步/異步刪除過時數據
      • 同步/異步刪除全部數據
  • PINMemoryCache

    • 屬性
      • totalCost:已經使用的總開銷
      • costLimit:開銷(內存)使用限制(每次賦值時,觸發trim)
      • ageLimit:統一輩子命週期限制(每次賦值時,觸發trim;GCD timer循環觸發)
      • ttlCache:是否ttl,配置此項,獲取數據只會返回生命週期存活狀態的數據
      • removeAllObjectsOnMemoryWarning
      • removeAllObjectsOnEnteringBackground
      • 將要/已經添加、移除緩存對象block監聽
      • 將要/已經移除緩存全部對象block監聽
      • 已經接收內存警告、已經進入後臺block監聽
    • 方法
      • 同步/異步刪除數據到指定的cost如下
      • 同步/異步刪除在指定日期以前的數據,繼續刪除數據到指定的cost如下(trimToCostLimitByDate)
      • 同步/異步遍歷全部緩存數據
    • 內部實現
      • 經過NSMutableDictionary保存須要緩存的數據,經過額外的NSMutableDictionary來保存createdDates(建立時間)、accessDates(最近訪問時間)、costLimit、ageLimit等信息
      • 使用互斥鎖保證多線程安全
      • 使用PINOperationQueue實現異步操做
      • setObject觸發costLimit trim時,對accessDates進行排序,實現LRU策略
  • PINDiskCache

    • 屬性
      • prefix:緩存名前綴
      • cacheURL:緩存路徑url
      • byteCount:硬盤已存儲數據大小
      • byteLimit:最大硬盤存儲空間限制,默認50M(每次賦值時,觸發trim)使用時注意,丟數據時不清楚爲何
      • ageLimit:同PINMemoryCache;默認30天
      • writingProtectionOption:
      • ttlCache:同PINMemoryCache
      • removeAllObjectsOnMemoryWarning(同PINMemoryCache)
      • removeAllObjectsOnEnteringBackground(同PINMemoryCache)
      • 將要/已經添加、移除緩存對象block監聽(同PINMemoryCache)
      • 將要/已經移除緩存全部對象block監聽(同PINMemoryCache)
      • 已經接收內存警告、已經進入後臺block監聽(同PINMemoryCache)
      • 支持對key進行自定義編碼和解碼(默認移除特殊字符.:/%
      • 支持對數據進行自定義序列化和反序列化(默認NSKeyedArchiver,須要遵照NSCoding協議)
    • 方法
      • lockFileAccessWhileExecutingBlockAsync、synchronouslyLockFileAccessWhileExecutingBlock:執行完全部文件寫操做後回調block
      • fileURLForKey:獲取指定文件的fileUrl
      • 同步/異步刪除數據到指定的cost如下(同PINMemoryCache)
      • 同步/異步刪除在指定日期以前的數據,繼續刪除數據到costLimit如下(同PINMemoryCache)
      • 同步/異步遍歷全部緩存數據(同PINMemoryCache)
    • 內部實現
      • 經過PINDiskCacheMetadata保存數據信息:createdDate、lastModifiedDate、size、ageLimit;初始化時,加載全部文件的metadata,保存在一個NSMutableDictionary中,經過fileKey存取;
      • 讀取文件獲取createdDate、lastModifiedDate、size等信息回寫metadata;setxattr、removexattr、getxattr存儲ageLimit信息,回寫metadata
      • trimDiskToSize:按照文件大小降序排序刪除,先刪大文件
      • trimDiskToSizeByDate:按最近修改時間升序排序,先刪較長時間未訪問的(LRU)
      • trimToDate:刪除建立日期在指定日期以前的文件(按修改時間倒序)
      • 使用互斥鎖保證多線程安全:
      • 使用PINOperationQueue實現異步操做
      • 對accessDates進行排序,實現LRU策略
  • PINCache

    • 屬性
      • diskByteCount:設置diskCache,byteCount
      • diskCache:磁盤緩存
      • memoryCache:內存緩存
    • 方法
      • 僅有初始化方法及 的實現
    • 實現
      • 二級緩存實現:先取內存;後取磁盤,取磁盤同時更新內存
      • 使用同一個PINOperationQueue實現異步操做
      • PINOperationGroup來實現內存緩存和磁盤緩存結束回調

1.3.二、實現

  • PINOperationQueue(async任務經過自定義的PINOperationQueue實現)
    • pthread_mutex PTHREAD_MUTEX_RECURSIVE(添加operation,線程安全)
    • dispatch_queue:
      • DISPATCH_QUEUE_SERIAL:併發數1時,直接使用串行隊列執行;使用串行隊列保證對信號量數據操做是安全的(修改併發數時,修改信號量數量)
      • DISPATCH_QUEUE_CONCURRENT:執行block中的耗時操做
    • dispatch_group:阻塞當前線程,用來實現 waitUntilAllOperationsAreFinished
    • dispatch_semaphore:併發數量控制,併發數爲大於1時使用。
  • PINOperationGroup
    • dispatch_group_enter、dispatch_group_leave、dispatch_group_notify,來回調group結束block
  • LRU淘汰
    • 每次設置新的object時,超出costLimit部分,根據訪問時間倒序刪除
  • 線程安全
    • pthread_mutex_lock 互斥🔐
    • PINOperationQueue 實現多線程隊列任務

1.四、YYCache

大神郭曜源開源的一個內存緩存實現,YYCache是對標PINCache實現的,實現了PINCache大部分的能力,同時作了一些針對性性能優化。 內存緩存相對於 PINMemoryCache 來講,去掉了異步訪問的接口,儘可能優化了同步訪問的性能,用 OSSpinLock pthread_mutex_t互斥鎖來保證線程安全。另外,緩存內部用雙向鏈表和 NSDictionary 實現了 LRU 淘汰算法。 磁盤緩存支持設置文件尺寸閾值來控制寫磁盤仍是存數據庫。

1.4.一、特性:

  • YYMemoryCache

    • 屬性
      • name:名稱
      • totalCount:緩存數
      • totalCost:已經使用的總開銷
      • countLimit:緩存數限制(並不是嚴格限制,GCD timer定時觸發後臺線程trim)
      • costLimit:開銷(內存)使用限制(並不是嚴格限制,GCD timer定時觸發後臺線程trim)
      • ageLimit:統一輩子命週期限制(並不是嚴格限制,GCD timer定時觸發後臺線程trim)
      • autoTrimInterval:定時觸發trim時長,默認5s
      • shouldRemoveAllObjectsOnMemoryWarning
      • shouldRemoveAllObjectsWhenEnteringBackground
      • releaseOnMainThread:是否容許主線程銷燬內存鍵值對,默認NO;注意,指定該值爲YES後,YYMemoryCache的緩存只有回到主線程才把緩存的對象銷燬,即執行release操做。
      • releaseAsynchronously:是否異步線程銷燬內存鍵值對,默認YES
      • 已經接收內存警告、已經進入後臺block監聽
    • 方法
      • 同步使用key存、取、刪、判斷存在、設置每一個存儲內存開銷值
      • 同步/異步刪除全部緩存(根據releaseOnMainThread、releaseAsynchronously決定)
      • 同步trim刪除數據到指定的count如下
      • 同步trim刪除數據到指定的cost如下(從tail開始移除,即移除最近未訪問數據)
      • 同步trim刪除在指定日期以前的數據
    • 內部實現
      • _YYLinkedMapNode:鏈表節點,key、value、pre、next、cost、time(CACurrentMediaTime,最近訪問時間)信息保存
      • _YYLinkedMap:最終使用_YYLinkedMap的節點經過鏈表方式執行增、刪、改操做
        • dic、totalCost、totalCount、head(MRU)、tail(LRU)、releaseOnMainThread、releaseAsynchronously
        • insertNodeAtHead
        • bringNodeToHead
        • removeNode
        • removeTailNode
        • removeAll
        • 鏈表最新訪問的放在頭結點,便於執行trim操做,直接從尾節點開始刪除
      • 使用pthread_mutex_t互斥鎖保證線程安全
      • 使用DISPATCH_QUEUE_SERIAL執行增長obj緩存觸發costLimit狀況下的trim任務
  • YYDiskCache

    • 屬性
      • name:緩存名
      • path:緩存路徑
      • inlineThreshold:控制保存sqlite或文件的閾值,大於該值存文件,默認20KB
      • customArchiveBlock、customUnarchiveBlock:對數據進行自定義序列化和反序列化(默認NSKeyedArchiver,須要遵照NSCoding協議)
      • customFileNameBlock:根據key名稱對文件名作自定義
      • countLimit:同YYMemoryCache;默認無限制
      • costLimit:同YYMemoryCache,這裏指真實的磁盤存儲大小;默認無限制
      • ageLimit:同YYMemoryCache;默認無限制
      • freeDiskSpaceLimit:磁盤可緩存最小剩餘空間限制;默認0
      • autoTrimInterval:同YYMemoryCache,默認60s
      • errorLogsEnabled:錯誤日誌
    • 方法
      • 同步/異步使用key存、取、判存、刪數據
      • 同步/異步刪除全部數據
      • 異步刪除全部數據並在block回調進度
      • 同步/異步獲取totalCount、totalCost
      • 同步/異步trimToCount、trimToCost、trimToAge
      • 爲指定object綁定extendedData
    • 內部實現
      • 使用dispatch_semaphore_t:信號量設置爲1,做爲鎖使用了
      • 使用dispatch_queue_t:DISPATCH_QUEUE_CONCURRENT,異步線程執行trim、CRUD等
        • 注意:這致使全部的異步操做回調block都是在異步線程,沒在主線程
      • _globalInstances:NSMapTable緩存了全部初始化的diskCache實例,key strong,value weak
      • YYKVStorage
      • 屬性
        • path:緩存路徑
        • type:YYKVStorageTypeFile、YYKVStorageTypeSQLite、YYKVStorageTypeMixed
        • errorLogsEnabled
      • 方法
        • 保存key-value數據
        • 根據key刪除key-value數據;刪除超過指定size的數據(訪問時間倒序刪除,每次刪除16個);刪除指定時間以前的數據(同);刪除數據到總體儲存空間到指定size內;刪除數據到總體儲存數量到指定count內;刪除全部數據
        • 使用key取數據
        • 判斷指定key是否存在數據;獲取存儲數量;獲取存儲佔用size
      • 實現
        • 內部使用selite存取數據
        • 刪除全部數據:先移動到指定的trash目錄下,而後後臺刪除trash目錄?移動文件比刪除文件更快?
        • DISPATCH_QUEUE_SERIAL:後臺刪除trash
  • YYCache

    • 屬性
      • name:名稱
      • memoryCache:內存緩存
      • diskCache:磁盤緩存
    • 方法
      • 同步/異步使用key存、取、判存、刪除數據
      • 同步/異步刪除全部數據
      • 異步刪除全部數據並在block回調進度
    • 實現
      • 二級緩存:先取內存,再取磁盤
      • 異步操做直接使用globalQueue執行了。

1.4.二、實現

  • 磁盤存取:封裝YYKVStorage執行文件讀寫、seqlite操做,具體的存取操做交給它完成
  • 內存LRU淘汰:每次設置新的object時,超出costLimit部分,根據訪問時間倒序刪除(藉助鏈表)
  • 線程安全
    • pthread_mutex_lock 互斥🔐 實現內存緩存線程安全
    • dispatch_semaphore_t:信號量設置爲1,做爲鎖使用了

二、內存緩存方案對比

2.一、性能

YYCache的讀寫性能均較爲優秀。NSCache和PINCache各有優劣。

內存緩存性能測

  • 個人性能測試圖:

性能測試說明:

在YYCache Demo基礎上進行的性能測試,使用的debug包,並不表明真實使用性能狀況。
複製代碼

個人內存緩存性能測試

2.一、對比

SDK API能力 易用性 實現 優缺點 是否維護
NSCache 同步存、取、刪,設置costLimit,countLimit、delegate(僅觸發trim刪除時通知) NSLock實現線程安全,內部將key-value信息轉換爲鏈表對象實體,使用NSDictionary存取實體,觸發trim時使用鏈表按cost降序刪除;應用後臺狀態觸發內存警告清除部分存儲 官方較可靠,但缺少拓展,功能不完善,性能通常 apple維護中
PINMemoryCache 同步/異步存、取、刪、判存、執行trim、遍歷全部已存儲數據;設置costLimit、ageLimit、ttlCache(超時數據不返回,清除)、removeAllObjectsOnMemoryWarning、removeAllObjectsOnEnteringBackground;添加刪除key-value block回調;應用進後臺、內存警告block回調; 使用pthread_mutex_t互斥鎖實現線程安全,使用NSDictionary存取實體,使用額外的NSDictionary存取實體的建立時間、更新時間、cost、ageLimit等信息,來實現相關能力,使用GCDtimer來定時trim 功能完善,易用性高,面向協議實現,總體架構清晰,根據存儲的更新時間實現了LRU策略,但內部存儲拆分了多個NSDictionary,致使性能降低 Pinterest維護中
YYMemoryCache 同步存、取、刪、判存、trim;設置countLimit、costLimit、ageLimit、autoTrimInterval、shouldRemoveAllObjectsOnMemoryWarning、shouldRemoveAllObjectsWhenEnteringBackground、應用進入後臺/接收內存警告block監聽 使用pthread_mutex_t互斥鎖實現線程安全,使用_YYLinkedMapNode內部類實體存儲鍵值對信息來實現雙向列表存儲結構,數據按訪問時間降序排序,基於此實現LRU cache 功能完善,易用性高,實現了LRU策略,性能高;但未抽象相關協議,內存和磁盤緩存重複度高 做者已不在維護

三、磁盤緩存方案對比

3.一、性能

小數據存取YYCache完勝。20KB以上文件存取YYCache較快。

內存緩存性能測試

  • 個人性能測試

性能測試說明: 在YYCache Demo基礎上進行的性能測試,使用的debug包,並不表明真實使用性能狀況。

3.二、對比

SDK API能力 易用性 實現 優缺點 是否維護
PINDiskCache 同步/異步存、取、刪、判斷存在、執行trim date/size/sizeByDate;設置byteLimit、ageLimit、ttlCache(超時數據不返回,清除)、NSDataWritingOptions(文件寫入模式),設置data自定義序列化block、key的自定義編解碼block;添加刪除key-value block回調;刪除全部數據回調;獲取緩存url、空間佔用大小,單個文件的存儲fileUrl;執行指定操做等待文件寫入鎖定打開;遍歷全部的已存儲文件 使用pthread_mutex_t互斥鎖實現讀寫線程安全,使用pthread_cond_t實現文件讀寫保護,使用PINDiskCacheMetadata將文件信息保存在內存中方便快速讀取,使用NSDictionary用key存取實體,,使用GCDtimer來定時trim,使用dispatch_semaphore_t控制併發實現自定義OperationQueue,按順序執行緩存隊列任務 功能完善,易用性高,面向協議實現,總體架構清晰,trim操做根據存儲的更新時間實現了LRU策略 Pinterest維護中
YYDiskCache 同步/異步存、取、刪、判斷存在、執行trim count/cost/age、獲取totalCost、totalCount;設置inlineThreshold、countLimit、costLimit、ageLimit、freeDiskSpaceLimit、autoTrimInterval;設置data自定義序列化block、fileName自定義的block 使用dispatch_semaphore_t信號量實現線程安全;使用YYKVStorageItem內部類實體存儲鍵值對key、value、filename、size、modTime、accessTime、extendedData等信息;由YYKVStorage實現具體文件存取,根據sqlite存取小空間數據速度優於直接文件讀寫的特性,設置存取方式閾值,空間小於閾值數據直接存sqlite,超過的閾值的數據索引信息存sqlite,數據存文件,基於此小數據存取性能較PINDiskCache提高數倍 功能完善,易用性高,實現了LRU策略,性能高;實現文件不一樣存儲策略更高效;但未抽象相關協議,內存和磁盤緩存重複度高 做者已不在維護

7、數據庫緩存

1.一、背景

原生的sqlite使用十分繁瑣,須要大量的代碼來完成一項sql操做,而且是c語言的API,對OC或者其它語言開發者並不友好,假如你想執行一個sql,須要作相似下面的操做:

- (void)example {
    sqlite3 *conn = NULL;
    //1. 打開數據庫
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"MyDatabase.db"];
    int result = sqlite3_open(path.UTF8String, &conn);
    if (result != SQLITE_OK) {
        sqlite3_close(conn);
        return;
    }
    const char *createTableSQL =
    "CREATE TABLE t_test_table (int_col INT, float_col REAL, string_col TEXT)";
    sqlite3_stmt* stmt = NULL;
    int len = strlen(createTableSQL);
    //2. 準備建立數據表,若是建立失敗,須要用sqlite3_finalize釋放sqlite3_stmt對象,以防止內存泄露。
    if (sqlite3_prepare_v2(conn,createTableSQL,len,&stmt,NULL) != SQLITE_OK) {
        if (stmt)
            sqlite3_finalize(stmt);
        sqlite3_close(conn);
        return;
    }
    //3. 經過sqlite3_step命令執行建立表的語句。對於DDL和DML語句而言,sqlite3_step執行正確的返回值只有SQLITE_DONE。
    //對於SELECT查詢而言,若是有數據返回SQLITE_ROW,當到達結果集末尾時則返回SQLITE_DONE。
    if (sqlite3_step(stmt) != SQLITE_DONE) {
        sqlite3_finalize(stmt);
        sqlite3_close(conn);
        return;
    }
    //4. 釋放建立表語句對象的資源。
    sqlite3_finalize(stmt);
    printf("Succeed to create test table now.\n");
    //5. 構造查詢表數據的sqlite3_stmt對象。
    const char* selectSQL = "SELECT * FROM TESTTABLE WHERE 1 = 0";
    sqlite3_stmt* stmt2 = NULL;
    if (sqlite3_prepare_v2(conn,selectSQL,strlen(selectSQL),&stmt2,NULL) != SQLITE_OK) {
        if (stmt2)
            sqlite3_finalize(stmt2);
        sqlite3_close(conn);
        return;
    }
    //6. 根據select語句的對象,獲取結果集中的字段數量。
    int fieldCount = sqlite3_column_count(stmt2);
    printf("The column count is %d.\n",fieldCount);
    //7. 遍歷結果集中每一個字段meta信息,並獲取其聲明時的類型。
    for (int i = 0; i < fieldCount; ++i) {
        //因爲此時Table中並不存在數據,再有就是SQLite中的數據類型自己是動態的,因此在沒有數據時沒法經過sqlite3_column_type函數獲取,此時sqlite3_column_type只會返回SQLITE_NULL,
        //直到有數據時才能返回具體的類型,所以這裏使用了sqlite3_column_decltype函數來獲取表聲明時給出的聲明類型。
        string stype = sqlite3_column_decltype(stmt2,i);
        stype = strlwr((char*)stype.c_str());
        //數據類型以決定字段親緣性的規則解析
        if (stype.find("int") != string::npos) {
            printf("The type of %dth column is INTEGER.\n",i);
        } else if (stype.find("char") != string::npos
                   || stype.find("text") != string::npos) {
            printf("The type of %dth column is TEXT.\n",i);
        } else if (stype.find("real") != string::npos
                   || stype.find("floa") != string::npos
                   || stype.find("doub") != string::npos ) {
            printf("The type of %dth column is DOUBLE.\n",i);
        }
    }
    sqlite3_finalize(stmt2);
    sqlite3_close(conn);
}
複製代碼

因爲sqlite在移動端不易直接使用,因此衍生出了許多對seqlite的封裝,包括如下被你們所熟知的流行庫,它們的最終實現都指向sqlite:

  • CoreData:蘋果基於sqlite封裝的ORM(Object Relational Mapping)的數據庫,直接對象映射————因爲CoreData的性能較差和學習成本較高,坑又很多(見唐巧老師的我爲何不喜歡 Core Data一文),下文不作詳細介紹
  • FMDB:iOS端github使用最廣的針對OC對sqlite的封裝,支持隊列操做
  • WCDB:微信技術團隊開源的對sqlite操做的封裝,支持對象和數據庫映射,ORM數據庫的一種實現,比FMDB更高效

有一個特例,它經過自建搜索引擎實現了一套ORM數據存儲:

  • Realm:realm團隊 對sqlite的封裝 經過自建搜索引擎實現的一套移動端數據庫,也是ORM數據庫的一種實現,是一個 MVCC 數據庫

1.二、對比

sqlite數據庫的使用包括增、刪、改、查等基本操做,同時在項目中運用,還須要數據轉模型、數據庫經過增刪表、字段和數據遷移完成版本升級等操做,下文經過對這些操做在各個流行庫中的使用示例來對比各個庫的易用性。

1.2.一、FMDB

FMDB是對sqlite的面向OC的封裝,把c語言對sql的操做封裝成OC風格代碼。主要有如下特色:

  • OC風格,省去了大量重複、冗餘的C語言代碼
  • 提供了多線程安全的數據庫操做方法,保證數據的一致性
  • 相比CoreData、Realm等更加輕量。
  • 支持事務
  • 支持全文檢索(fts subspec)
  • 支持對WAL(Write ahead logging)模式執行checkpoint操做

FMDB基本操做示例:

// 建表
NSString *sql = [NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS t_test_1 ('%@' INTEGER PRIMARY KEY AUTOINCREMENT,'%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' TEXT NOT NULL, '%@' INTEGER NOT NULL, '%@' FLOAT NOT NULL)", KEY_ID, KEY_MODEL_ID, KEY_MODEL_NAME, KEY_SERIES_ID, KEY_SERIES_NAME, KEY_TITLE, KEY_PRICE, KEY_DEALER_PRICE, KEY_SALES_STATUS, KEY_IS_SELECTED, KEY_DATE];
FMDatabaseQueue *_dbQueue = [FMDatabaseQueue databaseQueueWithPath:@"path"];
[_dbQueue inDatabase:^(FMDatabase *db) {
	BOOL result = [db executeUpdate:sql];
	if (result) {
	    //
	}
}];

// 插入一條數據
NSString *insertSql = [NSString stringWithFormat:@"INSERT INTO 't_test_1'(%@,%@,%@,%@,%@,%@,%@,%@,%@,%@) VALUES(\"%@\",\"%@\",\"%@\",\"%@\",\"%@\",\"%@\",\"%@\",\"%@\",%d,%.2f)", KEY_MODEL_ID, KEY_MODEL_NAME, KEY_SERIES_ID, KEY_SERIES_NAME, KEY_TITLE, KEY_PRICE, KEY_DEALER_PRICE, KEY_SALES_STATUS, KEY_IS_SELECTED, KEY_DATE, model.model_id, model.model_name, model.Id, model.Name, model.title, model.price, model.dealer_price, model.sales_status, isSelected,time];
[_dbQueue inDatabase:^(FMDatabase *db) {
    BOOL result = [db executeUpdate:sql];
	 if (result) {
	    //
	 }
}];

// 更新
NSString *sql = @"UPDATE t_userData SET userName = ? , userAge = ? WHERE id = ?";
[_dbQueue inDatabase:^(FMDatabase *db) {
    BOOL res = [db executeUpdate:sql,_nameTxteField.text,_ageTxteField.text,_userId];
	 if (result) {
	    //
	 }
}];

// 刪除
NSString *str = [NSString stringWithFormat:@"DELETE FROM t_userData WHERE id = %ld",userid];
[_dbQueue inDatabase:^(FMDatabase *db) {
    BOOL res = [db executeUpdate:str];
	 if (res) {
	    //
	 }
}];

// 查找
[_dbQueue inDatabase:^(FMDatabase *db) {
    FMResultSet *resultSet = [db executeQuery:@"SELECT * FROM message"];
	NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
	while ([resultSet next]) {
	    Message *message = [[Message alloc] init];
	    message.localID = [resultSet intForColumnIndex:0];
	    message.content = [resultSet stringForColumnIndex:1];
	    message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
	    message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
	    [messages addObject:message];
	}
}];

複製代碼

1.2.二、WCDB

WCDB是微信技術團隊內部在微信app sqlite使用實踐抽取的一套開源封裝,主要具備如下特色:

  • 經過宏定義的方式實現了ORM映射關係,根據映射關係完成建表、數據庫新增字段、修改字段名(綁定別名)、數據初始化綁定等操做
  • 自研了WINQ的語法,大部分場景不須要直接寫原生sqlite語句,易用性高
  • 內部實現了安全的多線程讀寫操做(寫操做仍是串行)和數據庫初始化優化,提高了性能(微信iOS SQLite源碼優化實踐

提供了其它較多場景的解決方案:

在WCDB內,ORM(Object Relational Mapping)是指

  • 將一個ObjC的類,映射到數據庫的表和索引;
  • 將類的property,映射到數據庫表的字段;

這一過程。經過ORM,能夠達到直接經過Object進行數據庫操做,省去拼裝過程的目的。

WCDB基本操做示例:

//Message.h
@interface Message : NSObject

@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(assign) int unused; //You can only define the properties you need

@end
//Message.mm
#import "Message.h"
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE(Message, modifiedTime)

WCDB_PRIMARY(Message, localID)

WCDB_INDEX(Message, "_index", createTime)

@end
//Message+WCTTableCoding.h
#import "Message.h"
#import <WCDB/WCDB.h>

@interface Message (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)

@end
複製代碼
// 建表
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
/*
 CREATE TABLE messsage (localID INTEGER PRIMARY KEY,
 						content TEXT,
 						createTime BLOB,
	 					modifiedTime BLOB)
 */
BOOL result = [database createTableAndIndexesOfName:@"message"
                                          withClass:Message.class];                              
//插入
Message *message = [[Message alloc] init];
message.localID = 1;
message.content = @"Hello, WCDB!";
message.createTime = [NSDate date];
message.modifiedTime = [NSDate date];
/*
 INSERT INTO message(localID, content, createTime, modifiedTime) 
 VALUES(1, "Hello, WCDB!", 1496396165, 1496396165);
 */
BOOL result = [database insertObject:message
                                into:@"message"];
//刪除
//DELETE FROM message WHERE localID>0;
BOOL result = [database deleteObjectsFromTable:@"message"
                                         where:Message.localID > 0];
//修改
//UPDATE message SET content="Hello, Wechat!";
Message *message = [[Message alloc] init];
message.content = @"Hello, Wechat!";
BOOL result = [database updateRowsInTable:@"message"
		                     onProperties:Message.content
        		               withObject:message];
//查詢
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [database getObjectsOfClass:Message.class
                                                fromTable:@"message"
                                                  orderBy:Message.localID.order()];
複製代碼

1.2.三、Realm

Realm團隊 基於sqlite封裝 自建搜索引擎實現的一套ORM數據庫操做模式,它是MVCC 數據庫,主要具備如下特色:

  • 對象就是一切(ORM映射)
  • MVCC 數據庫
  • Realm 採用了零拷貝 架構
  • 自動更新對象和查詢
  • String & Int 優化(String轉換爲枚舉,相似OC tagged point,)
  • 崩潰保護(系統異常崩潰時,經過copy-on-wirte機制保存了你已經修改的內容)
  • 真實的懶加載(使用時才從磁盤加載真實數據)
  • 內部加密(引擎層內建了加密)
  • 文檔詳細,且有中文版
  • 社區活躍,Stackoverflow能解決你幾乎全部問題
  • 跨平臺,支持iOS、Android
  • 提供Mac版Realm Browser,查看數據很方便
  • 簡便的數據庫版本升級。Realm能夠配置數據庫版本,進行判斷升級。
  • 支持KVC/KVO
  • 支持監聽屬性變化通知(寫入操做觸發通知)

限制:

  • 類名長度最大57個UTF8字符。
  • 屬性名長度最大63個UTF8字符。
  • NSData及NSString屬性不能保存超過16M數據。
  • 對字符串進行排序以及不區分大小寫查詢只支持「基礎拉丁字符集」、「拉丁字符補充集」、「拉丁文擴展字符集 A」 以及」拉丁文擴展字符集 B「(UTF-8 的範圍在 0~591 之間)。
  • 多線程訪問時須要新建新的Realm對象。
  • Realm對象的 Setters & Getters 不能被重載
  • Realm沒有自增屬性。也就是沒有自增主鍵,若是須要,須要本身去賦值,若是隻要求unique,那麼能夠設爲[[NSUUID UUID] UUIDString]
  • 全部的數據模型必須直接繼承自RealmObject。這阻礙咱們利用數據模型中的任意類型的繼承。(如JsonModel)
  • Realm不支持集合類型,僅有一個集合RLMArray,服務端返回的數組數據須要本身轉換。支持如下的屬性類型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊類型標記的NSNumber。

Realm基本操做示例:

// 定義模型的作法和定義常規 Objective‑C 類的作法相似
@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end
@implementation Dog
@end
RLM_ARRAY_TYPE(Dog)

Dog *mydog = [[Dog alloc] init];
mydog.name = @"Rex";
mydog.age = 1;
mydog.picture = nil; // 該屬性是可空的
NSLog(@"Name of dog: %@", mydog.name);

RLMRealm *realm = [RLMRealm defaultRealm];
[Dog createOrUpdateInRealm:realm withValue:mydog];

// 查找;找到小於2歲名叫Rex的全部狗
RLMResults<Dog *> *puppies = [Dog objectsWhere:@"age < 2 ADN name = 'Rex'"];
puppies.count; // => 0 由於目前尚未任何狗狗被添加到了 Realm 數據庫中

// 存儲
[realm transactionWithBlock:^{
    [realm addObject:mydog];
}];

// 檢索結果會實時更新
puppies.count; // => 1

/// 刪除數據
[realm transactionWithBlock:^{
    [realm deleteObject:mydog];
}];

//修改數據
[realm transactionWithBlock:^{
	theDog.age = 1;
}];

// 能夠在任何一個線程中執行檢索、更新操做
dispatch_async(dispatch_queue_create("background", 0), ^{
    @autoreleasepool {
        Dog *theDog = [[Dog objectsWhere:@"age == 1"] firstObject];
        RLMRealm *realm = [RLMRealm defaultRealm];
        [realm beginWriteTransaction];
        theDog.age = 3;
        [realm commitWriteTransaction];
    }
});
複製代碼

1.3 數據庫存取性能測試

性能測試說明:

測試數據見下方。因爲樣本比較少(僅1種數據),只進行了部分寫入和讀取操做,並不能徹底反應某個SDK的綜合性能,僅做爲參考。

測試數據和測試結果見下圖:

測試數據

順序插入1W條數據:

使用事務插入1W條數據:

讀取1W條數據:

多線程(2條)插入共2W條數據:

1.四、數據庫方案對比

SDK 優勢 缺點 是否維護
FMDB 較爲輕量級的sqlite封裝,API較原生使用方便許多,對SDK本省的學習成本較低,基本支持sqlite的全部能力,如事務、FTS等 不支持ORM,須要每一個編碼人員寫具體的sql語句,沒有較多的性能優化,數據庫操做相對複雜,關於數據加密、數據庫升級等操做須要用戶本身實現
WCDB 跨平臺;sqlite的深度封裝,支持ORM,基類支持本身繼承,不須要用戶直接寫sql,上手成本低,基本支持sqlite的全部能力;內部較多的性能優化;文檔較完善;拓展實現了錯誤統計、性能統計、損壞修復、反注入、加密等諸多能力,用戶須要作的事情較少 內部基於c++實現,基類須要.mm後綴(或者經過category解決),須要額外的宏來標記model和數據庫的映射關係
REALM 跨平臺;支持ORM;文檔十分完善;MVCC的實現;零拷貝提高性能;API十分友好;提供了配套可視化工具 不是基於sqlite的關係型數據庫,不能或很難創建表之間的關聯關係,項目中遇到相似場景可能較難解決; 基類只能繼承自RLMObject,不能自由繼承,不方便實現相似JsonModel等屬性綁定

性能數據:

8、持久化在項目中的應用(小結)

一、 圖片緩存

SDWebImageKingFisher)爲表明的圖片緩存庫基本都實現了二級緩存、隊列下載、異步解壓、Category拓展等能力,經常使用的圖片加載展現需求均可以使用它們來完成。

二、 簡單key-value存取

系統的如NSCache、NSKeyedArchive等緩存功能能知足基本的存取需求,可是並不易用。 PINCacheYYCache 等這些三方庫拓展了至關多的能力來知足大部分的使用場景,而且內部經過LRU等策略來提高效率,同時內部實現了二級緩存來加快加載速度,能夠考率直接使用。 其中PINCache雖然在一些測試數據上性能並不如YYCache,可是能夠看到github的PINCache最近依然有更新,而YYCache已經兩年沒有代碼提交了,issue沒有處理,遇到問題須要本身處理。 若是考慮維護成本的比例高一些,不妨使用PINCache,反之使用YYCache。

三、 數據庫

Core Data (本人未使用過)因爲入門門檻高、坑多等緣由致使口碑並不太好,這裏就不推薦嘗試了。 FMDB能夠說通過了大量iOS App的驗證,它雖然在一些擴展能力上並不盡人意,可是其穩定性久經考驗,基於sqlite實現,不改變表結構數據的狀況下,便於直接遷移到如WCDB等實現。 WCDB和Realm一樣都是支持ORM的,基本不須要寫sql語句就能完成增刪改查,都跨平臺,擴展瞭如加密、數據升級等不少便捷的封裝,用起來都比FMDB更爽。 但二者相較,假如你真的想使用ORM,我更推薦WCDB,由於Realm的搜索引擎暫不支持關聯表查詢是硬傷,而WCDB是基於sqlite的,支持直接使用sql語句查詢,若是業務中遇到相似場景沒法解決,還須要從Realm遷移到sqlite花費的力氣就大了。 除此以外,微信團隊自己就在使用WCDB,他們在數億用戶量的狀況下遇到的性能、數據損壞等問題比咱們要多得多,他們作的優化也就更多,而這些優化,你使用WCDB就能夠體驗到。

四、 其它

  1. 封裝 不管你使用哪一個三方庫進行緩存實現,最好作一層封裝,這樣便於你在想要切換別的實現時,直接內部作好數據遷移,對於使用方徹底無感知遷移,或者僅須要其作極少的工做,而不是全量的替換
  2. 區分用戶目錄存儲 每一個用戶都使用單獨的文件夾來存儲他的數據,對數據庫也同樣,這樣作的好處在於,用戶數據不會相互污染(好比數據庫中存在複雜的多表關聯關係時,會使你的sql語句變得很複雜,提高了你區分用戶出錯的機率),也便於進行數據診斷。
  3. 單例 建議對於某個時間段的數據操做都交給一個對象去作,內部來保證多線程讀寫安全,下降出錯的機率。
  4. 用戶切換的處理 因爲區分用戶存儲目錄,切換登陸用戶時,須要咱們切換數據存取的實例,此時,不要立刻銷燬上個實例,上個實例可能還有未完成的讀寫任務,等待完成或中斷其操做後再銷燬。

#參考

  • 文章
  1. iOS架構師之路:本地持久化方案
  2. IOS(數據持久化1)
  3. iOS應用架構談 本地持久化方案及動態部署
  4. 常見緩存算法和緩存策略
  5. 緩存淘汰算法--LRU算法
  6. iOS緩存框架-PINCache解讀
  7. IOS 緩存管理之 PINCache 使用
  8. YYCache 設計思路
  9. Sqlite學習筆記(四)&&SQLite-WAL原理
  10. 微信iOS SQLite源碼優化實踐
  11. 微信移動端數據庫組件WCDB系列(二) — 數據庫修復三板斧
  12. 數據庫的設計:深刻理解 Realm 的多線程處理機制
  13. Realm 核心數據庫引擎探祕
  14. Realm數據庫 從入門到「放棄」
  15. 使用Realm的一些總結
  16. Realm、WCDB與SQLite移動數據庫性能對比測試
  17. Realm、WCDB與SQLite移動數據庫性能測試
  • 開源庫
  1. wcdb
  2. realm
  3. PINCache
  4. YYCache
相關文章
相關標籤/搜索