iOS 資料持久化的幾種方法

Oranges發表於2017-12-13

簡介

持久化就是將資料儲存到硬碟中,讓APP重啟後可以使用之前儲存的資料.在iOS開發中,可能會用到一下幾種

  1. plist檔案:屬性列表
  2. preference:偏好設定
  3. NSKeyedArchiver:歸檔
  4. keychain:鑰匙串

沙盒

在介紹儲存方法之前,先說下沙盒機制.iOS程式預設情況下只能訪問程式的目錄,這個目錄就是沙盒。 沙盒的目錄結構如下:

  • 應用程式包:存放的是應用程式的原始檔:資原始檔和可執行檔案
NSString *path = [[NSBundle mainBundle] bundlePath];
複製程式碼
  • Documents:比較常用的目錄,itune同步會同步這個資料夾的內容,適合儲存重要的資料
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
複製程式碼
  • Library:
    1.Caches:tunes不會同步此資料夾,設個儲存體積大,不需要備份的重要資料
NSString *path  = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
複製程式碼
  • tmpitunes不會同步,系統可能在應用沒執行時就刪除該目錄的檔案,適合存臨時檔案,用完就刪除:
NSString *path  = NSTemporaryDirectory();
複製程式碼

一、plist檔案(序列化)

plist檔案是通過XML檔案的方式儲存在目錄中 以下型別可以被序列化:

NSString;//字串
NSMutableString;//可變字串
NSArray;//陣列
NSMutableArray;//可變陣列
NSDictionary;//字典
NSMutableDictionary;//可變字典
NSData;//二進位制資料
NSMutableData;//可變二進位制資料
NSNumber;//基本資料
NSDate;//日期
複製程式碼

這裡我們就用NSDictionary當例子,其他的型別和這個方法類似;

/**
 寫入資料
 */
-(void)writeToPlist:(NSDictionary *)dict plistName:(NSString *)plistName{
    //存取路徑
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    //路徑中檔名
    NSString *filePath = [path stringByAppendingPathComponent:plistName];
    //序列化,把資料存入指定目錄的plist檔案
    [dict writeToFile:filePath atomically:YES];
}
/**
 根據plist檔名讀取資料
 */
-(NSDictionary *)readFromPlistWithPlistName:(NSString *)plistName{
    //存取路徑
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    //路徑中檔名
    NSString *filePath = [path stringByAppendingPathComponent:plistName];
    NSDictionary *resultDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    return resultDict;
}
複製程式碼

atomically是否先寫入輔助檔案,增加安全性的寫入檔案方法,一般都是YES

圖1.png

二、preference:偏好設定

很多iOS應用都支援偏好設定,比如儲存使用者名稱、密碼、字型等設定。每個應用都有NSUserDefaults例項,通過它來讀取偏好設定。一般不要在偏好設定中儲存其他資料。
偏好設定是key-value的方式存取和讀取的。 使用方法:

/**
 儲存資料
 */
- (IBAction)saveDate:(UIButton *)sender {
    //獲得NSUserDefaults檔案
    NSUserDefaults *userDefaultes = [NSUserDefaults standardUserDefaults];
    //向偏好設定中寫入內容
    [userDefaultes setObject:@"lcf" forKey:@"name"];
    [userDefaultes setInteger:26 forKey:@"age"];
    [userDefaultes setObject:@"boy" forKey:@"sex"];
    //立即同步設定
    [userDefaultes synchronize];
}
/**
 讀取資料
 */
- (IBAction)readDate:(UIButton *)sender {
    //獲得NSUserDefaults檔案
    NSUserDefaults *userDefaultes = [NSUserDefaults standardUserDefaults];
    //讀取偏好設定
    NSString *name = [userDefaultes objectForKey:@"name"];
    NSInteger age = [userDefaultes integerForKey:@"age"];
    NSString *sexStr = [userDefaultes objectForKey:@"sex"];
    NSLog(@"\n%@\n%ld\n%@",name,age,sexStr);
}
複製程式碼

注意事項:

  • 如果沒有呼叫synchronize方法,系統會根據I/O情況不定時刻地儲存到檔案中。所以如果需要立即寫入,就必須呼叫synchronize方法。
  • 偏好設定會將所有資料儲存到同一個資料夾,使用同一個key,會把之前儲存的資料覆蓋。

三、NSKeyedArchiver:歸檔和解檔

歸檔在iOS中是另一種形式的序列化,只要遵循了NSCoding協議的物件都可以通過它來實現序列化。由於大多是類都遵循了NSCoding協議,因此,對於大多數類來說,歸檔是比較容易實現的。

  1. 遵循NSCoding協議,其中有2個方法是必須實現的:
    1. initWithCoder
    2. encodeWithCoder
//遵循NSCoding協議
@interface DWSave : NSObject<NSCoding>
/**name*/
@property(nonatomic ,copy)NSString *name;
/**age*/
@property(nonatomic ,assign)NSInteger age;
/**sex*/
@property(nonatomic ,assign)BOOL sex;
@end
//以上內容要寫在.h檔案中
@implementation DWSave
//歸檔
-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInteger:self.age forKey:@"age"];
    [aCoder encodeBool:self.sex forKey:@"sex"];
}
//解檔
-(instancetype)initWithCoder:(NSCoder *)aDecoder;{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
        self.sex = [aDecoder decodeBoolForKey:@"sex"];
    }
    return self;
}
@end
複製程式碼

NSKeyedArchiver歸檔

//儲存地址
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    //檔名
    NSString *filePath = [path stringByAppendingPathComponent:@"wyp.data"];
    DWSave *save = [[DWSave alloc] init];
    //設定資料
    save.name = @"lcf";
    save.age = 26;
    save.sex = 1;
    [NSKeyedArchiver archiveRootObject:save toFile:filePath];
複製程式碼

NSKeyedUnarchiver解檔

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *filePath = [path stringByAppendingPathComponent:@"wyp.data"];
    DWSave *save = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    if (save) {
        NSLog(@"\n%@\n%ld\n%d",save.name,save.age,save.sex);    
    }
複製程式碼

必須要遵循NSCoding協議 儲存檔案的副檔名可以自定義 如果需要歸檔的類是某個自定義類的子類時,就需要在歸檔和解檔之前實現父類的歸檔和解檔方法。[super encodeWithCoder:aCoder][super initWithCoder:aDecoder]方法。

keychain:鑰匙串

通常情況下,我們使用NSUserDefaults儲存資料資訊,但是對於一些私密資訊,但是對於一下比較私密的資訊,如帳號、密碼等等,我們就需要使用更為安全的keychain了。keychain儲存的資訊是儲存在沙盒之外的,不會因App的刪除而丟失,在使用者重新安裝了App後依然存在。其實可以把keychain理解成一個Dictionary,所有資料都以key-value的形式儲存,可以對這個Dictionary進行add、update、get、delete這四個操作。對一個應用來說,keychain都有兩個訪問區,私有和公共。

  1. Target - Capabilities - Keychain Sharing - ON
    圖2.png
    左側的目錄會自動生成Entitlements檔案,不需要自己建立了。
  2. 引入Security.framework
  3. 自定義一個類,取名keychain,如下:

.h檔案

#import <Foundation/Foundation.h>
@interface keychain : NSObject
/**新增*/
+(void)savePassWord:(NSString *)password;
/**讀取*/
+(id)loadPassWord;
/**刪除*/
+(void)deletePassword;
@end
複製程式碼

.m檔案

#import "keychain.h"
#import <Security/Security.h>

@implementation keychain
static NSString *const KEY_KEYCHAIN = @"LCF";
static NSString *const KEY_PASSWORD = @"PASSWORD";
/**新增*/
+(void)savePassWord:(NSString *)password{
    NSMutableDictionary *infoDict = [NSMutableDictionary dictionary];
    [infoDict setObject:password forKey:KEY_PASSWORD];
    [keychain save:KEY_KEYCHAIN data:infoDict];
}
/**讀取*/
+(id)loadPassWord{
    NSMutableDictionary *infoDict = [keychain load:KEY_KEYCHAIN];
    return [infoDict objectForKey:KEY_PASSWORD];
}
/**刪除*/
+(void)deletePassword{
    [self delete:KEY_KEYCHAIN];
}
+(NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kSecClassGenericPassword,(id)kSecClass,
            service, (id)kSecAttrService,
            service, (id)kSecAttrAccount,
            (id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
            nil];
}
+(void)save:(NSString *)service data:(id)data {
    //Get search dictionary
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    //Delete old item before add new item
    SecItemDelete((CFDictionaryRef)keychainQuery);
    //Add new object to search dictionary(Attention:the data format)
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    //Add item to keychain with the search dictionary
    SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
+(id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    //Configure the search setting
    //Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
        } @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        } @finally {
        }
    }
    if (keyData)
        CFRelease(keyData);
    return ret;
}
+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end
複製程式碼

kechain部分程式碼摘自網路

結束語: 文章中可能會存在錯誤,歡迎 指正。 Demo

相關文章