iOS数据持久化方案

持久化,就是将数据保存起来。使得下次APP启动的时候,可以继续访问。

在iOS开发中,我总结了一下,经常用到的几种数据存数存储方案。

  • plist文件
  • 偏好设置(preference)
  • 归解档(NSKeyedArchiver)
  • SQlite 3
  • CoreData

沙盒

每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,也就是默认情况下只能访问程序自己的目录,此区域被称为沙盒。
在介绍各种存储方法之前,我们必须了解一下沙盒的基本概念。

沙盒结构

如下:

"应用程序包"
Documents
Library
    Caches
    Preferences
tmp

顺便上传网上copy的图片:


沙盒结构
沙盒中相关路径
  • AppName.app 应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以不能在运行时对这个目录中的内容进行修改,否则会导致应用程序无法启动。
  • Documents/保存应用程序的重要数据文件和用户数据文件等。用户数据基本上都放在这个位置(例如从网上下载的图片或音乐文件),该文件夹在应用程序更新时会自动备份,在连接iTunes时也可以自动同步备份其中的数据。
  • Library:这个目录下有两个子目录,可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份.
    Library/Caches:保存应用程序使用时产生的支持文件和缓存文件(保存应用程序再次启动过程中需要的信息),还有日志文件最好也放在这个目录。iTunes 同步时不会备份该目录并且可能被其他工具清理掉其中的数据。
    Library/Preferences: 保存应用程序的偏好设置文件。NSUserDefaults类创建的数据和plist文件都放在这里。会被iTunes备份。
  • tmp/: 保存应用运行时所需要的临时数据。不会被iTunes备份。iPhone重启时,会被清空。

获取沙盒各个目录路径方法:

    // 获取沙盒根目录路径
    NSString *homeDir = NSHomeDirectory();
    
    // 获取Documents目录路径
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) firstObject];
    
    //获取Library的目录路径
    NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) lastObject];
    
    // 获取cache目录路径
    NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];

    // 获取tmp目录路径
    NSString *tmpDir =NSTemporaryDirectory();
    
    // 获取应用程序程序包中资源文件路径的方法:
    NSString *bundle = [[NSBundle mainBundle] bundlePath];

    NSLog(@"homeDir=%@ \n docDir=%@ \n libDir=%@ \n cachesDir=%@ \n tmpDir=%@ \n bundle=%@", homeDir,docDir, libDir, cachesDir, tmpDir, bundle);

开始进入正题

1. plist

plist文件是将某些特定的类,通过XML文件的方式保存在目录中。

可以被序列化的类型只有如下几种:

NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
使用
- (void)plist{
    //获取文件路径
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *fileName = [path stringByAppendingPathComponent:@"mygame.plist"];
    //存储
    NSArray *array = @[@"红色警戒", @"王者荣耀", @"穿越火线",@"英雄联盟",@"超级玛丽"];
    [array writeToFile:fileName atomically:YES];
    //读取
    NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
    NSLog(@"%@", result);
}
  • 存储时使用writeToFile: atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。

2 偏好设置

-几乎每个App都使用有偏好设置,如用户名,密码,字体大小等,iOS提供了一套标准的解决方案来为应用提供这项功能

  • 每个应用都有NSUserDefaults单例,通过它来存取偏好设置
    偏好设置专门用来保存应用程序的配置信息,通常不要在里面保存其它数据
  • 如果利用系统的偏好设置存储数据,默认保存在Preferences里,并且会将所有数据保存在同一个文件中
    使用
- (void)userDefaults{
    //1.获得NSUserDefaults文件
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        
    //2.向文件中写入内容
    [userDefaults setObject:@"小明" forKey:@"name"];
    [userDefaults setBool:YES forKey:@"sex"];
    [userDefaults setInteger:21 forKey:@"age"];
    //2.1立即同步
    [userDefaults synchronize];
        
    //3.读取文件
    NSString *name = [userDefaults objectForKey:@"name"];
    BOOL sex = [userDefaults boolForKey:@"sex"];
    NSInteger age = [userDefaults integerForKey:@"age"];
        
    NSLog(@"%@, %d, %ld", name, sex, age);
}
  • 如果没有调用synchronize方法,系统会根据I/O情况不定时刻地保存到文件中。所以如果需要立即写入文件的就必须调用synchronize方法。
  • 偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。

3. 归解档(NSKeyedArchiver)

归档可以实现把自定义的对象存入文件中,解决了plist和偏好设置只能存储常用类型的致命缺陷,可以一次性取出对象的全部属性

只要遵循了NSCoding协议的对象都可以通过它实现序列化,然后进行归档。

创建一个类,给它一些属性,如果想将这个类保存到文件中,必须让它遵守<NSCoding>

@interface Dog : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *color;
@end

在该类内部实现归档解档方法,(将一个自定义对象保存到文件中或从文件中读取的时候就会调用下面两个方法)

@implementation Dog

//归档,告诉系统该对象的哪些属性需要存储
- (void)encodeWithCoder:(NSCoder *)aCoder {

   [aCoder encodeObject:self.name forKey:@"name"];
   [aCoder encodeInteger:self.age forKey:@"age"];
   [aCoder encodeObject:self.color forKey:@"color"];
}

//解档,告诉系统该对象的哪些属性可以获取
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {

   if (self = [super init]) {
       self.name = [aDecoder decodeObjectForKey:@"name"];
       self.age = [aDecoder decodeIntegerForKey:@"age"];
       self.color = [aDecoder decodeObjectForKey:@"color"];
   }
   return self; 
}
@end

归档和解档

NSString * filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"DogList"];
[NSKeyedArchiver archiveRootObject:dog toFile:filePath];
Dog *dog = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];

4.SQLite3

对于大量数据的存储,就要用到数据库,原生的SQLite3极其晦涩,不易懂,不易用。所有就有了第三方库FMDB
我们就简单介绍一下FMDB的用法。

在 FMDB 中有三个重要的类:

  1. FMDatabase:是一个提供 SQLite 数据库的类,用于执行 SQL 语句。
  2. FMResultSet:用在FMDatabase中执行查询的结果的类。
  3. FMDatabaseQueue:在多线程下查询和更新数据库用到的类。

建立一个Person类,进行存储

@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) NSInteger age;
@property(nonatomic,assign) NSInteger number;
#import "DataManager.h"
#import "FMDB.h"

@interface DataManager (){
    FMDatabase  *_db;
    FMDatabaseQueue *_queue;
}

@end

@implementation DataManager

static id _instance;
+ (instancetype)shared{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc]init];
    });
    return _instance;
}

// 初始化数据
- (void)initDataBase{
    // 获得Documents目录路径
    NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 文件路
    NSString *filePath = [documentsPath stringByAppendingPathComponent:@"model5.sqlite"];
    _queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
    // 实例化FMDataBase对象
    _db = [FMDatabase databaseWithPath:filePath];
    
    if ([_db open]) {//'person_id' VARCHAR(255)
        // 初始化数据表
        NSString *personSql = @"CREATE TABLE 'person' ('id' INTEGER PRIMARY KEY AUTOINCREMENT  NOT NULL ,'id' VARCHAR(255),'name' VARCHAR(255),'age' VARCHAR(255),'number'VARCHAR(255)) ";
        
        BOOL res = [_db executeUpdate:personSql];
        if (res) {
            NSLog(@"success to creating db table");
        } else {
            NSLog(@"error when creating db table");
        }
        [_db close];
        
    } else {
        NSLog(@"error when open db");
    }
}
 
// 添加一条数据
//和insertStudent思路不一样
- (void)addPerson:(Person *)person{
    [_db open];
    NSNumber *maxID = @(0);
    FMResultSet *res = [_db executeQuery:@"SELECT * FROM person "];
    //获取数据库中最大的ID
    while ([res next]) {
        if ([maxID integerValue] < [[res stringForColumn:@"id"] integerValue]) {
            maxID = @([[res stringForColumn:@"id"] integerValue] ) ;
        }
    }
    maxID = @([maxID integerValue] + 1);
    
    [_db executeUpdate:@"INSERT INTO person(id,name,age,number) VALUES(?,?,?,?)",maxID,person.name,@(person.age),@(person.number)];
    [_db close];
    
}


/// 添加一条数据
/// @param person person
- (void)insertStudent:(Person *)person{
    NSLog(@"%s", __func__);
    static int idx = 1;
    //    FMDatabase *db = [FMDatabase databaseWithPath:self.dbPath];
    if ([_db open]) {
        NSString *sql = @"insert into person (name, age, number) values(?, ?, ?) ";
        BOOL res = [_db executeUpdate:sql, person.name, @(person.age), @(person.number)];
        if (!res) {
            NSLog(@"error to insert data");
        } else {
            NSLog(@"success to insert data");
        }
        [_db close];
    }
}

/// 添加多条数据
/// @param persons persons
- (void)insertStudents:(NSArray <Person *> *)persons{
    for (Person *p in persons) {
        [self insertStudent:p];
    }
}

/// 更新一条数据
/// @param person person
- (void)updateStudent:(Person *)person{
    
    [_queue inDatabase:^(FMDatabase * _Nonnull db) {
        //更新某个学生的年龄数据
        BOOL result = [db executeUpdate:@"update t_person set age = ? where name = ? ;",@(person.age), person.name];
        if (result) {
            NSLog(@"更新学生数据 name = %@ age = %ld  成功",person.name, (long)person.age);
        } else {
            NSLog(@"更新学生数据 name = %@ age = %ld  失败!",person.name, (long)person.age);
        }
    }];
}

/// 删除一条数据
/// @param person person
- (void)deleteStudent:(Person *)person{
    [_queue inDatabase:^(FMDatabase * _Nonnull db) {
        BOOL result = [db executeUpdate:@"delete from t_person where name = ? and age = ? ;",person.name, @(person.age)];
        if (result) {
            NSLog(@"删除学生数据 name = %@ age = %ld  成功",person.name, (long)person.age);
        } else {
            NSLog(@"删除学生数据 name = %@ age = %ld  失败!",person.name, (long)person.age);
        }
    }];
}


/// 查询数据
- (NSArray *)queryStudents{
    __block NSMutableArray *persons = [NSMutableArray array];
    
    //inDatabase 内部是一个同步线程,所以在 block 执行完毕之前,查询方法不会被提前 return
    [_queue inDatabase:^(FMDatabase * _Nonnull db) {
        //查询年龄大于 20岁的学生数据, ASC为升序(默认), DESC 为降序
        FMResultSet *rs = [db executeQuery:@"select * from t_student where age > ? order by age ASC;",@(20)];
        //用 while
        while (rs.next) {
            NSString *name = [rs stringForColumn:@"name"];
            int age = [rs intForColumn:@"age"];
            int number = [rs intForColumn:@"number"];
            Person *person = [[Person alloc] init];
            person.name = name;
            person.age = age;
            person.number = number;
            [persons addObject:person];
        }
    }];
    [_db close];
    return persons.copy;
}

- (NSArray *)getAllPerson{
    NSLog(@"%s", __func__);
    __block NSMutableArray *persons = [NSMutableArray array];
    if ([_db open]) {
        NSString *sql = @"select *from person";
        FMResultSet *rs = [_db executeQuery:sql];
        while ([rs next]) {
            NSString *name = [rs stringForColumn:@"name"];
            int age = [rs intForColumn:@"age"];
            int number = [rs intForColumn:@"number"];
            Person *person = [[Person alloc] init];
            person.name = name;
            person.age = age;
            person.number = number;
            [persons addObject:person];
        }
    }
    [_db close];
    return persons.copy;
}
@end

5. CoreData

CoreData是苹果官方出品的一款用于存储的数据库,其底层也是封装的SQlite3,但是比SQLite3更加面向对象。

但是,说实话,使用起来并不是非常的友好。

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

推荐阅读更多精彩内容