iOS數(shù)據(jù)持久化設(shè)計(jì)探討(NSCache,PINCache,YYCache,CoreData,F(xiàn)MDB,WCDB,Realm)

一、目標(biāo)

了解移動(dòng)端的數(shù)據(jù)持久化方式和對(duì)應(yīng)的使用場(chǎng)景,提供相關(guān)技術(shù)選型做技術(shù)儲(chǔ)備。

二、數(shù)據(jù)持久化的目的

  1. 快速展示,提升體驗(yàn)
    • 已經(jīng)加載過(guò)的數(shù)據(jù),用戶(hù)下次查看時(shí),不需要再次從網(wǎng)絡(luò)(磁盤(pán))加載,直接展示給用戶(hù)
  2. 節(jié)省用戶(hù)流量(節(jié)省服務(wù)器資源)
    • 對(duì)于較大的資源數(shù)據(jù)進(jìn)行緩存,下次展示無(wú)需下載消耗流量
    • 同時(shí)降低了服務(wù)器的訪問(wèn)次數(shù),節(jié)約服務(wù)器資源。(圖片)
  3. 離線使用。
    • 用戶(hù)瀏覽過(guò)的數(shù)據(jù)無(wú)需聯(lián)網(wǎng),可以再次查看。
    • 部分功能使用解除對(duì)網(wǎng)絡(luò)的依賴(lài)。(百度離線地圖、圖書(shū)閱讀器)
    • 無(wú)網(wǎng)絡(luò)時(shí),允許用戶(hù)進(jìn)行操作,等到下次聯(lián)網(wǎng)時(shí)同步到服務(wù)端。
  4. 記錄用戶(hù)操作
    • 草稿:對(duì)于用戶(hù)需要花費(fèi)較大成本進(jìn)行的操作,對(duì)用戶(hù)的每個(gè)步驟進(jìn)行緩存,用戶(hù)中斷操作后,下次用戶(hù)操作時(shí)直接繼續(xù)上次的操作。
    • 已讀內(nèi)容標(biāo)記緩存,幫助用戶(hù)識(shí)別哪些已讀。
    • 搜索記錄緩存
      ...

三、數(shù)據(jù)持久化方式分類(lèi)

在移動(dòng)端的數(shù)據(jù)持久化方式總體可以分為以下兩類(lèi):

1、內(nèi)存緩存

  • 定義

    對(duì)于使用頻率比較高的數(shù)據(jù),從網(wǎng)絡(luò)或者磁盤(pán)加載數(shù)據(jù)到內(nèi)存以后,使用后并不馬上銷(xiāo)毀,下次使用時(shí)直接從內(nèi)存加載。

  • 案例

    • iOS系統(tǒng)圖片加載——[UIImage imageNamed:@"imageName"]
    • 網(wǎng)絡(luò)圖片加載三方庫(kù):SDWebImage

2、磁盤(pán)緩存

  • 定義

    將從網(wǎng)絡(luò)加載的、用戶(hù)操作產(chǎn)生的數(shù)據(jù)寫(xiě)入到磁盤(pán),用戶(hù)下次查看、繼續(xù)操作時(shí),直接從磁盤(pán)加載使用。

  • 案例

    • 用戶(hù)輸入內(nèi)容草稿緩存(如:評(píng)論、文本編輯)
    • 網(wǎng)絡(luò)圖片加載三方庫(kù):SDWebImage
    • 搜索歷史緩存

四、緩存策略(常見(jiàn)緩存算法)

在緩存設(shè)計(jì)中,由于硬件設(shè)備的存儲(chǔ)空間不是無(wú)限的,我們期望存儲(chǔ)空間不要占用過(guò)多,僅能緩存有限的數(shù)據(jù),但是我們希望獲得更高的命中率。想達(dá)到這一目的。通常需要借助緩存算法來(lái)實(shí)現(xiàn)。

1、FIFO(First in First out)

實(shí)現(xiàn)原理:

FIFO 先進(jìn)先出的核心思想如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中,則應(yīng)該最早淘汰掉。類(lèi)似實(shí)現(xiàn)一個(gè)按照時(shí)間先后順序的隊(duì)列來(lái)管理緩存,將淘汰最早訪問(wèn)的數(shù)據(jù)緩存。

示意圖:

image

問(wèn)題:

沒(méi)有考慮時(shí)間最近和訪問(wèn)頻率對(duì)緩存命中率的影響。對(duì)于用戶(hù)較高概率訪問(wèn)最近訪問(wèn)數(shù)據(jù)的情況,命中率會(huì)比較低。

2、LFU(Least Frequently Used)

實(shí)現(xiàn)原理:

LFU 最近最少使用算法是基于“如果一個(gè)數(shù)據(jù)在最近一段時(shí)間內(nèi)使用次數(shù)很少,那么在將來(lái)一段時(shí)間內(nèi)被使用的可能性也很小”的思路。記錄用戶(hù)對(duì)數(shù)據(jù)的訪問(wèn)次數(shù),將訪問(wèn)次數(shù)多的數(shù)據(jù)降序排列在一個(gè)容器中,淘汰訪問(wèn)次數(shù)最少的數(shù)據(jù)。

image

問(wèn)題:

LFU僅維護(hù)各項(xiàng)的被訪問(wèn)頻率信息,對(duì)于某緩存項(xiàng),如果該項(xiàng)在過(guò)去有著極高的訪問(wèn)頻率而最近訪問(wèn)頻率較低,當(dāng)緩存空間已滿(mǎn)時(shí)該項(xiàng)很難被從緩存中替換出來(lái),進(jìn)而導(dǎo)致命中率下降。

3、 LRU (LeastRecentlyUsed)

實(shí)現(xiàn)原理:

LRU 是一種應(yīng)用廣泛的緩存算法。該算法維護(hù)一個(gè)緩存項(xiàng)隊(duì)列,隊(duì)列中的緩存項(xiàng)按每項(xiàng)的最后被訪問(wèn)時(shí)間排序。當(dāng)緩存空間已滿(mǎn)時(shí),將處于隊(duì)尾,即刪除最后一次被訪問(wèn)時(shí)間距現(xiàn)在最久的項(xiàng),將新的區(qū)段放入隊(duì)列首部。

示意圖:

[圖片上傳失敗...(image-482483-1547798872438)]

問(wèn)題:

LRU算法僅維護(hù)了緩存塊的訪問(wèn)時(shí)間信息,沒(méi)有考慮被訪問(wèn)頻率等因素,當(dāng)存在熱點(diǎn)數(shù)據(jù)時(shí),LRU的效率很好,但偶發(fā)性的、周期性的批量操作會(huì)導(dǎo)致LRU命中率急劇下降。例如對(duì)于VoD(視頻點(diǎn)播)系統(tǒng),用戶(hù)已經(jīng)訪問(wèn)過(guò)的數(shù)據(jù)不會(huì)重復(fù)訪問(wèn)等場(chǎng)景。

4、 LRU-K (LeastRecentlyUsed)

實(shí)現(xiàn)原理:

相比LRU,其核心思想是將“最近使用過(guò)1次”的判斷標(biāo)準(zhǔn)擴(kuò)展為“最近使用過(guò)K次”。具體來(lái)說(shuō)它多維護(hù)一個(gè)隊(duì)列,記錄所有緩存數(shù)據(jù)被訪問(wèn)的歷史。僅當(dāng)數(shù)據(jù)的訪問(wèn)次數(shù)達(dá)到K次的時(shí)候,才將數(shù)據(jù)放入緩存。當(dāng)需要淘汰數(shù)據(jù)時(shí),LRU-K會(huì)淘汰第K次訪問(wèn)時(shí)間距當(dāng)前時(shí)間最大的數(shù)據(jù)。

示意圖:

[圖片上傳失敗...(image-d63a5f-1547798872438)]

問(wèn)題:

需要額外的空間來(lái)存儲(chǔ)訪問(wèn)歷史,維護(hù)兩個(gè)隊(duì)列增加了算法的復(fù)雜度,提升了CPU等消耗。

5、2Q(Two queues)

實(shí)現(xiàn)原理:

2Q算法類(lèi)似于LRU-2,不同點(diǎn)在于2Q將LRU-2算法中的訪問(wèn)歷史隊(duì)列(注意這不是緩存數(shù)據(jù)的)改為一個(gè)FIFO緩存隊(duì)列,即:2Q算法有兩個(gè)緩存隊(duì)列,一個(gè)是FIFO隊(duì)列,一個(gè)是LRU隊(duì)列。

示意圖:

[圖片上傳失敗...(image-904e22-1547798872438)]

問(wèn)題:

需要兩個(gè)隊(duì)列,但兩個(gè)隊(duì)列本身都比較簡(jiǎn)單,2Q算法和LRU-2算法命中率、內(nèi)存消耗都比較接近,但對(duì)于最后緩存的數(shù)據(jù)來(lái)說(shuō),2Q會(huì)減少一次從原始存儲(chǔ)讀取數(shù)據(jù)或者計(jì)算數(shù)據(jù)的操作。

6、MQ(Multi Queue)

實(shí)現(xiàn)原理:

MQ算法根據(jù)優(yōu)先級(jí)(訪問(wèn)頻率)將數(shù)據(jù)劃分為多個(gè)LRU隊(duì)列,其核心思想是:優(yōu)先緩存訪問(wèn)次數(shù)多的數(shù)據(jù)。

示意圖:

[圖片上傳失敗...(image-670bc2-1547798872438)]

問(wèn)題:

多個(gè)隊(duì)列需要額外的空間來(lái)存儲(chǔ)緩存,維護(hù)多個(gè)隊(duì)列增加了算法的復(fù)雜度,提升了CPU等消耗。

五、iOS端可供選擇的數(shù)據(jù)持久化方案

1. 內(nèi)存緩存

實(shí)現(xiàn)內(nèi)存緩存的技術(shù)手段包括蘋(píng)果官方提供的NSURLCache,NSCache,還有性能和API上比較有優(yōu)勢(shì)的開(kāi)源緩存庫(kù)YYCache、PINCache等。

2. 磁盤(pán)緩存

  • NSUserDefault

    適合小規(guī)模數(shù)據(jù),弱業(yè)務(wù)相關(guān)數(shù)據(jù)的緩存。

  • keychain

    Keychain是蘋(píng)果提供的帶有可逆加密的存儲(chǔ)機(jī)制,普遍用在各種存用戶(hù)名、密碼的需求上。另外,Keychain是系統(tǒng)級(jí)存儲(chǔ),還可以被iCloud同步,即使App被刪除,Keychain數(shù)據(jù)依然保留,用戶(hù)下次安裝App,可以直接讀取,通常會(huì)用來(lái)存儲(chǔ)用戶(hù)唯一標(biāo)識(shí)串。所以需要加密、同步iCloud的敏感小數(shù)據(jù),一般使用Keychain存取。

  • 文件存儲(chǔ)

    • Plist:一般結(jié)構(gòu)化的數(shù)據(jù)可以Plist的方式去持久化
    • archive:Archive方式可以存取遵循<NSCoding>協(xié)議的數(shù)據(jù),比較方便的是存取使用的都是對(duì)象,不過(guò)中間的序列化和反序列化需要花費(fèi)一定的性能,可以在想要使用對(duì)象直接進(jìn)行磁盤(pán)存取時(shí)使用。
    • Stream:指文件存儲(chǔ),一般用來(lái)存圖片、視頻文件等數(shù)據(jù)
  • 數(shù)據(jù)庫(kù)存儲(chǔ)

    數(shù)據(jù)庫(kù)適合存取一些關(guān)系型的數(shù)據(jù);可以在有大量的條件查詢(xún)排序類(lèi)需求時(shí)使用。

    • Core Data:蘋(píng)果官方封裝的ORM(Object Relational Mapping)
    • FMDB:github最受歡迎的iOS sqlite 封裝開(kāi)源庫(kù)之一
    • WCDB:微信團(tuán)隊(duì)在自己使用的sqlite封裝基礎(chǔ)上的開(kāi)源實(shí)現(xiàn),具有ORM(Object Relational Mapping)的特性,支持iOS、Android。
    • Realm:由Y Combinator孵化的創(chuàng)業(yè)團(tuán)隊(duì)開(kāi)源出來(lái)的一款跨平臺(tái)(iOS、Android)移動(dòng)數(shù)據(jù)庫(kù)。

3. 應(yīng)該用哪種緩存方案

根據(jù)需求選擇:

  • 簡(jiǎn)單數(shù)據(jù)存儲(chǔ)直接寫(xiě)文件、key-value存取即可。
  • 需要按照一些條件查找、排序等需求的,可以使用sqlite等關(guān)系型存儲(chǔ)方式。
  • 敏感性高的數(shù)據(jù),加密存儲(chǔ)。
  • 不希望App刪除后清除的小容量數(shù)據(jù)(用戶(hù)名、密碼、token)存keychain。

六、內(nèi)存、磁盤(pán)數(shù)據(jù)持久化方案對(duì)比

1、可選方案詳解

1.1、NSCache

蘋(píng)果提供的一個(gè)簡(jiǎn)單的內(nèi)存緩存,它有著和 NSDictionary 類(lèi)似的 API,不同點(diǎn)是它是線程安全的,并且不會(huì) retain key,內(nèi)部實(shí)現(xiàn)了內(nèi)存警告處理(僅應(yīng)用在后臺(tái)時(shí),會(huì)移除一部分緩存)。

1.1.1、特性

  • 屬性
    • 名稱(chēng)
    • delegate:obj從cache移除時(shí),通知代理
    • countLimit:存儲(chǔ)數(shù)限制
    • costLimit:存儲(chǔ)空間開(kāi)銷(xiāo)值限制(不精確)
    • evictsObjectsWithDiscardedContent(自動(dòng)回收廢棄內(nèi)容,沒(méi)看到這個(gè)屬性的使用場(chǎng)景)
  • 方法
    • 使用key同步存、取、刪
    • 刪除所有內(nèi)容

1.1.2、實(shí)現(xiàn)

  • NSCacheEntry:內(nèi)部類(lèi),將key-value轉(zhuǎn)換成改實(shí)體,用來(lái)實(shí)現(xiàn)雙向鏈表存儲(chǔ)結(jié)構(gòu)
    • key:鍵
    • value:值
    • cost:開(kāi)銷(xiāo)
    • prevByCost:上個(gè)節(jié)點(diǎn)
    • nextByCost:下個(gè)節(jié)點(diǎn)
  • NSCacheKey:對(duì)存取使用的key的封裝,用于實(shí)現(xiàn)存取使用不支持NSCopy協(xié)議的object
    • value:存取使用的key的值
  • _entries:NSDictionary,使用它以鍵值對(duì)形式存取NSCacheEntry實(shí)例
  • _head:雙向鏈表頭節(jié)點(diǎn),鏈表按cost升序排序;setObject觸發(fā)costLimit/countLimit trim時(shí),從根節(jié)點(diǎn)開(kāi)始刪除
  • NSLock:實(shí)現(xiàn)讀寫(xiě)線程安全

1.2、TMCache

TMCache 最初由 Tumblr 開(kāi)發(fā),但現(xiàn)在已經(jīng)不再維護(hù)了。TMMemoryCache 實(shí)現(xiàn)了很多 NSCache 并沒(méi)有提供的功能,比如數(shù)量限制、總?cè)萘肯拗?、存活時(shí)間限制、內(nèi)存警告或應(yīng)用退到后臺(tái)時(shí)清空緩存等。TMMemoryCache 在設(shè)計(jì)時(shí),主要目標(biāo)是線程安全,它把所有讀寫(xiě)操作都放到了同一個(gè) concurrent queue 中,然后用 dispatch_barrier_async 來(lái)保證任務(wù)能順序執(zhí)行。它錯(cuò)誤的用了大量異步 block 回調(diào)來(lái)實(shí)現(xiàn)存取功能,以至于產(chǎn)生了很大的性能和死鎖問(wèn)題。
由于該庫(kù)很久不再維護(hù),不做詳細(xì)對(duì)比。

1.3、PINCache

Tumblr 宣布不在維護(hù) TMCache 后,由 Pinterest 維護(hù)和改進(jìn)的一個(gè)緩存SDK。它的功能和接口基本和 TMCache 一樣,但修復(fù)了性能和死鎖的問(wèn)題。它同樣也用 dispatch_semaphore 來(lái)保證線程安全,但去掉了dispatch_barrier_async,避免了線程切換帶來(lái)的巨大開(kāi)銷(xiāo),也避免了可能的死鎖。

1.3.1、特性:

  • PINCaching(protocal)

    • 屬性
      • 名稱(chēng)
    • 方法
      • 同步/異步使用key存、取、刪、判斷存在、設(shè)置ttl時(shí)長(zhǎng)、存儲(chǔ)空間消耗值
      • 同步/異步刪除指定日期之前的數(shù)據(jù)(磁盤(pán)緩存指創(chuàng)建日期)
      • 同步/異步刪除過(guò)期數(shù)據(jù)
      • 同步/異步刪除所有數(shù)據(jù)
  • PINMemoryCache <PINCaching>

    • 屬性
      • totalCost:已經(jīng)使用的總開(kāi)銷(xiāo)
      • costLimit:開(kāi)銷(xiāo)(內(nèi)存)使用限制(每次賦值時(shí),觸發(fā)trim)
      • ageLimit:統(tǒng)一生命周期限制(每次賦值時(shí),觸發(fā)trim;GCD timer循環(huán)觸發(fā))
      • ttlCache:是否ttl,配置此項(xiàng),獲取數(shù)據(jù)只會(huì)返回生命周期存活狀態(tài)的數(shù)據(jù)
      • removeAllObjectsOnMemoryWarning
      • removeAllObjectsOnEnteringBackground
      • 將要/已經(jīng)添加、移除緩存對(duì)象block監(jiān)聽(tīng)
      • 將要/已經(jīng)移除緩存所有對(duì)象block監(jiān)聽(tīng)
      • 已經(jīng)接收內(nèi)存警告、已經(jīng)進(jìn)入后臺(tái)block監(jiān)聽(tīng)
    • 方法
      • 同步/異步刪除數(shù)據(jù)到指定的cost以下
      • 同步/異步刪除在指定日期之前的數(shù)據(jù),繼續(xù)刪除數(shù)據(jù)到指定的cost以下(trimToCostLimitByDate)
      • 同步/異步遍歷所有緩存數(shù)據(jù)
    • 內(nèi)部實(shí)現(xiàn)
      • 通過(guò)NSMutableDictionary保存需要緩存的數(shù)據(jù),通過(guò)額外的NSMutableDictionary來(lái)保存createdDates(創(chuàng)建時(shí)間)、accessDates(最近訪問(wèn)時(shí)間)、costLimit、ageLimit等信息
      • 使用互斥鎖保證多線程安全
      • 使用PINOperationQueue實(shí)現(xiàn)異步操作
      • setObject觸發(fā)costLimit trim時(shí),對(duì)accessDates進(jìn)行排序,實(shí)現(xiàn)LRU策略
  • PINDiskCache <PINCaching>

    • 屬性
      • prefix:緩存名前綴
      • cacheURL:緩存路徑url
      • byteCount:硬盤(pán)已存儲(chǔ)數(shù)據(jù)大小
      • byteLimit:最大硬盤(pán)存儲(chǔ)空間限制,默認(rèn)50M(每次賦值時(shí),觸發(fā)trim)使用時(shí)注意,丟數(shù)據(jù)時(shí)不清楚為什么
      • ageLimit:同PINMemoryCache;默認(rèn)30天
      • writingProtectionOption:
      • ttlCache:同PINMemoryCache
      • removeAllObjectsOnMemoryWarning(同PINMemoryCache)
      • removeAllObjectsOnEnteringBackground(同PINMemoryCache)
      • 將要/已經(jīng)添加、移除緩存對(duì)象block監(jiān)聽(tīng)(同PINMemoryCache)
      • 將要/已經(jīng)移除緩存所有對(duì)象block監(jiān)聽(tīng)(同PINMemoryCache)
      • 已經(jīng)接收內(nèi)存警告、已經(jīng)進(jìn)入后臺(tái)block監(jiān)聽(tīng)(同PINMemoryCache)
      • 支持對(duì)key進(jìn)行自定義編碼和解碼(默認(rèn)移除特殊字符.:/%
      • 支持對(duì)數(shù)據(jù)進(jìn)行自定義序列化和反序列化(默認(rèn)NSKeyedArchiver,需要遵守NSCoding協(xié)議)
    • 方法
      • lockFileAccessWhileExecutingBlockAsync、synchronouslyLockFileAccessWhileExecutingBlock:執(zhí)行完所有文件寫(xiě)操作后回調(diào)block
      • fileURLForKey:獲取指定文件的fileUrl
      • 同步/異步刪除數(shù)據(jù)到指定的cost以下(同PINMemoryCache)
      • 同步/異步刪除在指定日期之前的數(shù)據(jù),繼續(xù)刪除數(shù)據(jù)到costLimit以下(同PINMemoryCache)
      • 同步/異步遍歷所有緩存數(shù)據(jù)(同PINMemoryCache)
    • 內(nèi)部實(shí)現(xiàn)
      • 通過(guò)PINDiskCacheMetadata保存數(shù)據(jù)信息:createdDate、lastModifiedDate、size、ageLimit;初始化時(shí),加載所有文件的metadata,保存在一個(gè)NSMutableDictionary中,通過(guò)fileKey存??;
      • 讀取文件獲取createdDate、lastModifiedDate、size等信息回寫(xiě)metadata;setxattr、removexattr、getxattr存儲(chǔ)ageLimit信息,回寫(xiě)metadata
      • trimDiskToSize:按照文件大小降序排序刪除,先刪大文件
      • trimDiskToSizeByDate:按最近修改時(shí)間升序排序,先刪較長(zhǎng)時(shí)間未訪問(wèn)的(LRU)
      • trimToDate:刪除創(chuàng)建日期在指定日期之前的文件(按修改時(shí)間倒序)
      • 使用互斥鎖保證多線程安全:
      • 使用PINOperationQueue實(shí)現(xiàn)異步操作
      • 對(duì)accessDates進(jìn)行排序,實(shí)現(xiàn)LRU策略
  • PINCache <PINCaching>

    • 屬性
      • diskByteCount:設(shè)置diskCache,byteCount
      • diskCache:磁盤(pán)緩存
      • memoryCache:內(nèi)存緩存
    • 方法
      • 僅有初始化方法及 <PINCaching> 的實(shí)現(xiàn)
    • 實(shí)現(xiàn)
      • 二級(jí)緩存實(shí)現(xiàn):先取內(nèi)存;后取磁盤(pán),取磁盤(pán)同時(shí)更新內(nèi)存
      • 使用同一個(gè)PINOperationQueue實(shí)現(xiàn)異步操作
      • PINOperationGroup來(lái)實(shí)現(xiàn)內(nèi)存緩存和磁盤(pán)緩存結(jié)束回調(diào)

1.3.2、實(shí)現(xiàn)

  • PINOperationQueue(async任務(wù)通過(guò)自定義的PINOperationQueue實(shí)現(xiàn))
    • pthread_mutex PTHREAD_MUTEX_RECURSIVE(添加operation,線程安全)
    • dispatch_queue:
      • DISPATCH_QUEUE_SERIAL:并發(fā)數(shù)1時(shí),直接使用串行隊(duì)列執(zhí)行;使用串行隊(duì)列保證對(duì)信號(hào)量數(shù)據(jù)操作是安全的(修改并發(fā)數(shù)時(shí),修改信號(hào)量數(shù)量)
      • DISPATCH_QUEUE_CONCURRENT:執(zhí)行block中的耗時(shí)操作
    • dispatch_group:阻塞當(dāng)前線程,用來(lái)實(shí)現(xiàn) waitUntilAllOperationsAreFinished
    • dispatch_semaphore:并發(fā)數(shù)量控制,并發(fā)數(shù)為大于1時(shí)使用。
  • PINOperationGroup
    • dispatch_group_enter、dispatch_group_leave、dispatch_group_notify,來(lái)回調(diào)group結(jié)束block
  • LRU淘汰
    • 每次設(shè)置新的object時(shí),超出costLimit部分,根據(jù)訪問(wèn)時(shí)間倒序刪除
  • 線程安全
    • pthread_mutex_lock 互斥??
    • PINOperationQueue 實(shí)現(xiàn)多線程隊(duì)列任務(wù)

1.4、YYCache

大神郭曜源開(kāi)源的一個(gè)內(nèi)存緩存實(shí)現(xiàn),YYCache是對(duì)標(biāo)PINCache實(shí)現(xiàn)的,實(shí)現(xiàn)了PINCache大部分的能力,同時(shí)做了一些針對(duì)性性能優(yōu)化。
內(nèi)存緩存相對(duì)于 PINMemoryCache 來(lái)說(shuō),去掉了異步訪問(wèn)的接口,盡量?jī)?yōu)化了同步訪問(wèn)的性能,用 OSSpinLock pthread_mutex_t互斥鎖來(lái)保證線程安全。另外,緩存內(nèi)部用雙向鏈表和 NSDictionary 實(shí)現(xiàn)了 LRU 淘汰算法。
磁盤(pán)緩存支持設(shè)置文件尺寸閾值來(lái)控制寫(xiě)磁盤(pán)還是存數(shù)據(jù)庫(kù)。

1.4.1、特性:

  • YYMemoryCache

    • 屬性
      • name:名稱(chēng)
      • totalCount:緩存數(shù)
      • totalCost:已經(jīng)使用的總開(kāi)銷(xiāo)
      • countLimit:緩存數(shù)限制(并非嚴(yán)格限制,GCD timer定時(shí)觸發(fā)后臺(tái)線程trim)
      • costLimit:開(kāi)銷(xiāo)(內(nèi)存)使用限制(并非嚴(yán)格限制,GCD timer定時(shí)觸發(fā)后臺(tái)線程trim)
      • ageLimit:統(tǒng)一生命周期限制(并非嚴(yán)格限制,GCD timer定時(shí)觸發(fā)后臺(tái)線程trim)
      • autoTrimInterval:定時(shí)觸發(fā)trim時(shí)長(zhǎng),默認(rèn)5s
      • shouldRemoveAllObjectsOnMemoryWarning
      • shouldRemoveAllObjectsWhenEnteringBackground
      • releaseOnMainThread:是否允許主線程銷(xiāo)毀內(nèi)存鍵值對(duì),默認(rèn)NO;注意,指定該值為YES后,YYMemoryCache的緩存只有回到主線程才把緩存的對(duì)象銷(xiāo)毀,即執(zhí)行release操作。
      • releaseAsynchronously:是否異步線程銷(xiāo)毀內(nèi)存鍵值對(duì),默認(rèn)YES
      • 已經(jīng)接收內(nèi)存警告、已經(jīng)進(jìn)入后臺(tái)block監(jiān)聽(tīng)
    • 方法
      • 同步使用key存、取、刪、判斷存在、設(shè)置每個(gè)存儲(chǔ)內(nèi)存開(kāi)銷(xiāo)值
      • 同步/異步刪除所有緩存(根據(jù)releaseOnMainThread、releaseAsynchronously決定)
      • 同步trim刪除數(shù)據(jù)到指定的count以下
      • 同步trim刪除數(shù)據(jù)到指定的cost以下(從tail開(kāi)始移除,即移除最近未訪問(wèn)數(shù)據(jù))
      • 同步trim刪除在指定日期之前的數(shù)據(jù)
    • 內(nèi)部實(shí)現(xiàn)
      • _YYLinkedMapNode:鏈表節(jié)點(diǎn),key、value、pre、next、cost、time(CACurrentMediaTime,最近訪問(wèn)時(shí)間)信息保存
      • _YYLinkedMap:最終使用_YYLinkedMap的節(jié)點(diǎn)通過(guò)鏈表方式執(zhí)行增、刪、改操作
        • dic、totalCost、totalCount、head(MRU)、tail(LRU)、releaseOnMainThread、releaseAsynchronously
        • insertNodeAtHead
        • bringNodeToHead
        • removeNode
        • removeTailNode
        • removeAll
        • 鏈表最新訪問(wèn)的放在頭結(jié)點(diǎn),便于執(zhí)行trim操作,直接從尾節(jié)點(diǎn)開(kāi)始刪除
      • 使用pthread_mutex_t互斥鎖保證線程安全
      • 使用DISPATCH_QUEUE_SERIAL執(zhí)行增加obj緩存觸發(fā)costLimit情況下的trim任務(wù)
  • YYDiskCache

    • 屬性
      • name:緩存名
      • path:緩存路徑
      • inlineThreshold:控制保存sqlite或文件的閾值,大于該值存文件,默認(rèn)20KB
      • customArchiveBlock、customUnarchiveBlock:對(duì)數(shù)據(jù)進(jìn)行自定義序列化和反序列化(默認(rèn)NSKeyedArchiver,需要遵守NSCoding協(xié)議)
      • customFileNameBlock:根據(jù)key名稱(chēng)對(duì)文件名做自定義
      • countLimit:同YYMemoryCache;默認(rèn)無(wú)限制
      • costLimit:同YYMemoryCache,這里指真實(shí)的磁盤(pán)存儲(chǔ)大??;默認(rèn)無(wú)限制
      • ageLimit:同YYMemoryCache;默認(rèn)無(wú)限制
      • freeDiskSpaceLimit:磁盤(pán)可緩存最小剩余空間限制;默認(rèn)0
      • autoTrimInterval:同YYMemoryCache,默認(rèn)60s
      • errorLogsEnabled:錯(cuò)誤日志
    • 方法
      • 同步/異步使用key存、取、判存、刪數(shù)據(jù)
      • 同步/異步刪除所有數(shù)據(jù)
      • 異步刪除所有數(shù)據(jù)并在block回調(diào)進(jìn)度
      • 同步/異步獲取totalCount、totalCost
      • 同步/異步trimToCount、trimToCost、trimToAge
      • 為指定object綁定extendedData
    • 內(nèi)部實(shí)現(xiàn)
      • 使用dispatch_semaphore_t:信號(hào)量設(shè)置為1,作為鎖使用了
      • 使用dispatch_queue_t:DISPATCH_QUEUE_CONCURRENT,異步線程執(zhí)行trim、CRUD等
        • 注意:這導(dǎo)致所有的異步操作回調(diào)block都是在異步線程,沒(méi)在主線程
      • _globalInstances:NSMapTable緩存了所有初始化的diskCache實(shí)例,key strong,value weak
      • YYKVStorage
      • 屬性
        • path:緩存路徑
        • type:YYKVStorageTypeFile、YYKVStorageTypeSQLite、YYKVStorageTypeMixed
        • errorLogsEnabled
      • 方法
        • 保存key-value數(shù)據(jù)
        • 根據(jù)key刪除key-value數(shù)據(jù);刪除超過(guò)指定size的數(shù)據(jù)(訪問(wèn)時(shí)間倒序刪除,每次刪除16個(gè));刪除指定時(shí)間之前的數(shù)據(jù)(同);刪除數(shù)據(jù)到整體儲(chǔ)存空間到指定size內(nèi);刪除數(shù)據(jù)到整體儲(chǔ)存數(shù)量到指定count內(nèi);刪除所有數(shù)據(jù)
        • 使用key取數(shù)據(jù)
        • 判斷指定key是否存在數(shù)據(jù);獲取存儲(chǔ)數(shù)量;獲取存儲(chǔ)占用size
      • 實(shí)現(xiàn)
        • 內(nèi)部使用selite存取數(shù)據(jù)
        • 刪除所有數(shù)據(jù):先移動(dòng)到指定的trash目錄下,然后后臺(tái)刪除trash目錄?移動(dòng)文件比刪除文件更快?
        • DISPATCH_QUEUE_SERIAL:后臺(tái)刪除trash
  • YYCache

    • 屬性
      • name:名稱(chēng)
      • memoryCache:內(nèi)存緩存
      • diskCache:磁盤(pán)緩存
    • 方法
      • 同步/異步使用key存、取、判存、刪除數(shù)據(jù)
      • 同步/異步刪除所有數(shù)據(jù)
      • 異步刪除所有數(shù)據(jù)并在block回調(diào)進(jìn)度
    • 實(shí)現(xiàn)
      • 二級(jí)緩存:先取內(nèi)存,再取磁盤(pán)
      • 異步操作直接使用globalQueue執(zhí)行了。

1.4.2、實(shí)現(xiàn)

  • 磁盤(pán)存?。悍庋bYYKVStorage執(zhí)行文件讀寫(xiě)、seqlite操作,具體的存取操作交給它完成
  • 內(nèi)存LRU淘汰:每次設(shè)置新的object時(shí),超出costLimit部分,根據(jù)訪問(wèn)時(shí)間倒序刪除(借助鏈表)
  • 線程安全
    • pthread_mutex_lock 互斥?? 實(shí)現(xiàn)內(nèi)存緩存線程安全
    • dispatch_semaphore_t:信號(hào)量設(shè)置為1,作為鎖使用了

2、內(nèi)存緩存方案對(duì)比

2.1、性能

YYCache的讀寫(xiě)性能均較為優(yōu)秀。NSCache和PINCache各有優(yōu)劣。

內(nèi)存緩存性能測(cè)
  • 我的性能測(cè)試圖:

性能測(cè)試說(shuō)明:

在YYCache Demo基礎(chǔ)上進(jìn)行的性能測(cè)試,使用的debug包,并不代表真實(shí)使用性能情況。
我的內(nèi)存緩存性能測(cè)試

2.1、對(duì)比

SDK API能力 易用性 實(shí)現(xiàn) 優(yōu)缺點(diǎn) 是否維護(hù)
NSCache 同步存、取、刪,設(shè)置costLimit,countLimit、delegate(僅觸發(fā)trim刪除時(shí)通知) NSLock實(shí)現(xiàn)線程安全,內(nèi)部將key-value信息轉(zhuǎn)換為鏈表對(duì)象實(shí)體,使用NSDictionary存取實(shí)體,觸發(fā)trim時(shí)使用鏈表按cost降序刪除;應(yīng)用后臺(tái)狀態(tài)觸發(fā)內(nèi)存警告清除部分存儲(chǔ) 官方較可靠,但缺乏拓展,功能不完善,性能一般 apple維護(hù)中
PINMemoryCache 同步/異步存、取、刪、判存、執(zhí)行trim、遍歷所有已存儲(chǔ)數(shù)據(jù);設(shè)置costLimit、ageLimit、ttlCache(超時(shí)數(shù)據(jù)不返回,清除)、removeAllObjectsOnMemoryWarning、removeAllObjectsOnEnteringBackground;添加刪除key-value block回調(diào);應(yīng)用進(jìn)后臺(tái)、內(nèi)存警告block回調(diào); 使用pthread_mutex_t互斥鎖實(shí)現(xiàn)線程安全,使用NSDictionary存取實(shí)體,使用額外的NSDictionary存取實(shí)體的創(chuàng)建時(shí)間、更新時(shí)間、cost、ageLimit等信息,來(lái)實(shí)現(xiàn)相關(guān)能力,使用GCDtimer來(lái)定時(shí)trim 功能完善,易用性高,面向協(xié)議實(shí)現(xiàn),整體架構(gòu)清晰,根據(jù)存儲(chǔ)的更新時(shí)間實(shí)現(xiàn)了LRU策略,但內(nèi)部存儲(chǔ)拆分了多個(gè)NSDictionary,導(dǎo)致性能下降 Pinterest維護(hù)中
YYMemoryCache 同步存、取、刪、判存、trim;設(shè)置countLimit、costLimit、ageLimit、autoTrimInterval、shouldRemoveAllObjectsOnMemoryWarning、shouldRemoveAllObjectsWhenEnteringBackground、應(yīng)用進(jìn)入后臺(tái)/接收內(nèi)存警告block監(jiān)聽(tīng) 使用pthread_mutex_t互斥鎖實(shí)現(xiàn)線程安全,使用_YYLinkedMapNode內(nèi)部類(lèi)實(shí)體存儲(chǔ)鍵值對(duì)信息來(lái)實(shí)現(xiàn)雙向列表存儲(chǔ)結(jié)構(gòu),數(shù)據(jù)按訪問(wèn)時(shí)間降序排序,基于此實(shí)現(xiàn)LRU cache 功能完善,易用性高,實(shí)現(xiàn)了LRU策略,性能高;但未抽象相關(guān)協(xié)議,內(nèi)存和磁盤(pán)緩存重復(fù)度高 作者已不在維護(hù)

3、磁盤(pán)緩存方案對(duì)比

3.1、性能

小數(shù)據(jù)存取YYCache完勝。20KB以上文件存取YYCache較快。

內(nèi)存緩存性能測(cè)試
  • 我的性能測(cè)試

性能測(cè)試說(shuō)明:
在YYCache Demo基礎(chǔ)上進(jìn)行的性能測(cè)試,使用的debug包,并不代表真實(shí)使用性能情況。

image
image

3.2、對(duì)比

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

七、數(shù)據(jù)庫(kù)緩存

1.1、背景

原生的sqlite使用十分繁瑣,需要大量的代碼來(lái)完成一項(xiàng)sql操作,并且是c語(yǔ)言的API,對(duì)OC或者其它語(yǔ)言開(kāi)發(fā)者并不友好,假如你想執(zhí)行一個(gè)sql,需要做類(lèi)似下面的操作:

- (void)example {
    sqlite3 *conn = NULL;
    //1. 打開(kāi)數(shù)據(jù)庫(kù)
    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. 準(zhǔn)備創(chuàng)建數(shù)據(jù)表,如果創(chuàng)建失敗,需要用sqlite3_finalize釋放sqlite3_stmt對(duì)象,以防止內(nèi)存泄露。
    if (sqlite3_prepare_v2(conn,createTableSQL,len,&stmt,NULL) != SQLITE_OK) {
        if (stmt)
            sqlite3_finalize(stmt);
        sqlite3_close(conn);
        return;
    }
    //3. 通過(guò)sqlite3_step命令執(zhí)行創(chuàng)建表的語(yǔ)句。對(duì)于DDL和DML語(yǔ)句而言,sqlite3_step執(zhí)行正確的返回值只有SQLITE_DONE。
    //對(duì)于SELECT查詢(xún)而言,如果有數(shù)據(jù)返回SQLITE_ROW,當(dāng)?shù)竭_(dá)結(jié)果集末尾時(shí)則返回SQLITE_DONE。
    if (sqlite3_step(stmt) != SQLITE_DONE) {
        sqlite3_finalize(stmt);
        sqlite3_close(conn);
        return;
    }
    //4. 釋放創(chuàng)建表語(yǔ)句對(duì)象的資源。
    sqlite3_finalize(stmt);
    printf("Succeed to create test table now.\n");
    //5. 構(gòu)造查詢(xún)表數(shù)據(jù)的sqlite3_stmt對(duì)象。
    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. 根據(jù)select語(yǔ)句的對(duì)象,獲取結(jié)果集中的字段數(shù)量。
    int fieldCount = sqlite3_column_count(stmt2);
    printf("The column count is %d.\n",fieldCount);
    //7. 遍歷結(jié)果集中每個(gè)字段meta信息,并獲取其聲明時(shí)的類(lèi)型。
    for (int i = 0; i < fieldCount; ++i) {
        //由于此時(shí)Table中并不存在數(shù)據(jù),再有就是SQLite中的數(shù)據(jù)類(lèi)型本身是動(dòng)態(tài)的,所以在沒(méi)有數(shù)據(jù)時(shí)無(wú)法通過(guò)sqlite3_column_type函數(shù)獲取,此時(shí)sqlite3_column_type只會(huì)返回SQLITE_NULL,
        //直到有數(shù)據(jù)時(shí)才能返回具體的類(lèi)型,因此這里使用了sqlite3_column_decltype函數(shù)來(lái)獲取表聲明時(shí)給出的聲明類(lèi)型。
        string stype = sqlite3_column_decltype(stmt2,i);
        stype = strlwr((char*)stype.c_str());
        //數(shù)據(jù)類(lèi)型以決定字段親緣性的規(guī)則解析
        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在移動(dòng)端不易直接使用,所以衍生出了許多對(duì)seqlite的封裝,包括以下被大家所熟知的流行庫(kù),它們的最終實(shí)現(xiàn)都指向sqlite:

  • CoreData:蘋(píng)果基于sqlite封裝的ORM(Object Relational Mapping)的數(shù)據(jù)庫(kù),直接對(duì)象映射————由于CoreData的性能較差和學(xué)習(xí)成本較高,坑又不少(見(jiàn)唐巧老師的我為什么不喜歡 Core Data一文),下文不做詳細(xì)介紹
  • FMDB:iOS端github使用最廣的針對(duì)OC對(duì)sqlite的封裝,支持隊(duì)列操作
  • WCDB:微信技術(shù)團(tuán)隊(duì)開(kāi)源的對(duì)sqlite操作的封裝,支持對(duì)象和數(shù)據(jù)庫(kù)映射,ORM數(shù)據(jù)庫(kù)的一種實(shí)現(xiàn),比FMDB更高效

有一個(gè)特例,它通過(guò)自建搜索引擎實(shí)現(xiàn)了一套ORM數(shù)據(jù)存儲(chǔ):

  • Realm:realm團(tuán)隊(duì) 對(duì)sqlite的封裝 通過(guò)自建搜索引擎實(shí)現(xiàn)的一套移動(dòng)端數(shù)據(jù)庫(kù),也是ORM數(shù)據(jù)庫(kù)的一種實(shí)現(xiàn),是一個(gè) MVCC 數(shù)據(jù)庫(kù)

1.2、對(duì)比

sqlite數(shù)據(jù)庫(kù)的使用包括增、刪、改、查等基本操作,同時(shí)在項(xiàng)目中運(yùn)用,還需要數(shù)據(jù)轉(zhuǎn)模型、數(shù)據(jù)庫(kù)通過(guò)增刪表、字段和數(shù)據(jù)遷移完成版本升級(jí)等操作,下文通過(guò)對(duì)這些操作在各個(gè)流行庫(kù)中的使用示例來(lái)對(duì)比各個(gè)庫(kù)的易用性。

1.2.1、FMDB

FMDB是對(duì)sqlite的面向OC的封裝,把c語(yǔ)言對(duì)sql的操作封裝成OC風(fēng)格代碼。主要有以下特點(diǎn):

  • OC風(fēng)格,省去了大量重復(fù)、冗余的C語(yǔ)言代碼
  • 提供了多線程安全的數(shù)據(jù)庫(kù)操作方法,保證數(shù)據(jù)的一致性
  • 相比CoreData、Realm等更加輕量。
  • 支持事務(wù)
  • 支持全文檢索(fts subspec)
  • 支持對(duì)WAL(Write ahead logging)模式執(zhí)行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) {
        //
    }
}];

// 插入一條數(shù)據(jù)
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.2、WCDB

WCDB是微信技術(shù)團(tuán)隊(duì)內(nèi)部在微信app sqlite使用實(shí)踐抽取的一套開(kāi)源封裝,主要具有以下特點(diǎn):

  • 通過(guò)宏定義的方式實(shí)現(xiàn)了ORM映射關(guān)系,根據(jù)映射關(guān)系完成建表、數(shù)據(jù)庫(kù)新增字段、修改字段名(綁定別名)、數(shù)據(jù)初始化綁定等操作
  • 自研了WINQ的語(yǔ)法,大部分場(chǎng)景不需要直接寫(xiě)原生sqlite語(yǔ)句,易用性高
  • 內(nèi)部實(shí)現(xiàn)了安全的多線程讀寫(xiě)操作(寫(xiě)操作還是串行)和數(shù)據(jù)庫(kù)初始化優(yōu)化,提升了性能(微信iOS SQLite源碼優(yōu)化實(shí)踐

提供了其它較多場(chǎng)景的解決方案:

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

  • 將一個(gè)ObjC的類(lèi),映射到數(shù)據(jù)庫(kù)的表和索引;
  • 將類(lèi)的property,映射到數(shù)據(jù)庫(kù)表的字段;

這一過(guò)程。通過(guò)ORM,可以達(dá)到直接通過(guò)Object進(jìn)行數(shù)據(jù)庫(kù)操作,省去拼裝過(guò)程的目的。

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];
//查詢(xún)
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [database getObjectsOfClass:Message.class
                                                fromTable:@"message"
                                                  orderBy:Message.localID.order()];

1.2.3、Realm

Realm團(tuán)隊(duì) 基于sqlite封裝 自建搜索引擎實(shí)現(xiàn)的一套ORM數(shù)據(jù)庫(kù)操作模式,它是MVCC 數(shù)據(jù)庫(kù),主要具有以下特點(diǎn):

  • 對(duì)象就是一切(ORM映射)
  • MVCC 數(shù)據(jù)庫(kù)
  • Realm 采用了零拷貝 架構(gòu)
  • 自動(dòng)更新對(duì)象和查詢(xún)
  • String & Int 優(yōu)化(String轉(zhuǎn)換為枚舉,類(lèi)似OC tagged point,)
  • 崩潰保護(hù)(系統(tǒng)異常崩潰時(shí),通過(guò)copy-on-wirte機(jī)制保存了你已經(jīng)修改的內(nèi)容)
  • 真實(shí)的懶加載(使用時(shí)才從磁盤(pán)加載真實(shí)數(shù)據(jù))
  • 內(nèi)部加密(引擎層內(nèi)建了加密)
  • 文檔詳細(xì),且有中文版
  • 社區(qū)活躍,Stackoverflow能解決你幾乎所有問(wèn)題
  • 跨平臺(tái),支持iOS、Android
  • 提供Mac版Realm Browser,查看數(shù)據(jù)很方便
  • 簡(jiǎn)便的數(shù)據(jù)庫(kù)版本升級(jí)。Realm可以配置數(shù)據(jù)庫(kù)版本,進(jìn)行判斷升級(jí)。
  • 支持KVC/KVO
  • 支持監(jiān)聽(tīng)屬性變化通知(寫(xiě)入操作觸發(fā)通知)

限制:

  • 類(lèi)名長(zhǎng)度最大57個(gè)UTF8字符。
  • 屬性名長(zhǎng)度最大63個(gè)UTF8字符。
  • NSData及NSString屬性不能保存超過(guò)16M數(shù)據(jù)。
  • 對(duì)字符串進(jìn)行排序以及不區(qū)分大小寫(xiě)查詢(xún)只支持“基礎(chǔ)拉丁字符集”、“拉丁字符補(bǔ)充集”、“拉丁文擴(kuò)展字符集 A” 以及”拉丁文擴(kuò)展字符集 B“(UTF-8 的范圍在 0~591 之間)。
  • 多線程訪問(wèn)時(shí)需要新建新的Realm對(duì)象。
  • Realm對(duì)象的 Setters & Getters 不能被重載
  • Realm沒(méi)有自增屬性。也就是沒(méi)有自增主鍵,如果需要,需要自己去賦值,如果只要求unique,那么可以設(shè)為[[NSUUID UUID] UUIDString]
  • 所有的數(shù)據(jù)模型必須直接繼承自RealmObject。這阻礙我們利用數(shù)據(jù)模型中的任意類(lèi)型的繼承。(如JsonModel)
  • Realm不支持集合類(lèi)型,僅有一個(gè)集合RLMArray,服務(wù)端返回的數(shù)組數(shù)據(jù)需要自己轉(zhuǎn)換。支持以下的屬性類(lèi)型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊類(lèi)型標(biāo)記的NSNumber。

Realm基本操作示例:

// 定義模型的做法和定義常規(guī) Objective?C 類(lèi)的做法類(lèi)似
@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 因?yàn)槟壳斑€沒(méi)有任何狗狗被添加到了 Realm 數(shù)據(jù)庫(kù)中

// 存儲(chǔ)
[realm transactionWithBlock:^{
    [realm addObject:mydog];
}];

// 檢索結(jié)果會(huì)實(shí)時(shí)更新
puppies.count; // => 1

/// 刪除數(shù)據(jù)
[realm transactionWithBlock:^{
    [realm deleteObject:mydog];
}];

//修改數(shù)據(jù)
[realm transactionWithBlock:^{
    theDog.age = 1;
}];

// 可以在任何一個(gè)線程中執(zhí)行檢索、更新操作
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 數(shù)據(jù)庫(kù)存取性能測(cè)試

性能測(cè)試說(shuō)明:

測(cè)試數(shù)據(jù)見(jiàn)下方。由于樣本比較少(僅1種數(shù)據(jù)),只進(jìn)行了部分寫(xiě)入和讀取操作,并不能完全反應(yīng)某個(gè)SDK的綜合性能,僅作為參考。

測(cè)試數(shù)據(jù)和測(cè)試結(jié)果見(jiàn)下圖:

測(cè)試數(shù)據(jù)

順序插入1W條數(shù)據(jù):

image

使用事務(wù)插入1W條數(shù)據(jù):

image

讀取1W條數(shù)據(jù):

image

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

image

1.4、數(shù)據(jù)庫(kù)方案對(duì)比

SDK 優(yōu)點(diǎn) 缺點(diǎn) 是否維護(hù)
FMDB 較為輕量級(jí)的sqlite封裝,API較原生使用方便許多,對(duì)SDK本省的學(xué)習(xí)成本較低,基本支持sqlite的所有能力,如事務(wù)、FTS等 不支持ORM,需要每個(gè)編碼人員寫(xiě)具體的sql語(yǔ)句,沒(méi)有較多的性能優(yōu)化,數(shù)據(jù)庫(kù)操作相對(duì)復(fù)雜,關(guān)于數(shù)據(jù)加密、數(shù)據(jù)庫(kù)升級(jí)等操作需要用戶(hù)自己實(shí)現(xiàn)
WCDB 跨平臺(tái);sqlite的深度封裝,支持ORM,基類(lèi)支持自己繼承,不需要用戶(hù)直接寫(xiě)sql,上手成本低,基本支持sqlite的所有能力;內(nèi)部較多的性能優(yōu)化;文檔較完善;拓展實(shí)現(xiàn)了錯(cuò)誤統(tǒng)計(jì)、性能統(tǒng)計(jì)、損壞修復(fù)、反注入、加密等諸多能力,用戶(hù)需要做的事情較少 內(nèi)部基于c++實(shí)現(xiàn),基類(lèi)需要.mm后綴(或者通過(guò)category解決),需要額外的宏來(lái)標(biāo)記model和數(shù)據(jù)庫(kù)的映射關(guān)系
REALM 跨平臺(tái);支持ORM;文檔十分完善;MVCC的實(shí)現(xiàn);零拷貝提升性能;API十分友好;提供了配套可視化工具 不是基于sqlite的關(guān)系型數(shù)據(jù)庫(kù),不能或很難建立表之間的關(guān)聯(lián)關(guān)系,項(xiàng)目中遇到類(lèi)似場(chǎng)景可能較難解決; 基類(lèi)只能繼承自RLMObject,不能自由繼承,不方便實(shí)現(xiàn)類(lèi)似JsonModel等屬性綁定

性能數(shù)據(jù):

八、持久化在項(xiàng)目中的應(yīng)用(小結(jié))

1、 圖片緩存

SDWebImageKingFisher)為代表的圖片緩存庫(kù)基本都實(shí)現(xiàn)了二級(jí)緩存、隊(duì)列下載、異步解壓、Category拓展等能力,常用的圖片加載展示需求都可以使用它們來(lái)完成。

2、 簡(jiǎn)單key-value存取

系統(tǒng)的如NSCache、NSKeyedArchive等緩存功能能滿(mǎn)足基本的存取需求,但是并不易用。
PINCacheYYCache 等這些三方庫(kù)拓展了相當(dāng)多的能力來(lái)滿(mǎn)足大部分的使用場(chǎng)景,并且內(nèi)部通過(guò)LRU等策略來(lái)提升效率,同時(shí)內(nèi)部實(shí)現(xiàn)了二級(jí)緩存來(lái)加快加載速度,可以考率直接使用。
其中PINCache雖然在一些測(cè)試數(shù)據(jù)上性能并不如YYCache,但是可以看到github的PINCache最近依然有更新,而YYCache已經(jīng)兩年沒(méi)有代碼提交了,issue沒(méi)有處理,遇到問(wèn)題需要自己處理。
如果考慮維護(hù)成本的比例高一些,不妨使用PINCache,反之使用YYCache。

3、 數(shù)據(jù)庫(kù)

Core Data (本人未使用過(guò))由于入門(mén)門(mén)檻高、坑多等原因?qū)е驴诒⒉惶?,這里就不推薦嘗試了。
FMDB可以說(shuō)經(jīng)過(guò)了大量iOS App的驗(yàn)證,它雖然在一些擴(kuò)展能力上并不盡人意,但是其穩(wěn)定性久經(jīng)考驗(yàn),基于sqlite實(shí)現(xiàn),不改變表結(jié)構(gòu)數(shù)據(jù)的情況下,便于直接遷移到如WCDB等實(shí)現(xiàn)。
WCDB和Realm同樣都是支持ORM的,基本不需要寫(xiě)sql語(yǔ)句就能完成增刪改查,都跨平臺(tái),擴(kuò)展了如加密、數(shù)據(jù)升級(jí)等很多便捷的封裝,用起來(lái)都比FMDB更爽。
但兩者相較,假如你真的想使用ORM,我更推薦WCDB,因?yàn)镽ealm的搜索引擎暫不支持關(guān)聯(lián)表查詢(xún)是硬傷,而WCDB是基于sqlite的,支持直接使用sql語(yǔ)句查詢(xún),如果業(yè)務(wù)中遇到類(lèi)似場(chǎng)景無(wú)法解決,還需要從Realm遷移到sqlite花費(fèi)的力氣就大了。
除此之外,微信團(tuán)隊(duì)本身就在使用WCDB,他們?cè)跀?shù)億用戶(hù)量的情況下遇到的性能、數(shù)據(jù)損壞等問(wèn)題比我們要多得多,他們做的優(yōu)化也就更多,而這些優(yōu)化,你使用WCDB就可以體驗(yàn)到。

4、 其它

  1. 封裝
    無(wú)論你使用哪個(gè)三方庫(kù)進(jìn)行緩存實(shí)現(xiàn),最好做一層封裝,這樣便于你在想要切換別的實(shí)現(xiàn)時(shí),直接內(nèi)部做好數(shù)據(jù)遷移,對(duì)于使用方完全無(wú)感知遷移,或者僅需要其做極少的工作,而不是全量的替換
  2. 區(qū)分用戶(hù)目錄存儲(chǔ)
    每個(gè)用戶(hù)都使用單獨(dú)的文件夾來(lái)存儲(chǔ)他的數(shù)據(jù),對(duì)數(shù)據(jù)庫(kù)也一樣,這樣做的好處在于,用戶(hù)數(shù)據(jù)不會(huì)相互污染(比如數(shù)據(jù)庫(kù)中存在復(fù)雜的多表關(guān)聯(lián)關(guān)系時(shí),會(huì)使你的sql語(yǔ)句變得很復(fù)雜,提升了你區(qū)分用戶(hù)出錯(cuò)的概率),也便于進(jìn)行數(shù)據(jù)診斷。
  3. 單例
    建議對(duì)于某個(gè)時(shí)間段的數(shù)據(jù)操作都交給一個(gè)對(duì)象去做,內(nèi)部來(lái)保證多線程讀寫(xiě)安全,降低出錯(cuò)的概率。
  4. 用戶(hù)切換的處理
    由于區(qū)分用戶(hù)存儲(chǔ)目錄,切換登錄用戶(hù)時(shí),需要我們切換數(shù)據(jù)存取的實(shí)例,此時(shí),不要馬上銷(xiāo)毀上個(gè)實(shí)例,上個(gè)實(shí)例可能還有未完成的讀寫(xiě)任務(wù),等待完成或中斷其操作后再銷(xiāo)毀。

參考

  • 文章
  1. iOS架構(gòu)師之路:本地持久化方案
  2. IOS(數(shù)據(jù)持久化1)
  3. iOS應(yīng)用架構(gòu)談 本地持久化方案及動(dòng)態(tài)部署
  4. 常見(jiàn)緩存算法和緩存策略
  5. 緩存淘汰算法--LRU算法
  6. iOS緩存框架-PINCache解讀
  7. IOS 緩存管理之 PINCache 使用
  8. YYCache 設(shè)計(jì)思路
  9. Sqlite學(xué)習(xí)筆記(四)&&SQLite-WAL原理
  10. 微信iOS SQLite源碼優(yōu)化實(shí)踐
  11. 微信移動(dòng)端數(shù)據(jù)庫(kù)組件WCDB系列(二) — 數(shù)據(jù)庫(kù)修復(fù)三板斧
  12. 數(shù)據(jù)庫(kù)的設(shè)計(jì):深入理解 Realm 的多線程處理機(jī)制
  13. Realm 核心數(shù)據(jù)庫(kù)引擎探秘
  14. Realm數(shù)據(jù)庫(kù) 從入門(mén)到“放棄”
  15. 使用Realm的一些總結(jié)
  16. Realm、WCDB與SQLite移動(dòng)數(shù)據(jù)庫(kù)性能對(duì)比測(cè)試
  17. Realm、WCDB與SQLite移動(dòng)數(shù)據(jù)庫(kù)性能測(cè)試
  • 開(kāi)源庫(kù)
  1. wcdb
  2. realm
  3. PINCache
  4. YYCache
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 基本的排序算法有 冒泡排序 選擇排序 插入排序 希爾排序 歸并排序 快速排序 堆排序 各種排序的復(fù)雜度 冒泡排序 ...
    PYM_祺閱讀 315評(píng)論 0 0
  • 她漫不經(jīng)心地在街上走著,腦海中不斷浮現(xiàn)出老白問(wèn)的問(wèn)題… “你意識(shí)到自己愛(ài)上他了?” 她在逃避?到底在害怕什么? 無(wú)...
    寒樰閱讀 1,291評(píng)論 2 4
  • 姨媽來(lái)時(shí)情緒真容易波動(dòng)。 這幾天把自己又折騰得神神叨叨。 好不容易慢慢開(kāi)始接收撒狗糧的情侶們了。 原本以為自己恢復(fù)...
    Totoro大臉貓閱讀 223評(píng)論 0 0

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