文章目錄
-
-
-
- 沙盒
-
- Documents
- Library
- tmp
- NSFileManager
- NSBundle
- NSUserDefaults
- 歸檔解檔
- sqlite3
-
-
- 資料存儲
資料存儲本質就是運作時的對象儲存在檔案、資料庫中。資料存儲可以分為兩步:首先是将對象轉換成二進制資料,這一步也叫序列化;相反,将二進制資料轉換成對象則稱為反序列化;然後是考慮二進制資料如何儲存和讀取。
-
沙盒
iOS系統為每個App配置設定了獨立的資料目錄,App隻能對自己的目錄進行操作,這個目錄所在被稱為沙盒目錄。
一個應用的沙盒包括下面三個部分:應用目錄、沙盒目錄、iCloud目錄。
-
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