iOS 数据持久化

iOS的数据持久化的主要几种方法:

  1. NSUserDefaults
  2. KeyChain Services
  3. Archive归档/解档
  4. SQLite数据库(Core Data底层也是由SQLite实现)

对于一些轻量级的数据保存,我们通常都使用NSUserDefaults或KeyChain,需求对应场景也相对较为简单例如:保存用户输入的账号密码、服务器鉴权Token、UUID等。个人感觉苹果爸爸对有些数据持久化方法提供的API非常不友好。

1.NSUserDefaults

NSUserDefaults是一个单例类,通过[NSUserDefaults standardUserDefaults]获取其实例对象,支持存储的对象类型有:NSNumber、NSString、NSDate、NSArray、NSDictionary、BOOL
NSUserDefaults保存的数据依赖App沙盒,一旦App被删除原先NSUserDefaults中保存的数据也会随之被清除,在平时开发中需要注意

例如我们如果只需要保存一个NSString字符串数据时,使用起来就非常方便,因为NSUserDefaults本身就支持该类型存储:

+ (void)setString:(NSString*)str key:(NSString*)keyWord{
    NSUserDefaults *user=[NSUserDefaults standardUserDefaults];
    [user setObject:str forKey:keyWord];
    [user synchronize];
}

+ (NSString *)getStringBy:(NSString*)keyWord{
    NSUserDefaults *user=[NSUserDefaults standardUserDefaults];
    id obj=[user objectForKey:keyWord];
    return obj;
}

但是实际开发中我们大多数时候使用到的都是自定义类型,这时我们就需要将其实例对象转换成NSData类型,在这之前还要实现NSCoding协议,下面是NSUserDefaults在保存自定义类型数据时set和get方法的具体实现:

+ (void)setObject:(id)obj key:(NSString*)keyWord{
    NSUserDefaults *user=[NSUserDefaults standardUserDefaults];
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:obj];
    [user setObject:data forKey:keyWord];
    [user synchronize];
}

+ (id)getObjectBy:(NSString*)keyWord{
    NSUserDefaults *user=[NSUserDefaults standardUserDefaults];
    id obj=[user objectForKey:keyWord];
    if (obj && [obj isKindOfClass:[NSData class]]) {
        id file=[NSKeyedUnarchiver unarchiveObjectWithData:obj];
        return file;
    }
    return nil;
}

2.KeyChain Services

keychain其实是一个数据库,对于iOS每个设备只有一个keychain,因此它本身是跟设备绑定的,所以在同一个iCloud帐号下keychain是不变的,这样一来对开发者而言最大的好处就是数据是保存在沙盒之外的,即使用户删除App,并不会影响到先前保存的数据。

#pragma mark public method
+ (void)setObject:(id)obj key:(NSString*)keyWord{
    
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:keyWord];
    SecItemDelete((CFDictionaryRef)keychainQuery);
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:obj] forKey:(id)kSecValueData];
    SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

+ (id)getObjectBy:(NSString*)keyWord{
    
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:keyWord];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    
    unsigned long t = SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData);
    if (t == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
        } @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", keyWord, e);
        } @finally {
            
        }
    }
    
    if (keyData)
        CFRelease(keyData);
    return ret;
}

#pragma mark private method
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service{
    
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kSecClassGenericPassword,(id)kSecClass,
            service, (id)kSecAttrService,
            service, (id)kSecAttrAccount,
            (id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
            nil];
}

使用示例:获取设备UUID
很早之前,我们可以通过UIDevice类获取UDID作为设备的唯一标识,but 这个方法从iOS 5之后就被禁用了

NSString *udidString = [[UIDevice currentDevice] uniqueIdentifier];

后来获取UUID的方法本身不会自动存储,所以每次调都会获得一个新的UUID标识符,函数实现如下:

//创建一个UUID
+ (NSString *)createUUID{
    
    CFUUIDRef uuidObject = CFUUIDCreate(kCFAllocatorDefault);
    NSString *uuidStr = (NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuidObject);
    CFRelease(uuidObject);
    return [uuidStr autorelease];
}

为了保证UUID的唯一性,可以通过KeyChain来持久存储以保证即使删除后重装应用都能够获取这个唯一标识:

#define DF_UUID_KEY @"DF_UUID_KEY"

+ (void)saveUUID:(NSString *)deviceUUID{
    
    NSMutableDictionary *uuidDict = [NSMutableDictionary dictionary];
    [uuidDict setObject:deviceUUID forKey:DF_UUID_KEY];
    [DFKeyChainService setObject:uuidDict key:DF_UUID_KEY];
}

+ (NSString *)loadUUID{
    
    NSMutableDictionary *uuidDict = (NSMutableDictionary *)[DFKeyChainService getObjectBy:DF_UUID_KEY];
    NSString *retUUID = [uuidDict objectForKey:DF_UUID_KEY];
    
    if (StrisEmpty(retUUID)) {
        //取UUID并存入系统中的keychain中
        retUUID =  [NSString createUUID];
        [NSString saveUUID:retUUID];
    }

    return retUUID;
}

3.Archive归档/解档

需要注意的是:

  1. 归档支持的数据类型有:NSString、NSNumber、NSArray、NSDictionary;
  2. 如果需要归档的是自定义数据类型,使用方法上面keychain相同,对象转换成NSData类型,在这之前还要实现NSCoding协议;

NSKeyedArchiver归档:

[NSKeyedArchiver archiveRootObject:object toFile:filePath];

NSKeyedUnarchiver解档:

[NSKeyedUnarchiver unarchiveObjectWithFile:filePath];

4.SQLite 数据库

SQLite是一个轻量级关系型数据库,使用它需要最基本的数据库操作语言,不明所以的小伙伴建议先去W3Cschool学习一下。
如果你说没有用过SQLite原生方法,那非常荣幸因为苹果爸爸提供的都是c接口使用起来非常复杂还需要自己管理线程,但是如果说FMDB都没有用过那就应该关小黑屋了。
在GitHub上FMDB的链接:https://github.com/ccgus/fmdb

FMDB提供的方法比苹果提供的Core Data更加轻巧灵活,并且还提供多线程安全处理。与之前的数据持久化方法一样,SQLite也需要注意以下几点:

  1. 由于iOS的沙盒机制,SQLite数据库只能被创建在沙盒内部,删除App同样会导致数据丢失;
  2. 对数据库进行任何操作(建表、增、删、改、查)前必须调用open方法,在完成操作后及时调用close方法关闭;
  3. 数据库操作是需要消耗时间的,在插入、查询大量数据时尤为明显,这也是为什么FMDB需要做多线程安全处理的原因,即使是在当前iOS设备性能相当强悍的今天,本人也不建议一次性处理大量数据(我曾写过一个类似微信好友通讯录列表,由于必须一次性全部读取,当数据量达到上千时,每次从数据库查询时需要消耗较长时间,用户体验非常不好);
  4. 建表时尽量保证表字段的完整性,App产品本身在迭代的同时可能会有字段的增减,除非在项目有非常健壮的框架支持下,不建议经常直接修改数据库表体,处理不谨慎会导致一系列问题;
  5. 作为轻量级数据库并不能代替服务器本身,请不要把SQLite当成服务器数据库存入成千上万条数据,阶段性维护是很有必要的,删除无用的表体或表数据可以很有效的减少数据库文件所占用的硬盘空间。

使用步骤如下:

(1)创建数据库

//1.获取App沙盒路径下的Document文件夹
NSString *docuPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
//2.指定DB路径,后面需要每次初始化FMDatabase对象时都要使用这个.db结尾的文件
NSString *dbPath = [docuPath stringByAppendingPathComponent:@"test.db"];
//3.在对应路径下创建数据库
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
//4.在对数据库进行 增、删、改、查、建表 等一系列操作时,需要先调用FMDatabase实例方法open,如果open失败,可能是权限或者资源不足
if([db open]){
  //这里可以进行数据库操作
  
}
//5.完成数据库操作后,必须使用close方法关闭数据库
[db close];

(2)创建数据库表

NSString *sql = @"create table if not exists t_student ('ID' INTEGER PRIMARY KEY AUTOINCREMENT,'name' TEXT NOT NULL,'age' INTEGER NOT NULL)";
//解释上面这句SQL语句:
//创建一张名为“t_student”的数据库表
//表字段:ID(integer类型,主键,自增)
//表字段:name(text类型,不可为空)
//表字段:age(integer类型,不可为空)

BOOL result = [db executeUpdate:sql];//执行SQL语句
if (result) {
  NSLog(@"create table success");
}

(2)插入数据

[db open];

BOOL result = [db executeUpdate:@"insert into 't_student'(ID,name,age) values(?,?,?)" withArgumentsInArray:@[model.ID, model.name, @(model.age)]];
if (result) {
  NSLog(@"insert success");
} else {
  NSLog(@"insert fail");
}

[db close];

(3)查询数据

//从t_student表中查询ID为113的数据
FMResultSet *result = [db executeQuery:@"select * from 't_student' where ID = ?" withArgumentsInArray:@[@113]];
    
NSMutableArray *dataArr = [NSMutableArray array];
while ([result next]) {
    Student *stu = [Student new];
    stu.ID = [result intForColumn:@"ID"];
    stu.name = [result stringForColumn:@"name"];
    stu.age = [result intForColumn:@"age"];
    [dataArr addObject:stu];
}

retrun dataArr;

(4)删除数据

//从t_student表中删除ID为113的数据
BOOL result = [db executeUpdate:@"delete from 't_student' where ID = ?" withArgumentsInArray:@[@113]];
if (result) {
  NSLog(@"delete success");
} else {
  NSLog(@"delete fail");
}

如果本文对你有所帮助,记得点击一下喜欢哈

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 下课后 杨思瑶神秘地 给我一盒喜糖 说: “老师,我哥哥结婚!”
    无所事事的盲从阅读 261评论 0 0
  • 每个人都有自己想到达到的高度,在这个竞争如此激烈的社会环境下,可能大多数人都是在残酷的竞争环境里假装自己没有很高的...
    大东静阅读 260评论 0 2
  • 用途 通过[单例模式]可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资...
    pgydbh阅读 198评论 0 0