天天看點

iOS中幾種資料持久化方案:我要永遠地記住你!

概論

所謂的持久化,就是将資料儲存到硬碟中,使得在應用程式或機器重新開機後可以繼續通路之前儲存的資料。在ios開發中,有很多資料持久化的方案,接下來我将嘗試着介紹一下5種方案:

plist檔案(屬性清單)

preference(偏好設定)

nskeyedarchiver(歸檔)

sqlite 3

coredata

沙盒

在介紹各種存儲方法之前,有必要說明以下沙盒機制。ios程式預設情況下隻能通路程式自己的目錄,這個目錄被稱為“沙盒”。

1.結構

既然沙盒就是一個檔案夾,那就看看裡面有什麼吧。沙盒的目錄結構如下:

1

2

3

4

5

6

<code>"應用程式包"</code>

<code>documents</code>

<code>library</code>

<code>    </code><code>caches</code>

<code>    </code><code>preferences</code>

<code>tmp</code>

2.目錄特性

雖然沙盒中有這麼多檔案夾,但是沒有檔案夾都不盡相同,都有各自的特性。是以在選擇存放目錄時,一定要認真選擇适合的目錄。

"應用程式包": 這裡面存放的是應用程式的源檔案,包括資源檔案和可執行檔案。

<code>  </code><code>nsstring *path = [[nsbundle mainbundle] bundlepath];</code>

<code>  </code><code>nslog(@</code><code>"%@"</code><code>, path);</code>

documents: 最常用的目錄,itunes同步該應用時會同步此檔案夾中的内容,适合存儲重要資料。

<code>  </code><code>nsstring *path = nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes).firstobject;</code>

library/caches: itunes不會同步此檔案夾,适合存儲體積大,不需要備份的非重要資料。

<code>  </code><code>nsstring *path = nssearchpathfordirectoriesindomains(nscachesdirectory, nsuserdomainmask, yes).firstobject;</code>

library/preferences: itunes同步該應用時會同步此檔案夾中的内容,通常儲存應用的設定資訊。

tmp: itunes不會同步此檔案夾,系統可能在應用沒運作時就删除該目錄下的檔案,是以此目錄适合儲存應用中的一些臨時檔案,用完就删除。

<code>  </code><code>nsstring *path = nstemporarydirectory();</code>

plist檔案

plist檔案是将某些特定的類,通過xml檔案的方式儲存在目錄中。

可以被序列化的類型隻有如下幾種:

7

8

9

10

<code>nsarray;</code>

<code>nsmutablearray;</code>

<code>nsdictionary;</code>

<code>nsmutabledictionary;</code>

<code>nsdata;</code>

<code>nsmutabledata;</code>

<code>nsstring;</code>

<code>nsmutablestring;</code>

<code>nsnumber;</code>

<code>nsdate;</code>

1.獲得檔案路徑

<code>    </code><code>nsstring *path = nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes).firstobject;</code>

<code>    </code><code>nsstring *filename = [path stringbyappendingpathcomponent:@</code><code>"123.plist"</code><code>];</code>

2.存儲

<code>nsarray *array = @[@</code><code>"123"</code><code>, @</code><code>"456"</code><code>, @</code><code>"789"</code><code>];</code>

<code>[array writetofile:filename atomically:yes];</code>

3.讀取

<code>nsarray *result = [nsarray arraywithcontentsoffile:filename];</code>

<code>nslog(@</code><code>"%@"</code><code>, result);</code>

4.注意

隻有以上列出的類型才能使用plist檔案存儲。

存儲時使用writetofile: atomically:方法。 其中atomically表示是否需要先寫入一個輔助檔案,再把輔助檔案拷貝到目标檔案位址。這是更安全的寫入檔案方法,一般都寫yes。

讀取時使用arraywithcontentsoffile:方法。

preference

1.使用方法

11

12

13

<code>//1.獲得nsuserdefaults檔案</code>

<code>nsuserdefaults *userdefaults = [nsuserdefaults standarduserdefaults];</code>

<code>//2.向檔案中寫入内容</code>

<code>[userdefaults setobject:@</code><code>"aaa"</code> <code>forkey:@</code><code>"a"</code><code>];</code>

<code>[userdefaults setbool:yes forkey:@</code><code>"sex"</code><code>];</code>

<code>[userdefaults setinteger:21 forkey:@</code><code>"age"</code><code>];</code>

<code>//2.1立即同步</code>

<code>[userdefaults synchronize];</code>

<code>//3.讀取檔案</code>

<code>nsstring *name = [userdefaults objectforkey:@</code><code>"a"</code><code>];</code>

<code>bool sex = [userdefaults boolforkey:@</code><code>"sex"</code><code>];</code>

<code>nsinteger age = [userdefaults integerforkey:@</code><code>"age"</code><code>];</code>

<code>nslog(@</code><code>"%@, %d, %ld"</code><code>, name, sex, age);</code>

2.注意

偏好設定是專門用來儲存應用程式的配置資訊的,一般不要在偏好設定中儲存其他資料。

如果沒有調用synchronize方法,系統會根據i/o情況不定時刻地儲存到檔案中。是以如果需要立即寫入檔案的就必須調用synchronize方法。

偏好設定會将所有資料儲存到同一個檔案中。即preference目錄下的一個以此應用包名來命名的plist檔案。

nskeyedarchiver

歸檔在ios中是另一種形式的序列化,隻要遵循了nscoding協定的對象都可以通過它實作序列化。由于決大多數支援存儲資料的foundation和cocoa touch類都遵循了nscoding協定,是以,對于大多數類來說,歸檔相對而言還是比較容易實作的。

1.遵循nscoding協定

nscoding協定聲明了兩個方法,這兩個方法都是必須實作的。一個用來說明如何将對象編碼到歸檔中,另一個說明如何進行解檔來擷取一個新對象。

遵循協定和設定屬性

<code>  </code><code>//1.遵循nscoding協定 </code>

<code>  </code><code>@interface person : nsobject   </code><code>//2.設定屬性</code>

<code>  </code><code>@property (strong, nonatomic) uiimage *avatar;</code>

<code>  </code><code>@property (copy, nonatomic) nsstring *name;</code>

<code>  </code><code>@property (assign, nonatomic) nsinteger age;</code>

<code>  </code><code>@end</code>

實作協定方法

14

15

<code>  </code><code>//解檔</code>

<code>  </code><code>- (id)initwithcoder:(nscoder *)adecoder {</code>

<code>      </code><code>if</code> <code>([</code><code>super</code> <code>init]) {</code>

<code>          </code><code>self.avatar = [adecoder decodeobjectforkey:@</code><code>"avatar"</code><code>];</code>

<code>          </code><code>self.name = [adecoder decodeobjectforkey:@</code><code>"name"</code><code>];</code>

<code>          </code><code>self.age = [adecoder decodeintegerforkey:@</code><code>"age"</code><code>];</code>

<code>      </code><code>}</code>

<code>      </code><code>return</code> <code>self;</code>

<code>  </code><code>}</code>

<code>  </code><code>//歸檔</code>

<code>  </code><code>- (void)encodewithcoder:(nscoder *)acoder {</code>

<code>      </code><code>[acoder encodeobject:self.avatar forkey:@</code><code>"avatar"</code><code>];</code>

<code>      </code><code>[acoder encodeobject:self.name forkey:@</code><code>"name"</code><code>];</code>

<code>      </code><code>[acoder encodeinteger:self.age forkey:@</code><code>"age"</code><code>];</code>

特别注意

如果需要歸檔的類是某個自定義類的子類時,就需要在歸檔和解檔之前先實作父類的歸檔和解檔方法。即 [super encodewithcoder:acoder] 和 [super initwithcoder:adecoder] 方法;

2.使用

需要把對象歸檔是調用nskeyedarchiver的工廠方法 archiverootobject: tofile: 方法。

<code>  </code><code>nsstring *file = [nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes).firstobject stringbyappendingpathcomponent:@</code><code>"person.data"</code><code>];</code>

<code>  </code><code>person *person = [[person alloc] init];</code>

<code>  </code><code>person.avatar = self.avatarview.image;</code>

<code>  </code><code>person.name = self.namefield.text;</code>

<code>  </code><code>person.age = [self.agefield.text integervalue];</code>

<code>  </code><code>[nskeyedarchiver archiverootobject:person tofile:file];</code>

需要從檔案中解檔對象就調用nskeyedunarchiver的一個工廠方法 unarchiveobjectwithfile: 即可。

<code>  </code><code>person *person = [nskeyedunarchiver unarchiveobjectwithfile:file];</code>

<code>  </code><code>if</code> <code>(person) {</code>

<code>     </code><code>self.avatarview.image = person.avatar;</code>

<code>     </code><code>self.namefield.text = person.name;</code>

<code>     </code><code>self.agefield.text = [nsstring stringwithformat:@</code><code>"%ld"</code><code>, person.age];</code>

3.注意

必須遵循并實作nscoding協定

儲存檔案的擴充名可以任意指定

繼承時必須先調用父類的歸檔解檔方法

sqlite3

之前的所有存儲方法,都是覆寫存儲。如果想要增加一條資料就必須把整個檔案讀出來,然後修改資料後再把整個内容覆寫寫入檔案。是以它們都不适合存儲大量的内容。

1.字段類型

表面上sqlite将資料分為以下幾種類型:

integer : 整數

real : 實數(浮點數)

text : 文本字元串

blob : 二進制資料,比如檔案,圖檔之類的

實際上sqlite是無類型的。即不管你在創表時指定的字段類型是什麼,存儲是依然可以存儲任意類型的資料。而且在創表時也可以不指定字段類型。sqlite之是以什麼類型就是為了良好的程式設計規範和友善開發人員交流,是以平時在使用時最好設定正确的字段類型!主鍵必須設定成integer

2. 準備工作

準備工作就是導入依賴庫啦,在ios中要使用sqlite3,需要添加庫檔案:libsqlite3.dylib并導入主頭檔案,這是一個c語言的庫,是以直接使用sqlite3還是比較麻煩的。

3.使用

建立資料庫并打開

操作資料庫之前必須先指定資料庫檔案和要操作的表,是以使用sqlite3,首先要打開資料庫檔案,然後指定或建立一張表。

16

17

18

19

20

21

22

<code>/**</code>

<code>*  打開資料庫并建立一個表</code>

<code>*/</code>

<code>- (void)opendatabase {</code>

<code>   </code><code>//1.設定檔案名</code>

<code>   </code><code>nsstring *filename = [nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes).firstobject stringbyappendingpathcomponent:@</code><code>"person.db"</code><code>];</code>

<code>   </code><code>//2.打開資料庫檔案,如果沒有會自動建立一個檔案</code>

<code>   </code><code>nsinteger result = sqlite3_open(filename.utf8string, &amp;_sqlite3);</code>

<code>   </code><code>if</code> <code>(result == sqlite_ok) {</code>

<code>       </code><code>nslog(@</code><code>"打開資料庫成功!"</code><code>);</code>

<code>       </code><code>//3.建立一個資料庫表</code>

<code>       </code><code>char *errmsg = null;</code>

<code>       </code><code>sqlite3_exec(_sqlite3, </code><code>"create table if not exists t_person(id integer primary key autoincrement, name text, age integer)"</code><code>, null, null, &amp;errmsg);</code>

<code>       </code><code>if</code> <code>(errmsg) {</code>

<code>           </code><code>nslog(@</code><code>"錯誤:%s"</code><code>, errmsg);</code>

<code>       </code><code>} </code><code>else</code> <code>{</code>

<code>           </code><code>nslog(@</code><code>"創表成功!"</code><code>);</code>

<code>       </code><code>}</code>

<code>   </code><code>} </code><code>else</code> <code>{</code>

<code>       </code><code>nslog(@</code><code>"打開資料庫失敗!"</code><code>);</code>

<code>   </code><code>}</code>

<code>}</code>

執行指令

使用 sqlite3_exec() 方法可以執行任何sql語句,比如創表、更新、插入和删除操作。但是一般不用它執行查詢語句,因為它不會傳回查詢到的資料。

<code>*  往表中插入1000條資料</code>

<code>- (void)insertdata {</code>

<code>nsstring *namestr;</code>

<code>nsinteger age;</code>

<code>for</code> <code>(nsinteger i = 0; i &lt; 1000; i++) {</code>

<code>  </code><code>namestr = [nsstring stringwithformat:@</code><code>"bourne-%d"</code><code>, arc4random_uniform(10000)];</code>

<code>  </code><code>age = arc4random_uniform(80) + 20;</code>

<code>  </code><code>nsstring *sql = [nsstring stringwithformat:@</code><code>"insert into t_person (name, age) values('%@', '%ld')"</code><code>, namestr, age];</code>

<code>  </code><code>char *errmsg = null;</code>

<code>  </code><code>sqlite3_exec(_sqlite3, sql.utf8string, null, null, &amp;errmsg);</code>

<code>  </code><code>if</code> <code>(errmsg) {</code>

<code>      </code><code>nslog(@</code><code>"錯誤:%s"</code><code>, errmsg);</code>

<code>nslog(@</code><code>"插入完畢!"</code><code>);</code>

查詢指令

前面說過一般不使用 sqlite3_exec() 方法查詢資料。因為查詢資料必須要獲得查詢結果,是以查詢相對比較麻煩。示例代碼如下:

sqlite3_prepare_v2() : 檢查sql的合法性

sqlite3_step() : 逐行擷取查詢結果,不斷重複,直到最後一條記錄

sqlite3_coloum_xxx() : 擷取對應類型的内容,icol對應的就是sql語句中字段的順序,從0開始。根據實際查詢字段的屬性,使用sqlite3_column_xxx取得對應的内容即可。

sqlite3_finalize() : 釋放stmt

<code>*  從表中讀取資料到數組中</code>

<code>- (void)readdata {</code>

<code>   </code><code>nsmutablearray *marray = [nsmutablearray arraywithcapacity:1000];</code>

<code>   </code><code>char *sql = </code><code>"select name, age from t_person;"</code><code>;</code>

<code>   </code><code>sqlite3_stmt *stmt;</code>

<code>   </code><code>nsinteger result = sqlite3_prepare_v2(_sqlite3, sql, -1, &amp;stmt, null);</code>

<code>       </code><code>while</code> <code>(sqlite3_step(stmt) == sqlite_row) {</code>

<code>           </code><code>char *name = (char *)sqlite3_column_text(stmt, 0);</code>

<code>           </code><code>nsinteger age = sqlite3_column_int(stmt, 1);</code>

<code>           </code><code>//建立對象</code>

<code>           </code><code>person *person = [person personwithname:[nsstring stringwithutf8string:name] age:age];</code>

<code>           </code><code>[marray addobject:person];</code>

<code>       </code><code>self.datalist = marray;</code>

<code>   </code><code>sqlite3_finalize(stmt);</code>

4.總結

總得來說,sqlite3的使用還是比較麻煩的,因為都是些c語言的函數,了解起來有些困難。不過在一般開發過程中,使用的都是第三方開源庫 fmdb,封裝了這些基本的c語言方法,使得我們在使用時更加容易了解,提高開發效率。

fmdb

1.簡介

fmdb是ios平台的sqlite資料庫架構,它是以oc的方式封裝了sqlite的c語言api,它相對于cocoa自帶的c語言架構有如下的優點:

使用起來更加面向對象,省去了很多麻煩、備援的c語言代碼

對比蘋果自帶的core data架構,更加輕量級和靈活

提供了多線程安全的資料庫操作方法,有效地防止資料混亂

2.核心類

fmdb有三個主要的類:

fmdatabase

一個fmdatabase對象就代表一個單獨的sqlite資料庫,用來執行sql語句

fmresultset

使用fmdatabase執行查詢後的結果集

fmdatabasequeue

用于在多線程中執行多個查詢或更新,它是線程安全的

3.打開資料庫

和c語言架構一樣,fmdb通過指定sqlite資料庫檔案路徑來建立fmdatabase對象,但fmdb更加容易了解,使用起來更容易,使用之前一樣需要導入sqlite3.dylib。打開資料庫方法如下:

<code>nsstring *path = [nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes).firstobject stringbyappendingpathcomponent:@</code><code>"person.db"</code><code>];</code>

<code>fmdatabase *database = [fmdatabase databasewithpath:path];    </code>

<code>if</code> <code>(![database open]) {</code>

<code>    </code><code>nslog(@</code><code>"資料庫打開失敗!"</code><code>);</code>

值得注意的是,path的值可以傳入以下三種情況:

具體檔案路徑,如果不存在會自動建立

空字元串@"",會在臨時目錄建立一個空的資料庫,當fmdatabase連接配接關閉時,資料庫檔案也被删除

nil,會建立一個記憶體中臨時資料庫,當fmdatabase連接配接關閉時,資料庫會被銷毀

4.更新

在fmdb中,除查詢以外的所有操作,都稱為“更新”, 如:create、drop、insert、update、delete等操作,使用executeupdate:方法執行更新:

<code>//常用方法有以下3種:   </code>

<code>- (bool)executeupdate:(nsstring*)sql, ...</code>

<code>- (bool)executeupdatewithformat:(nsstring*)format, ...</code>

<code>- (bool)executeupdate:(nsstring*)sql withargumentsinarray:(nsarray *)arguments</code>

<code>//示例</code>

<code>[database executeupdate:@</code><code>"create table if not exists t_person(id integer primary key autoincrement, name text, age integer)"</code><code>];   </code>

<code>//或者  </code>

<code>[database executeupdate:@</code><code>"insert into t_person(name, age) values(?, ?)"</code><code>, @</code><code>"bourne"</code><code>, [nsnumber numberwithint:42]];</code>

5.查詢

查詢方法也有3種,使用起來相當簡單:

<code>- (fmresultset *)executequery:(nsstring*)sql, ...</code>

<code>- (fmresultset *)executequerywithformat:(nsstring*)format, ...</code>

<code>- (fmresultset *)executequery:(nsstring *)sql withargumentsinarray:(nsarray *)arguments</code>

查詢示例:

<code>//1.執行查詢</code>

<code>fmresultset *result = [database executequery:@</code><code>"select * from t_person"</code><code>];</code>

<code>//2.周遊結果集</code>

<code>while</code> <code>([result next]) {</code>

<code>    </code><code>nsstring *name = [result stringforcolumn:@</code><code>"name"</code><code>];</code>

<code>    </code><code>int age = [result intforcolumn:@</code><code>"age"</code><code>];</code>

6.線程安全

在多個線程中同時使用一個fmdatabase執行個體是不明智的。不要讓多個線程分享同一個fmdatabase執行個體,它無法在多個線程中同時使用。 如果在多個線程中同時使用一個fmdatabase執行個體,會造成資料混亂等問題。是以,請使用 fmdatabasequeue,它是線程安全的。以下是使用方法:

建立隊列。

<code>fmdatabasequeue *queue = [fmdatabasequeue databasequeuewithpath:apath];</code>

使用隊列

<code>[queue indatabase:^(fmdatabase *database) {    </code>

<code>          </code><code>[database executeupdate:@</code><code>"insert into t_person(name, age) values (?, ?)"</code><code>, @</code><code>"bourne_1"</code><code>, [nsnumber numberwithint:1]];    </code>

<code>          </code><code>[database executeupdate:@</code><code>"insert into t_person(name, age) values (?, ?)"</code><code>, @</code><code>"bourne_2"</code><code>, [nsnumber numberwithint:2]];    </code>

<code>          </code><code>[database executeupdate:@</code><code>"insert into t_person(name, age) values (?, ?)"</code><code>, @</code><code>"bourne_3"</code><code>, [nsnumber numberwithint:3]];      </code>

<code>          </code><code>fmresultset *result = [database executequery:@</code><code>"select * from t_person"</code><code>];    </code>

<code>         </code><code>while</code><code>([result next]) {   </code>

<code>         </code><code>}    </code>

<code>}];</code>

而且可以輕松地把簡單任務包裝到事務裡:

<code>[queue intransaction:^(fmdatabase *database, bool *rollback) {    </code>

<code>             </code><code>while</code><code>([result next]) {   </code>

<code>             </code><code>}   </code>

<code>           </code><code>//復原</code>

<code>           </code><code>*rollback = yes;  </code>

<code>    </code><code>}];</code>

fmdatabasequeue 背景會建立系列化的g-c-d隊列,并執行你傳給g-c-d隊列的塊。這意味着 你從多線程同時調用調用方法,gdc也會按它接收的塊的順序來執行。