你是用什么方法來持久保存數(shù)據(jù)的?這是在幾乎每一次關(guān)于iOS技術(shù)的交流或討論都會被提到的問題,而且大家對這個(gè)問題的熱情持續(xù)高漲。本文主要從概念上把“數(shù)據(jù)存儲”這個(gè)問題進(jìn)行剖析,并且結(jié)合各自特點(diǎn)和適用場景給大家提供一個(gè)選擇的思路,并不詳細(xì)介紹某一種方式的技術(shù)細(xì)節(jié)。
談到數(shù)據(jù)儲存,首先要明確區(qū)分兩個(gè)概念,數(shù)據(jù)結(jié)構(gòu)和儲存方式。所謂數(shù)據(jù)結(jié)構(gòu)就是數(shù)據(jù)存在的形式。除了基本的NSDictionary、NSArray和NSSet這些對象,還有更復(fù)雜的如:關(guān)系模型、對象圖和屬性列表多種結(jié)構(gòu)。而存儲方式則簡單的分為兩種:內(nèi)存與閃存。內(nèi)存存儲是臨時(shí)的,運(yùn)行時(shí)有效的,但效率高,而閃存則是一種持久化存儲,但產(chǎn)生I/O消耗,效率相對低。把內(nèi)存數(shù)據(jù)轉(zhuǎn)移到閃存中進(jìn)行持久化的操作稱成為歸檔。
二者結(jié)合起來才是完整的數(shù)據(jù)存儲方案,我們最常談起的那些:SQLite、CoreData、NSUserDefaults等都是數(shù)據(jù)存儲方案。當(dāng)然在這些框架提供的方案之外,我們自己也可以按照個(gè)性化需求訂制方案。這些存儲方案側(cè)重不同,支持的形式和方式也各不相同,在不同的使用場景下表現(xiàn)也是各有優(yōu)劣。但萬變不離其宗,無論什么方案都可以用下圖來解釋。
圖1,存儲方案示意圖
以下將對四種存儲方式進(jìn)行詳細(xì)的介紹:
- NSUserDefaults,用于存儲配置信息
- SQLite,用于存儲查詢需求較多的數(shù)據(jù)
- CoreData,用于規(guī)劃應(yīng)用中的對象
- 使用基本對象類型定制的個(gè)性化緩存方案
用NSUserDefaults存儲配置信息
NSUserDefaults被設(shè)計(jì)用來存儲設(shè)備和應(yīng)用的配置信息,它通過一個(gè)工廠方法返回默認(rèn)的、也是最常用到的實(shí)例對象。這個(gè)對象中儲存了系統(tǒng)中用戶的配置信息,開發(fā)者可以通過這個(gè)實(shí)例對象對這些已有的信息進(jìn)行修改,也可以按照自己的需求創(chuàng)建新的配置項(xiàng)。
圖2,筆者手機(jī)中[NSUserDefaults standardUserDefaults]內(nèi)容
NSUserDefaults把配置信息以字典的形式組織起來,支持字典的項(xiàng)包括:字符串或者是數(shù)組,除此之外還支持?jǐn)?shù)字等基本格式。一句話概括就是:基礎(chǔ)類型的小數(shù)據(jù)的字典。操作方法幾乎與NSDictionary的操作方法無異,另外還可以通過指定返回類型的方法獲取到指定類型的返回值。
圖3,NSUserDefaults提供的指定返回類型的方法列表
NSUserDefaults的所有數(shù)據(jù)都放在內(nèi)存里,因此操作速度很快,并還提供一個(gè)歸檔方法:+ (void)synchronize。開發(fā)者自定義的配置項(xiàng)(如圖2中的最后一項(xiàng) key:alkdjfkladsjfmm)會以plist格式的文件歸檔在相應(yīng)應(yīng)用目錄的/Library/Preferences/[App_Bundle_Identifier].plist文件。再次初始化獲得實(shí)例對象后,框架會把用戶自定義的這個(gè)配置和系統(tǒng)配置合并得到完整數(shù)據(jù)。
用SQLite存儲查詢需求較多的數(shù)據(jù)
iOS的SDK里預(yù)置了SQLite的庫,開發(fā)者可以自建SQLite數(shù)據(jù)庫。SQLite每次寫入數(shù)據(jù)都會產(chǎn)生IO消耗,把數(shù)據(jù)歸檔到相應(yīng)的文件。
SQLite擅長處理的數(shù)據(jù)類型其實(shí)與NSUserDefaults差不多,也是基礎(chǔ)類型的小數(shù)據(jù),只是從組織形式上不同。開發(fā)者可以以關(guān)系型數(shù)據(jù)庫的方式組織數(shù)據(jù),使用SQL DML來管理數(shù)據(jù)。 一般來說應(yīng)用中的格式化的文本類數(shù)據(jù)可以存放在數(shù)據(jù)庫中,尤其是類似聊天記錄、Timeline等這些具有條件查詢和排序需求的數(shù)據(jù)。
每一個(gè)數(shù)據(jù)庫的句柄都會在內(nèi)存中都會被分配一段緩存,用于提高查詢效率。另一個(gè)方面,由于查詢緩存,當(dāng)產(chǎn)生大量句柄或數(shù)據(jù)量較大時(shí),會出現(xiàn)緩存過大,造成內(nèi)存浪費(fèi)。
SQLite的使用起來要比NSUserDefaults復(fù)雜的多,因此建議開發(fā)者使用SQLite要搭配一個(gè)操作控件使用,可以簡化操作。筆者開發(fā)的SQLight是一款對SQLite操作的封裝,把相對復(fù)雜的SQLite命令封裝成對象和方法,可以供大家參考。大家可以在Github上獲取這個(gè)工程的代碼進(jìn)一步了解。
用CoreData規(guī)劃應(yīng)用中對象
官方給出的定義是,一個(gè)支持持久化的,對象圖和生命周期的自動(dòng)化管理方案。嚴(yán)格意義上說CoreData是一個(gè)管理方案,他的持久化可以通過SQLite、XML或二進(jìn)制文件儲存。如官方定義所說,CoreData的作用遠(yuǎn)遠(yuǎn)不止儲存數(shù)據(jù)這么簡單,它可以把整個(gè)應(yīng)用中的對象建模并進(jìn)行自動(dòng)化的管理。
圖4,官方文檔中解釋CoreData給出的對象圖示例
正如上圖所示,MyDocument是一個(gè)對象實(shí)例,有兩個(gè)Collection:Employee和Department,存放各自的對象列表。MyDocument、Employee和Department三個(gè)對象以及他們之間的關(guān)系都通過CoreData建模,并可以通過save方法進(jìn)行持久化。
從歸檔文件還原模型時(shí)CoreData并不是一次性把整個(gè)模型中的所有數(shù)據(jù)都載入內(nèi)存,而是根據(jù)運(yùn)行時(shí)狀態(tài),把被調(diào)用到的對象實(shí)例載入內(nèi)存?蚣軙詣(dòng)控制這個(gè)過程,從而達(dá)到控制內(nèi)存消耗,避免浪費(fèi)。
無論從設(shè)計(jì)原理還是使用方法上看,CoreData都比較復(fù)雜。因此,如果僅僅是考慮緩存數(shù)據(jù)這個(gè)需求,CoreData絕對不是一個(gè)優(yōu)選方案。CoreData的使用場景在于:整個(gè)應(yīng)用使用CoreData規(guī)劃,把應(yīng)用內(nèi)的數(shù)據(jù)通過CoreData建模,完全基于CoreData架構(gòu)應(yīng)用。
蘋果官方給出的一個(gè)示例代碼,結(jié)構(gòu)相對簡單,可以幫助大家入門CoreData。
使用基本對象類型定制的個(gè)性化緩存方案
之前提到的NSUserDefaults和SQLite適合存儲基礎(chǔ)類型的小數(shù)據(jù),而CoreData則不適合存儲單一的數(shù)據(jù),那么對于類似圖片這種較大的數(shù)據(jù)要用什么方式儲存呢?我給出的建議就是:自己實(shí)現(xiàn)一套存儲方案。說到訂制存儲方案大家非常容易質(zhì)疑,這是不是又在重新發(fā)明輪子。我可以非常明確的告訴大家,這絕不是在重新發(fā)明輪子。首先要明確,這個(gè)所謂的定制方案適用于互聯(lián)網(wǎng)應(yīng)用中對遠(yuǎn)程數(shù)據(jù)的緩存,幾個(gè)限制條件缺一不可。
從需求出發(fā)分析緩存數(shù)據(jù)有哪些要求:按Key查找,快速讀取,寫入不影響正常操作,不浪費(fèi)內(nèi)存,支持歸檔。這些都是基本需求,那么再進(jìn)一步或許還需要固定緩存項(xiàng)數(shù)量,支持隊(duì)列緩存,緩存過期等。從這些需求入手設(shè)計(jì)一個(gè)緩存方案并不十分復(fù)雜,Kache是筆者根據(jù)開發(fā)應(yīng)用的需求開發(fā)的一套緩存組件,通過分析Kache希望可以給大家一個(gè)思路。
圖5,Kache架構(gòu)圖
如上圖所示,Kache扮演的是一個(gè)典型緩存角色。應(yīng)用加載遠(yuǎn)程數(shù)據(jù)生成應(yīng)用數(shù)據(jù)對象的同時(shí),通過Kache把數(shù)據(jù)緩存起來,再次請求則直接通過Kache獲取數(shù)據(jù)。
緩存對象可以是NSDictionary、NSArray、NSSet或NSData這些可直接歸檔的類型,每個(gè)緩存對象對應(yīng)一個(gè)Key。緩存對象包括數(shù)據(jù)和過期時(shí)間,內(nèi)存中存放在一個(gè)單例字典中,閃存中每個(gè)對象存為一個(gè)文件。Key空間按照各種順序存放緩存對象的Key集合,Pool為固定大小的數(shù)組,當(dāng)數(shù)量達(dá)到上限,最早過期的一個(gè)Key將被刪除,對應(yīng)的緩存對象也被清除。Queue也是固定大小的數(shù)組,以先進(jìn)先出的規(guī)則管理Key的增刪。 每一次有新的緩存對象存入,自動(dòng)檢測Key空間中過期的集合并清除。
此外,控件提供save和load方法支持持久化和重新載入。
Kache最初設(shè)計(jì)為存放圖片緩存,之后也曾用于緩存文本數(shù)據(jù),由于使用了過期和歸檔相結(jié)合的邏輯,可以保證大部分命中的緩存對象都在內(nèi)存中,從而獲取了較高的效率。讀者可以從Github上獲取Kache源碼了解更多。
以上介紹了幾種iOS開發(fā)中經(jīng)常會遇到的儲存數(shù)據(jù)方法,從其存儲原理、使用方式和適用場景幾方面進(jìn)行進(jìn)了簡單的對比。事實(shí)上每一款應(yīng)用都很難采用一種單一的方案完成整個(gè)應(yīng)用的數(shù)據(jù)儲存任務(wù),需要根據(jù)不同的數(shù)據(jù)類型,選擇最合適的方案,以便整個(gè)應(yīng)用獲得良好的運(yùn)行時(shí)性能。
作者簡介:
高嘉峻(微博:@gaosboy),SegmentFault.com聯(lián)合創(chuàng)始人,杭州iOS開發(fā)者沙龍發(fā)起人,資深iOS開發(fā)者。