天天看點

iOS資料存儲的幾種方式【plist、序列化/反序列化、sqlite3、FMDB】

文章目錄

        • 沙盒
          • Documents
          • Library
          • tmp
          • NSFileManager
          • NSBundle
        • NSUserDefaults
        • 歸檔解檔
        • sqlite3
  • 資料存儲
資料存儲本質就是運作時的對象儲存在檔案、資料庫中。資料存儲可以分為兩步:首先是将對象轉換成二進制資料,這一步也叫序列化;相反,将二進制資料轉換成對象則稱為反序列化;然後是考慮二進制資料如何儲存和讀取。
  • 沙盒

iOS系統為每個App配置設定了獨立的資料目錄,App隻能對自己的目錄進行操作,這個目錄所在被稱為沙盒目錄。

一個應用的沙盒包括下面三個部分:應用目錄、沙盒目錄、iCloud目錄。

iOS資料存儲的幾種方式【plist、序列化/反序列化、sqlite3、FMDB】
  • Documents

用于儲存App的資料,包括App運作時需要的各類檔案以及使用者的資料等。Documents檔案夾可以在連接配接iTunes時選擇備份,通常Documents目錄用來存放可以對外的檔案。

  • Library

用來儲存不對外的資料

  • Library/Caches 用來儲存不對外的資料,存放程式運作時的緩存檔案。比如通過網絡請求下載下傳的圖檔放到Caches中,再用到這個圖檔時不用再請求,直接加載。不可被iTunes備份。空間不足時可能會被iOS系統删除。
  • Library/Preferences 通常用于儲存使用者的設定等資訊,比如我們常用的NSUserDefaults類就會以plist的方式儲存在該目錄中,可被iTunes備份。
  • tmp

目錄用來儲存不重要的臨時檔案,在系統重新開機後會被清空。不會被iTunes備份。

- (void)sandbox:(NSString *)file writeData:(NSArray *)arr {
/**
 資料存儲到沙盒
 */
       //初始化一個資料
       //NSArray *array = @[@"data", @"storage"];
       //2、擷取沙盒根目錄
       NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];//數組裡的第一個,不止一個
       //3、拼接檔案名
       NSString *filePath = [path stringByAppendingPathComponent:file];
       //4、寫入
       [arr writeToFile:filePath atomically:YES];//是否原子性,YES表示寫入成功之後再生成檔案,NO表示不管寫入成功與否都生成檔案。
}

- (NSArray *)sandboxReadFrom:(NSString *)file {
    /**
     從沙盒中取資料
     */
    //擷取檔案路徑
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];//數組裡的第一個,不止一個
    NSString *filePath = [path stringByAppendingPathComponent:file];
    NSArray *arr = [NSArray arrayWithContentsOfFile:filePath];
    NSLog(@"%@-----%@", filePath, arr);
    return arr;
}
           
  • NSFileManager

系統提供了NSFileManager類給開發去讀取沙盒目錄中的檔案。

NSFileManager是單例,通過defaultManager方法可以擷取:

拿到fileManager就可以判斷檔案是否存在,并且傳回是檔案還是檔案夾:

周遊檔案夾:

複制或者移動檔案:

[fileManager copyItemAtPath:sourceFilePath toPath:targetFilePath error:nil];
[fileManager moveItemAtPath:sourceFilePath toPath:targetFilePath error:nil];
           

更詳細的API可以自行檢視

NSFileManager.h

檔案。

  • NSBundle

在用NSFileManager去讀取檔案的時候需要提供檔案路徑,但是有時候我們并不知道資源被放置在哪個目錄,此時可以用到NSBundle。

在Xcode編譯運作的時候,會把Xcode内的圖檔、xib、音頻等都拷貝到.app檔案中。

NSBundle就是系統提供,用來讀取這些資源的類。

這樣我們就拿到我們的mainBundle,通過mainBundle我們可以查找對應的資源:

也可以通過mainBundle直接加載xib:

通過CocoaPods安裝的Pod庫,要如何讀取其資源

NSString *path = [[NSBundle mainBundle] pathForResource:@"SSTestPod" ofType:@"bundle"];
NSBundle *podBundle = [NSBundle bundleWithPath:path];
           
  • NSUserDefaults

[NSUserDefaults standardUserDefaults] 是一個單例,通常用來存儲使用者的配置資訊。本質其實就是一個字典類型的plist檔案,不用關心路徑,自動儲存到Library下的Prefrences。

- (void)userDefaultWrite {
    /**
        它的本質其實就是一個字典類型的plist檔案,不用關心路徑,自動儲存到Library下的Prefrences,單例
        */
       [[NSUserDefaults standardUserDefaults] setObject:@"objcet" forKey:@"key"];
       //[[NSUserDefaults standardUserDefaults] synchronize];//立即存儲,iOS7之後不需要
}

- (void)userDefaultRead {
    //key要與寫入時的key相同
    NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@"key"];
    NSLog(@"NSUserDefaults------%@", obj);
}
           
  • 歸檔解檔

建立一個MKKeyedArchiver檔案,頭檔案内容

@interface MKKeyedArchiver : NSObject<NSCoding, NSSecureCoding>//遵守協定

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

           

控制器中使用歸檔:

- (void)archiver {
    /**
     plist檔案不能存儲自定義對象(NSUserDefaults存儲本質上也是plist檔案,是以也不能存儲)
     */
    
    //存儲在Library下的Caches目錄下
    NSString *pa = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *fiPa = [pa stringByAppendingPathComponent:@"archiver.data"];
    
    MKKeyedArchiver *archiver = [[MKKeyedArchiver alloc] init];
    archiver.name = @"喬巴";
    archiver.age = 5;
    
    //[NSKeyedArchiver archiveRootObject:archiver toFile:fiPa];//棄用
    //iOS 12新api,MKKeyedArchiver遵守NSSecureCoding協定,并且+(Bool)supportsSecureCoding傳回yes
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:archiver requiringSecureCoding:YES error:nil];
    [data writeToFile:fiPa atomically:YES];
    
    NSLog(@"====%@", fiPa);
}
           
- (void)unarchiver {
    NSString *pa = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *fiPa = [pa stringByAppendingPathComponent:@"archiver.data"];
    //MKKeyedArchiver *archiver = [NSKeyedUnarchiver unarchiveObjectWithFile:fiPa];棄用
    NSData *data = [NSData dataWithContentsOfFile:fiPa];
    MKKeyedArchiver *archiver = (MKKeyedArchiver *)[NSKeyedUnarchiver unarchivedObjectOfClass:[MKKeyedArchiver class] fromData:data error:nil];
    NSLog(@"====%@-----%ld", archiver.name, (long)archiver.age);
}
           
  • sqlite3

SQLite3是一款輕型的關系型資料庫,在移動端中廣泛應用。

SQLite3基于C語言實作,OC可以直接相容,iOS系統也自帶了SQLite3,提供的方法是直接操作資料庫。

  • 建立自定義表
  • sqlite的使用步驟主要就是
  • 擷取資料庫路徑
  • 打開資料庫(sqlite3_open)、檢視是否存在需要的表,沒有建立(sqlite3_exec可以執行任何SQL語句,但是一般不用它執行查詢語句,因為它不會傳回查詢到的資料)
  • 對資料操作:增、删、改、查
  • 關閉資料庫(sqlite3_close)

工程中Link Binary With Libraries引入libsqlite3.tbd,并且導入頭檔案#import <sqlite3.h>。

create table 表名 (字段名1 字段類型1, 字段名2 字段類型2, …) ;

create table if not exists 表名 (字段名1 字段類型1, 字段名2 字段類型2, …) ;

建立一個表,表名叫t_students,一個text類型的name和integer 類型的age

- (void)create {
    NSString *document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *path = [document stringByAppendingPathComponent:@"sql.data"];
    //OC轉C字元串
    const char *filePaht = path.UTF8String;
    //打開資料庫檔案, 如果資料庫檔案不存在,這個函數就會自動建立資料庫檔案
    int result = sqlite3_open(filePaht, &dataBase);
    if (result == SQLITE_OK) {
        NSLog(@"打開成功=======建表位置%@", path);
        
        //建表
        /**
         @para 資料庫對象
         @para sql語句
         @para 寫入的回調
         @para 寫入回調的第一個參數
         @para 錯誤資訊
         sqlite3_exec(<#sqlite3 *#>, <#const char sql#>, <#int (callback)(void *, int, char **, char **)#>, <#void *#>, <#char **errmsg#>)
         */
        const char  *sql = "CREATE TABLE IF NOT EXISTS t_students (id integer PRIMARY KEY AUTOINCREMENT,name text NOT NULL,age integer NOT NULL);";
        char *error;
        
        int status = sqlite3_exec(dataBase, sql, NULL, NULL, &error);
        if (status == SQLITE_OK) {
            NSLog(@"建表成功!");
        }else {
            sqlite3_close(dataBase);
            NSAssert(NO, @"建表失敗");
        }
        
    }else {
        sqlite3_close(dataBase);
        NSAssert(NO,@"資料庫打開失敗");
    }
}
           
  • FMDB

FMDB對SQLite資料庫進行封裝,開放OC的接口便于開發者接入,是很普遍使用的iOS第三方資料庫。

GitHub倉庫位址,也可以使用pod接入。

三個核心類:

1、FMDatabase:表示一個SQLite資料庫,用于執行sql語句;

2、FMResultSet:FMDatabase執行查詢得到的結果集;

3、FMDatabaseQueue:多線程用的查詢或更新隊列;

FMDB的使用:

FMDatabase *db = [FMDatabase databaseWithPath:path]; // create db
[db open]; // open
// create table
NSString *createSqlStr = @"create table if not exists test_table_name(id integer primary key autoincrement,test_name_key char)";
[db executeUpdate:createSqlStr];
// insert table
NSString *insertSqlStr = @"insert into test_table_name(test_name_key) values('anyname')";
[db executeUpdate:insertSqlStr];
                

sql還可以使用?參數,然後在執行的時候填寫具體的值:

NSString *insertSqlStr2 = @"insert into test_table_name(test_name_key) values(?)";
[db executeUpdate:insertSqlStr2, @"another_name"];
                

查詢也很友善,可以結合FMDatabaseQueue來看:

FMDatabaseQueue *sqlQueue = [FMDatabaseQueue databaseQueueWithPath:path];
[sqlQueue inDatabase:^(FMDatabase * _Nonnull db) {
    NSString *selectSqlStr = @"select id, test_name_key FROM test_table_name";
    FMResultSet *result = [db executeQuery:selectSqlStr];
    while ([result next]) {
        int value_id = [result intForColumn:@"id"];
        NSString *value_name = [result stringForColumn:@"test_name_key"];
        NSLog(@"id:%d, name:%@", value_id, value_name);
    }
}];

           

FMDatabaseQueue是使所有操作都在同一個隊列進行,避免多線程操作資料庫,引起資料異常。

CoreData

如果不想使用第三方庫,也可以使用iOS系統提供的CoreData架構。

CoreData的接口更加簡化,部分可視化操作,對象代碼自動生成等。

表結構(可視化操作,代碼生成):

根據這個表結構,先選中CoreData的模型檔案,在Xcode的Editor有Create NSManagedObject Subclass的選項,選中後會自動生成類的代碼,如下:

@interface User (CoreDataProperties)
+ (NSFetchRequest<User *> *)fetchRequest;
@property (nonatomic) int16_t gender;
@property (nullable, nonatomic, copy) NSString *name;
@end

           

CoreData的具體使用:

//從本地加載對象模型
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"LearnCoreData" ofType:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]];
// 建立沙盒中的資料庫
NSPersistentStoreCoordinator* coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"database.sqlite"];
[coord addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:path] options:nil error:nil];
// 資料庫關聯緩存
NSManagedObjectContext* objContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
objContext.persistentStoreCoordinator = coord;

           

資料的插入操作:

// 資料插入
User *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:objContext];
user.name = [NSString stringWithFormat:@"name_%d", arc4random_uniform(100)];
user.gender = arc4random_uniform(2);
NSError *error;
[objContext save:&error];

           

資料查詢操作:

NSFetchRequest *fetch = [[NSFetchRequest alloc] initWithEntityName:@"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"gender=1"]; //查詢條件
fetch.predicate = predicate;
NSArray *results = [objContext executeFetchRequest:fetch error:nil];
for (int i = 0; i < results.count; ++i) {
    User *selectedUser = results[i];
    NSLog(@"name…:%@", selectedUser.name);
}

           

配合前面所學的知識,我們從沙盒可以導出項目中實際使用的資料庫。

用SQLPro for SQLite打開,就可以看到裡面的具體資訊:(這在分析競品的時候很有用)

Keychain

從上文我們可以知道,儲存在沙盒目錄的資料也是不安全的,使用者可能會導出沙盒資料進行分析。

有沒有什麼儲存方式是更安全的呢?

iOS給出的答案是keychain。

keychain是iOS提供給App存儲敏感和安全相關資料用的工具。keychain同樣會被iTunes備份,即使App重裝仍能讀取到上次儲存的結果。為了保證資料安全,keychain内的資料都是經過加密。

keychain的使用

1、打開keychain的開關。

2、

import <Security/Security.h>

3、使用API;

// SELECT
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
// ADD
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
// UPDATE
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
// DELETE
OSStatus SecItemDelete(CFDictionaryRef query);

           

這些api非常不友好,幸好蘋果官方有提供demo,第三方開發者也有人嘗試去封裝這些接口,我們以

KeychainWrapper為例,來看看封裝後更簡單的接口。

- (void)savePassword:(NSString *)password;
- (BOOL)deleteItem;
           

- (NSString )readPassword;

//傳回目前accessGroup下的service的所有Keychain Item

+ (NSArray )passwordItemsForService:(NSString )service accessGroup:(NSString )accessGroup;

比之前更加貼近OC的文法。

具體的使用樣例:

KeychainWrapper *wrapper = [[KeychainWrapper alloc] initWithSevice:kKeychainService account:self.account accessGroup:kKeychainAccessGroup];
NSString *saveStr = [wrapper readPassword];
if (!saveStr) {
    [wrapper savePassword:@"test_password"];
}
NSLog(@"saveStr:%@", saveStr);

           

隻要儲存在keychain,即使應用解除安裝重裝,仍舊能讀取到該值。

具體的邏輯可見GitHub。

對象序列化

前面介紹了各種存儲的工具,那麼如何把運作中的對象序列化成第三方庫呢?

有的開發者會使用系統提供的NSCoding協定手動添加字段,有的開發者會使用Runtime自動實作NSCoding,有的開發者會使用成熟的第三方庫(例如YYModel),下面分别介紹這幾種序列化的方式。

NSCoding是系統提供的序列化協定,在對象轉換為二進制的時候,會通過NSCoding的方法回調開發者。

@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end

           

使用樣例:

@property (nonatomic, assign) NSInteger gender;

@property (nonatomic, strong) NSString *userName;

- (instancetype)initWithCoder:(NSCoder *)aDecoder {

self = [super init];

self.gender = [[aDecoder decodeObjectForKey:@“gender”] integerValue];

self.userName = [aDecoder decodeObjectForKey:@“userName”];

return self;

}

- (void)encodeWithCoder:(NSCoder *)aCoder {

[aCoder encodeObject:@(self.gender) forKey:@“gender”];

[aCoder encodeObject:self.userName forKey:@“userName”];

}

@end

上面的方式随着屬性增多,代碼越來越臃腫,于是有的開發者便利用Runtime的特性,讀取類的屬性名,自動完成這個過程。

随着iOS的社群發展,有一個序列化的第三方庫脫穎而出,那就是YYModel。

YYModel具有幾大特點:

1、利用iOS的Runtime特點,無需繼承;

2、安全轉換資料類型,常見Crash都進行了保護;

3、擴充性強,提供多種容器擴充;

YYModel的使用:

1、安裝Pod庫,

pod 'YYModel'

2、

import<NSObject+YYModel.h>

在對象添加YYModel的聲明。

@property (nonatomic, assign) NSInteger gender;

@property (nonatomic, strong) NSString *userName;

@end

3、将字典轉換會對象;

NSDictionary *dic = @{
                  @"gender":@0,
                  @"userName": @"test_name",
                    };
SSUser *user = [SSUser modelWithDictionary:dic];

           

YYModel還提供豐富的特性,比如說自定義屬性名映射、容易類型轉換、自定義類的資料映射。

以自定義屬性名映射為例:

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"userName":@"name"};
}

           

YYModel原理和更多進階使用技巧可以見GitHub。

總結

iOS的本地資料存儲,其實就是記憶體資料的序列化和反序列化。

通常我們的資料都會儲存在沙盒目錄中,讀取的時候可以直接指定路徑,也可以用NSFileManager去查找和周遊目錄;我們工程中的資源檔案會存在應用目錄,需要用NSBundle去讀取。

APP在運作過程中,有時候需要臨時儲存一些變量,在下次運作時讀取,此時可以用輕量級的持久化工具NSUserDefault,如果資料量比較大則需要考慮使用資料進行存儲。SQLite3是iOS中最常用的資料庫,通常我們會第三方封裝庫FMDB來操作,簡化代碼邏輯。

如果涉及到安全相關的敏感資料,則不應該儲存在檔案、資料庫等可以被抓取的地方。此時可以使用iOS提供的keychain對敏感資料進行儲存。keychain的資料是經過加密處理,具有較高的安全性。

在将對象轉換成二進制資料,以及将二進制資料轉換成對象時,可以使用系統提供的NSCoding協定,也可以使用第三方庫YYModel。

所有代碼GitHub可見,位址。

CoreData注意事項

在生成代碼的時候,可能會如下的提示:

看詳細的編譯錯誤并沒有額外的資訊,仍是符号沖突。

duplicate symbol _OBJC_CLASS_$_CDUser in: 
    /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnDatabase-dkstmlwuljogjqbnffnrdaqurvyv/Build/Intermediates.noindex/LearnDatabase.build/Debug-iphonesimulator/LearnDatabase.build/Objects-normal/x86_64/CDUser+CoreDataClass.o 
duplicate symbol _OBJC_METACLASS_$_CDUser in: 
    /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnDatabase-dkstmlwuljogjqbnffnrdaqurvyv/Build/Intermediates.noindex/LearnDatabase.build/Debug-iphonesimulator/LearnDatabase.build/Objects-normal/x86_64/CDUser+CoreDataClass.o 
ld: 2 duplicate symbols for architecture x86_64 
clang: error: linker command failed with exit code 1 (use -v to see invocation) 

           

但是在工程中,僅僅隻有一個CDUser+CoreDataProperties.m,并沒有其他CDUser的類。

嘗試把CDUser+CoreDataProperties.m從compile source中移除,工程中仍保留CDUser+CoreDataProperties.h檔案,結果編譯可以通過。

檢查工程的build settings也沒有有用的資訊,最後打開DerivedData中找到對應的目錄,結果找到下面的CoreDataGenerated檔案夾:

從名字上可以得知,這也是CoreData自動生成!

經過一番搜尋,終于找到CoreData對應的設定。

附錄

蘋果官方文檔-File System Programming Guide