【iOS知識學習】_數據持久化

  • 文件系統
  • 歸檔和序列化
  • 數據庫

1.文件系統

不管是Mac OS X 還是iOS的文件系統都是建立在UNIX文件系統基礎之上的。

1.1 沙盒模型

在iOS中,一個App的讀寫權限只侷限於自己的沙盒目錄中。

沙盒模型到底有哪些好處呢?
安全:別的App無法修改你的程序或數據
保護隱私:別的App無法讀取你的程序和數據
方便刪除:因爲一個App所有產生的內容都在自己的沙盒中,所以刪除App只需要將沙盒刪除就可以徹底刪除程序了

iOS App沙盒中的目錄

  • App Bundle ,如xxx.app 其實是一個目錄,裏面有app本身的二進制數據以及資源文件
  • Documents, 存放程序產生的文檔數據
  • Library , 下面默認包含下面兩個目錄 Caches Preferences
  • tmp, 臨時文件目錄

如果我們想在程序中獲取上面某個目錄的路徑,應該如何實現呢? 下面就講講路徑的獲取, 通過NSPathUtilities.h中的NSSearchPathForDirectoriesInDomains函數,我們便可以獲取我們想要的路徑。 此函數具體聲明如下:

NSArray *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde); 
directory 目錄類型 比如Documents目錄 就是NSDocumentDirectory 
domainMask 在iOS的程序中這個取NSUserDomainMask 
expandTilde YES,表示將~展開成完整路徑

注意函數返回的類型爲數組,在iOS中一般這個數組中只包含一個元素,所以直接取lastObject即可。

1.2 NSFileManager

NSFileManager提供一個類方法獲得一個單例。

/* Returns the default singleton instance.*/ + (NSFileManager *)defaultManager;


下面羅列了NSFileManager的常用方法

  • 新建目錄
- (BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error;


createIntermediates這個參數一般爲YES,表示如果目錄路徑中間的某個目錄不存在則創建之,如果是NO的話,則要保證所創建目錄的父目錄都必須已經存在

  • 獲取目錄下的所有文件
- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;


如果目錄爲空,則返回空數組

  • 其他的一些方法
1       - (BOOL)copyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
2	- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
3	- (BOOL)linkItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
4	- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;

更多的可以查看文檔 NSFileManager Class Reference

在實際項目中,我們一般會寫一個工具類來負責項目中所有的路徑操作。

2. 歸檔(Archives) 和 序列化(Serializations)

我們經常聽到“序列化”,“反序列化”這樣的字眼,其實“序列化”的意思就是將對象轉換成字節流以便保存或傳輸,“反序列化”便是一個相反的過程,從字節流轉到對象。

在這節中涉及到一種文件類型plist,plist就是Property List 的縮寫,即所謂的屬性列表,屬性列表有兩種數據格式,一種是XML的,方便閱讀和編輯;另一種是二進制的,節省存儲空間,以及提高效率。

在Objective-C中這個對象和字節流的互轉分成兩類:

  • 歸檔 普通自定義對象和字節流之間的轉換
  • 序列化 某些特定類型(NSDictionary, NSArray, NSString, NSDate, NSNumber,NSData)的數據和字節流之間(通常將其保存爲plist文件)的轉換

不過本質上講上述兩種都是對象圖(Object Graph)和字節流之間的轉換. Apple關於序列化和歸檔的編程指南: Archives and Serializations Programming Guide 。

2.1 歸檔

如果我們需要將自定義的一個對象保存到文件,應該如何做呢? 
這裏引入兩個東西:一個是NSCoding協議 ;另一個是NSKeyedArchiver,NSKeyedArchiver其實繼承於NSCoder,可以以鍵值對的方式將對象的屬性進行序列化和反序列化。 
具體的過程可以這樣描述 通過NSKeyedArchiver 可以將實現了NSCoding協議的對象 和 字節流 相互轉換 。

像一些框架中的數據類型如NSDictionary,NSArray,NSString... 都已經實現了NSCoding協議,所以可以直接對他們進行歸檔操作。

這裏來一個比較完整的例子,一個Address類,一個User類,User類下有個Address類型的屬性。

Address類

<strong>@interface Address : NSObject<NSCoding>{
	    NSString *country;
	    NSString *city;
	}
	@property(nonatomic,copy) NSString *country;
	@property(nonatomic,copy) NSString *city;
	@end
	//////////////////////////////////////////////////////
	#import "Address.h"
	 
	@implementation Address
	@synthesize country;
	@synthesize city;
	 
	- (void)encodeWithCoder:(NSCoder *)aCoder{
	    [aCoder encodeObject:country forKey:@"country"];
	    [aCoder encodeObject:city forKey:@"city"];
	}
	 
	- (id)initWithCoder:(NSCoder *)aDecoder{
	    if (self = [super init]) {
	        [self setCountry:[aDecoder decodeObjectForKey:@"country"]];
	        [self setCity:[aDecoder decodeObjectForKey:@"city"]];
	    }
	    return self;
	}
	 
	@end</strong>
User類
<strong>#import <Foundation/Foundation.h>
#import "Address.h"
@interface User : NSObject<NSCoding>{
    NSString *_name;
    NSString *_password;

    Address *_address;
}
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *password;
@property(nonatomic,retain) Address *address;

@end
/////////////////////////////////////////////////////////
#import "User.h"

@implementation User
@synthesize name = _name;
@synthesize password = _password;
@synthesize address = _address;

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeObject:_password forKey:@"password"];
    [aCoder encodeObject:_address forKey:@"address"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        [self setName:[aDecoder decodeObjectForKey:@"name"]];
        [self setPassword:[aDecoder decodeObjectForKey:@"password"]];
        [self setAddress:[aDecoder decodeObjectForKey:@"address"]];
    }
    return self;
}
@end
</strong>
使用示例
<strong>Address *myAddress = [[[Address alloc] init] autorelease];
myAddress.country = @"中國";
myAddress.city = @"杭州";
 
User *user = [[[User alloc] init] autorelease];
user.name = @"盧克";
user.password = @"lukejin";
user.address = myAddress;

[NSKeyedArchiver archiveRootObject:user toFile:@"/Users/Luke/Desktop/user"];

id object = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/Luke/Desktop/user"];
NSLog(@"Object Class : %@",[object class]);</strong>
通過查看文件內容可以發現,保存的是plist的二進制數據格式。 轉成XML可以看到如下內容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>$archiver</key>
    <string>NSKeyedArchiver</string>
    <key>$objects</key>
    <array>
        <string>$null</string>
        <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>8</integer>
            </dict>
            <key>address</key>
            <dict>
                <key>CF$UID</key>
                <integer>4</integer>
            </dict>
            <key>name</key>
            <dict>
                <key>CF$UID</key>
                <integer>2</integer>
            </dict>
            <key>password</key>
            <dict>
                <key>CF$UID</key>
                <integer>3</integer>
            </dict>
        </dict>
        <string>盧克</string>
        <string>lukejin</string>
        <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>7</integer>
            </dict>
            <key>city</key>
            <dict>
                <key>CF$UID</key>
                <integer>6</integer>
            </dict>
            <key>country</key>
            <dict>
                <key>CF$UID</key>
                <integer>5</integer>
            </dict>
        </dict>
        <string>中國</string>
        <string>杭州</string>
        <dict>
            <key>$classes</key>
            <array>
                <string>Address</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>Address</string>
        </dict>
        <dict>
            <key>$classes</key>
            <array>
                <string>User</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>User</string>
        </dict>
    </array>
    <key>$top</key>
    <dict>
        <key>root</key>
        <dict>
            <key>CF$UID</key>
            <integer>1</integer>
        </dict>
    </dict>
    <key>$version</key>
    <integer>100000</integer>
</dict>
</plist>

2.2 序列化

在實際的項目中,我們一般是將NSDictionary或NSArray的對象保存到文件或者從文件讀取成對象。 當然這種只是適用於數據量不是很大的應用場景。 NSDictionary和NSArray 都有一個寫入文件的方法

- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;


NSDictionary和NSArray會直接寫成plist文件。

2.2.1 序列化的方式

序列化可以通過兩種途徑來進行

使用數據對象自帶的方法

寫文件

NSMutableDictionary *dataDictionary = [[[NSMutableDictionary alloc] init] autorelease];
 [dataDictionary setValue:[NSNumber numberWithInt:222] forKey:@"intNumber"];
 [dataDictionary setValue:[NSArray arrayWithObjects:@"1",@"2", nil] forKey:@"testArray"];
 [dataDictionary writeToFile:@"/Users/Luke/Desktop/test.plist" atomically:YES];

寫完的文件內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>intNumber</key>
    <integer>222</integer>
    <key>testArray</key>
    <array>
        <string>1</string>
        <string>2</string>
    </array>
</dict>
</plist>

從文件讀取

NSDictionary *dictionaryFromFile = [NSDictionary dictionaryWithContentsOfFile:@"/Users/Luke/Desktop/test.plist"];

使用NSPropertyListSerialization類

通過NSPropertyListSerialization類可以將數據對象直接轉成NSData或者直接寫到文件或者流中去.

NSMutableDictionary *dataDictionary = [[[NSMutableDictionary alloc] init] autorelease];
[dataDictionary setValue:[NSNumber numberWithInt:222] forKey:@"intNumber"];
[dataDictionary setValue:[NSArray arrayWithObjects:@"1",@"2", nil] forKey:@"testArray"];

NSString *error;
NSData *xmlData = [NSPropertyListSerialization dataFromPropertyList:dataDictionary
                                                           format:NSPropertyListXMLFormat_v1_0
                                                 errorDescription:&error];
if(xmlData) {
    NSLog(@"No error creating XML data.");
    [xmlData writeToFile:@"/Users/Luke/Desktop/test2.plist" atomically:YES];
}
else {
    if (error) {
        NSLog(@"error:%@", error);
        [error release];
    }
}

讀取
NSDictionary *dictionaryFromFile = (NSDictionary *)[NSPropertyListSerialization 
                                                       propertyListWithData:[NSData dataWithContentsOfFile:@"/Users/Luke/Desktop/test2.plist"] 
                                                        options:0
                                                        format:NULL
                                                        error:&error];

2.2.2 User Defaults

User Defaults 顧名思義就是一個用戶爲系統以及程序設置的默認值。每個用戶都有自己的一套數據,用戶和用戶之間沒法共享的。

我們都知道每一個程序都會保存一些設置數據,比如記住上次窗口的位置和大小,記住是否彈出某些提示信息等。蘋果提供了一個統一的解決方案,就是每一個app都有一個plist文件專門用以保存偏好設置數據。 plist文件名默認是程序Bundle identifier,擴展名爲plist.

除了程序自己的設置外,系統還有一些全局的或者其它的一些設置,也屬於User Defaults的範疇,User Defaults的持久化數據都保存在 ~/Library/Preferences 目錄中.

這裏有一點簡要的說一下,User Defaults 中存放的key value分放在多個Domain中,取的時候按一定的次序取查找,次序如下:

  • The Argument Domain 程序啓動的時候以參數的方式傳入的
  • The Application Domain 通過NSUserDefaults往裏面寫數據的時候默認就是寫到這個Domain的,通過Bundle identifier來標識
  • The Global Domain 用戶的全局的設置(系統的偏好設置)會放在這個Domain下,比如用戶的語言設置,滾動條的設置等,裏面的設置會對所有的程序起作用。
  • The Languages Domains
  • The Registration Domain 這個domain裏面的key value是提供默認值的,一般會在程序啓動的設置進行設置,他們都不會被持久化到文件的。當某個key對應的值在上面的那些domain中都不存在的時候,就到這裏找。

Mac系統還爲user defaults提供了很好的命令行工具,defaults 你可以通過下面的方式查看具體使用方式

man defaults

可以通過defaults domains查看當前用戶的所有的domain,通過 defaults read NSGlobalDomain 讀取 The Global Domain 中的所有值。

NSUserDefaults 類來讀寫Preferences設置,而無需考慮文件位置等細節問題。

NSUserDefaults 用起來和 NSDictionary 很相似,多了一個Domain的概念在裏面。NSUserDefaults 一樣提供了一個獲取單例的方法.

+ (NSUserDefaults *)standardUserDefaults

NSUserDefaults提供了一系列的接口來根據key獲取對應的value,搜索的次序按照上面提及到的次序在各個Domain中進行查找。還提供了一系列的 Setting Default Values的方法,這些設置的值都是在 The Application Domain 下的.當然也提供了修改其他Domain下的值的方法,只是需要整體的設置。

3.數據庫

Mac上自帶安裝了SQLite3 ,如果你之前接觸過關係型數據庫,你可以通過命令行來對SQLite進行初步的認識

$ sqlite3 test.db
SQLite version 3.7.5
Enter ".help" for instructions
Enter SQL statements terminated with a ";" sqlite>create table if not exists names(id integer primary key asc, name text); 
sqlite> insert into names(name) values('Luke');
sqlite> select * from names;
1|Luke
sqlite>

那如果在代碼中使用SQLite呢?

  • 添加sqlite的動態鏈接庫 libsqlite3.0.dylib
  • 引入頭文件 #import "sqlite3.h"

這樣之後你便可以通過C的接口來操作數據庫了

qlite3 *database;//sqlite3的類型其實只是一個結構體struct
NSArray *documentsPaths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory 
                                                         , NSUserDomainMask 
                                                         , YES); 
NSString *databaseFilePath=[[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:@"luke.db"];

//打開數據庫
if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) { 
    NSLog(@"open sqlite db ok."); 
    char *errorMsg; 
    const char *createSql="create table if not exists names (id integer primary key asc,name text)";
    //創建表
    if (sqlite3_exec(database, createSql, NULL, NULL, &errorMsg)==SQLITE_OK) { 
        NSLog(@"create ok."); 
    }else {
        NSLog(@"error: %s",errorMsg); 
        sqlite3_free(errorMsg);
    }
    
    
    //插入數據
    const char *insertSql="insert into names (name) values(\"Luke\")"; 
    if (sqlite3_exec(database, insertSql, NULL, NULL, &errorMsg) == SQLITE_OK) { 
        NSLog(@"insert ok."); 
    }else {
        NSLog(@"error: %s",errorMsg); 
        sqlite3_free(errorMsg); 
    }
    
    
    const char *selectSql="select id,name from names"; 
    sqlite3_stmt *statement; 
    if (sqlite3_prepare_v2(database, selectSql, -1, &statement, nil) == SQLITE_OK) { 
        NSLog(@"select ok.");
    }
    
    while (sqlite3_step(statement)==SQLITE_ROW) { 
        int _id=sqlite3_column_int(statement, 0); 
        char *name=(char *)sqlite3_column_text(statement, 1); 
        NSString *nameString = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
        NSLog(@"row>>id %i, name %@",_id,nameString); 
    }
    
    sqlite3_finalize(statement);
    
}

sqlite3_close(database);

你會發現這完全是C語言編程,和Objective-C的代碼混在一起格格不入,也很不方便,所以便有人開發了開源的sqlite c接口的wrapper

具體的使用方法,各自的文檔都寫的比較清楚。 FMDB不支持多線程同時使用同一個數據庫連接進行操作,否則會有線程安全問題,有可能導致數據庫文件損壞。EGODatabase則引入了多線程的支持,部分代碼借鑑了FMDB,兩者在使用上非常的相似。另EGODatabase提供了異步數據庫操作的支持,將數據庫操作封裝成數據庫請求(其繼承於NSOperation),數據庫請求創建好了,丟到一個OperationQueue中被異步的進行執行,當請求數據完成之後 ,相應的delegate方法會被調用,然後你可以在主線程更新顯示了.




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章