iOS常用的数据存储

2016-08-25  本文已影响38人  可能是含钙最高的钙钙

在iOS开发过程中,会经常使用到数据存储.数据存储有好几种常见的方式:plist存储,偏好设置,归档,数据库等,该篇文章是我想做个记录,也想和大家分享下自己的一些见解,涉及的内容不是很深,只做实用性的参考.如果哪位大神对此有深入研究,可以留下来深入讨论下,我请吃饭.

沙盒
沙盒目录.png
// Documents获取方式
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
//Caches获取方法
   NSString *cachesStr = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
- Preferences:存放应用程序的偏好设置的文件.
//tmp获取方式
 NSString *tmpStr =  NSTemporaryDirectory();
关于沙盒的详细信息,可以去搜下别人的文章,我发现有很多文章对沙盒有详细的讲解的,或者参考苹果官方文档:About Files and Directories
Plist存储

定义:plist文件的全名是:Property List,它是一种用来存储串行化后的对象的文件。文件的扩展名是.plist,通常就称为plist文件.主要是用来存储一些OC对象,比如NSArray/NSDictionary,不能存放自定义对象.一般常用的Foundation对象有NSString, NSData, NSDate, NSNumber, NSArray, NSDictionary
存储原理:写入沙盒.只要有writeToFile的对象,就能进行plist存储,调用writeToFile就能自动生成plist格式的文件
注意:plist存储,不能存储自定义对象,会失败的。
plist文件在数据更新的时候需要先取出然后修改在存储
常用方法:

// 写入方法
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
- (BOOL)writeToURL:(NSURL *)url atomically:(BOOL)atomically;
// 取出 字典
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfFile:(NSString *)path;
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfURL:(NSURL *)url;
// 取出 数组
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfFile:(NSString *)path;
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfURL:(NSURL *)url;

获取沙盒路径

NSString *str = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

偏好设置

在开发中偏好设置用的还是比较多的,存取比较方便,不需要关心文件名,其实底层是利用字典存储一些键值对,你可以存储后打开路径文件看一下.
存储过程是用NSUserDefaults的单利调用setObject:forKey进行存储.
注意:不能存储对象,不能及时存储,需要做同步操作,把内存中的数据同步到硬盘上。

简单使用

 // 存储名字jack到沙盒中,存储的key是 @"name"
    [[NSUserDefaults standardUserDefaults] setObject:@"jack" forKey:@"name"];
    // 同步
    [[NSUserDefaults standardUserDefaults] synchronize];
    // 用key  @"name" 从沙盒中取出名字
    NSString *str =[[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
    

存储方法:

// 存储方法(set开头)
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
- (void)setFloat:(float)value forKey:(NSString *)defaultName;
- (void)setDouble:(double)value forKey:(NSString *)defaultName;
- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;
- (void)setURL:(nullable NSURL *)url forKey:(NSString *)defaultName

读取数据的方法:

// 读取数据的方法
- (nullable id)objectForKey:(NSString *)defaultName;
- (nullable NSString *)stringForKey:(NSString *)defaultName;
- (nullable NSArray *)arrayForKey:(NSString *)defaultName;
- (nullable NSDictionary<NSString *, id> *)dictionaryForKey:(NSString *)defaultName;
- (nullable NSData *)dataForKey:(NSString *)defaultName;
- (nullable NSArray<NSString *> *)stringArrayForKey:(NSString *)defaultName;
- (NSInteger)integerForKey:(NSString *)defaultName;
- (float)floatForKey:(NSString *)defaultName;
- (double)doubleForKey:(NSString *)defaultName;
- (BOOL)boolForKey:(NSString *)defaultName;
- (nullable NSURL *)URLForKey:(NSString *)defaultName NS_AVAILABLE(10_6, 4_0);

删除数据:

// 删除数据
- (void)removeObjectForKey:(NSString *)defaultName;
归档

个人理解,归档的过程就是一个序列化的过程,然后存储到指定的路径路径文件.归档可以存储对象,需要遵守NSCoding协议,然后重写下面两种方法

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
具体使用方法:
Person类的.h文件

#import <UIKit/UIKit.h>

@interface Person : NSObject

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

@end

Person类的.m文件

#import "Person.h"
@interface Person ()<NSCoding>

@end

@implementation Person

// 解档
// 从一个给定unarchiver的数据中返回一个初始化对象。
- (instancetype)initWithCoder:(NSCoder *)decoder{
    
    if (self = [super init]) {
        
        self.name = [decoder decodeObjectForKey:@"name"];
        self.age = [decoder decodeIntegerForKey:@"age"];
        self.height = [decoder decodeFloatForKey:@"height"];
        
    }
    return self;
}
// 归档
/**
 通过一个给定的archiver把消息接收者进行编码。
 当接收到encodeObject消息的时候,类终端encodeWithCoder方法被调用。
 */
- (void)encodeWithCoder:(NSCoder *)coder{
    
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeInteger:self.age forKey:@"age"];
    [coder encodeFloat:self.height forKey:@"height"];
    
}
// 创建对象
    Person *person = [Person new];
    person.name = @"jack";
    person.age = 20;
    person.height = 180.0f;

// 获取路径
    NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
// 归档
   BOOL flag = [NSKeyedArchiver archiveRootObject:person toFile:filePath];
    if (flag) {
        NSLog(@"归档成功");
    }else{
        NSLog(@"归档失败");
    }

// 解档
   Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:str];
// 创建对象
    Person *person = [Person new];
    person.name = @"jack";
    person.age = 20;
    person.height = 180.0f;

    //  归档成二进制 (可以用偏好设置保存或者直接写入到沙盒)
    NSData *dataArc = [NSKeyedArchiver archivedDataWithRootObject:person];
   [[NSUserDefaults standardUserDefaults] setObject:dataArc forKey:@"archiver"];
   [[NSUserDefaults standardUserDefaults] synchronize];

  // 解档
    NSData *dataUn = [[NSUserDefaults standardUserDefaults] objectForKey:@"archiver"];
    Person *unPerson = [NSKeyedUnarchiver unarchiveObjectWithData:dataUn];
    
    // 获取路径
    NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
    // 创建一个二进制对象
    NSMutableData *data = [[NSMutableData alloc] init];
    // 设置归档写入对象
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    // 归档
    [archiver encodeObject:person forKey:@"archiver"];// archivingDate的encodeWithCoder方法被调用
    [archiver finishEncoding];
    // 保存二进制到沙盒中
    [data writeToFile:str atomically:YES];

    // 解档
    // 通过文件获取一个二进制对象
    NSMutableData *data = [[NSMutableData alloc] initWithContentsOfFile:str];
    // 读取二进制文件
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    // 获得类
    Person *person = [unarchiver decodeObjectForKey:@"person"]; ;// initWithCoder方法被调用
    [unarchiver finishDecoding];
    

在自定义模型的.m文件中,重写 - (instancetype)initWithCoder:(NSCoder *)decoder 时,调用的是[super init], 而不是 [super initWithCoder:decoder]; 这是因为Person类继承的是NSObject,NSObject类没有遵守NSCoding这个协议.
initWithCoder原理:只要解析文件就会调用,xib,storyboard都是文件,因此只要解析这两个文件,就会调用initWithCoder。
因此如果在storyboard使用自定义view,重写initWithCoder方法,一定要调用[super initWithCoder:],因为只有系统才知道怎么解析storyboard,如果没有调用,就解析不了这个文件。
重写NSCoding协议的方法时有一个不便的地方就是,当自定义模型特别多的时候,每个.m文件中都需要重写一遍,挺麻烦的.下面这个文件是通过运行时的方法遍历模型中的属性进行设置的,在使用的时候,需要导入#import "GGArchiverCoding.h", #import <objc/runtime.h>头文件,在.m文件中写入这句话就可以了:NSCodingImplementation;

GGArchiverCoding.h 文件
注意:该文件可以原版copy的

#define NSCodingImplementation \
- (instancetype)initWithCoder:(NSCoder *)aDecoder{\
    if (self = [super init]) { \
        unsigned int count; \
        Ivar *ivarList = class_copyIvarList([self class], &count);\
        for (int i = 0; i < count; i++) {\
            Ivar ivar = ivarList[i];\
            const char *ivarName = ivar_getName(ivar);\
            NSString *key = [NSString stringWithUTF8String:ivarName];\
            id value = [aDecoder decodeObjectForKey:key]; \
            [self setValue:value forKey:key];\
        }\
    }\
    return self;\
}\
- (void)encodeWithCoder:(NSCoder *)aCoder{ \
    unsigned int count;\
    Ivar *ivarList = class_copyIvarList([self class], &count);\
    for (int i = 0; i < count; i++) {\
        Ivar ivar = ivarList[i];\
        const char *ivarName = ivar_getName(ivar);\
        NSString *key = [NSString stringWithUTF8String:ivarName];\
        id value = [self valueForKeyPath:key];\
        [aCoder encodeObject:value forKey:key];\
    }\
}

iOS中最常见的三种存储方式介绍完了,虽然使用率还是蛮好的,对于大型数据的存储上面三种就做不到了,或者说想做的很麻烦的.数据库就能这解决这种问题

SQLite介绍(PS:下面只是对SQLite3的简单实用的介绍,没有做详细的讲解,事后详情再补上.如果哪位大神有详细介绍的文章,可以跟帖链接,共同学习)

实际开发过程中,很少会直接用SQLite3,常常会采用一些对SQLite3封装好的三方框架,FMDB就是一个非常优秀的框架.
(度娘介绍):SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了,它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快,数据库(Database)是按照数据结构来组织、存储和管理数据的仓库.
想学习数据就,建议安装下Navicat,用着很不错的一款数据库软件.
数据库可以分为两大种类:关系型数据库(主流)和 对象型数据库
常用关系型数据库PC端:Oracle、MySQL、SQL Server、Access、DB2、Sybase.嵌入式\移动客户端:SQLite

数据库的存储结构和excel很像,以表(table)为单位
数据库的每一列叫字段(column,列,属性)
每一行叫记录(row,record,每行存放多个字段对应的值)
建表:Tabel Name: 填写表明,建议以_t开头

常用的关键字有:select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index等等数据库中不可以使用关键字来命名表、字段

创表

// 在编写数据库语句是,尽可能多的添加一些约束条件,这样可以方便交流

// 导入数据库头文件
#import <sqlite3.h>

// 保存数据库实例对象
@property (nonatomic, assign) sqlite3 *db;


// 获取沙盒路径
    NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.db"];
 // 将OC字符串 转成 C语言字符串
    const char *cFilePath = filePath.UTF8String;
// 创表
    // 将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库
    // 返回值表示状态
    int result = sqlite3_open(cFilePath, &_db);
  if (result == SQLITE_OK) {
        NSLog(@"成功打开数据库");
    // 创表
        const char *sql = "CREATE TABLE IF NOT EXISTS t_shop (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, price integer NOT NULL);";
        char *erroMsg = NULL;
        // 创表语句  (exec:执行sql语句)
       sqlite3_exec(_db, sql, NULL, NULL, &erroMsg);
        if (erroMsg) {
            NSLog(@"创表失败--%s", erroMsg);
        }
    }else{
         NSLog(@"打开数据库失败");
    }
  // 移动端在操作数据库的时候一般不会考虑关闭
  // 关闭数据库:sqlite3_close(db);

增加数据

    static int i = 0;
    i++;
    NSString *name = [NSString stringWithFormat:@"name-%d", i];
    
    NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_shop (name, price) VALUES ('%@', %d);", name, i];
    char *errMsg;
    sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
    NSLog(@"错误信息 = %s", errMsg);

删除数据

// 删除price大于10的数据
    NSString *sql = [NSString stringWithFormat:@"DELETE FROM t_shop where price > 10"];
   char *errMsg;
   sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, NULL);
    NSLog(@"错误信息 = %s", errMsg);

修改数据

// price小于5,设置为0
    NSString *sql = [NSString stringWithFormat:@"UPDATE t_shop set price = 0 where price < 5"];
    char *errMsg;
    sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
    NSLog(@"%s", errMsg);

查询数据

 // 查询数据如果用  sqlite3_exec(适合执行没有返回数据的操作)  需要用到第三个参数,函数的回调


    // 查询所有字段
    const char *sql = "SELECT * FROM t_shop;";
    // 准备
    //  第三个是sql长度, -1自动计算
    // stmt是用来取出查询结果的
    sqlite3_stmt *stmt = NULL;
    int status = sqlite3_prepare_v2(self.db, sql, -1, &stmt, NULL);
    if (status == SQLITE_OK) { // 准备成功 -- SQL语句正确
        
        // sqlite3_step  步骤(一步一步的执行)
        while (sqlite3_step(stmt) == SQLITE_ROW) { // 成功取出一条数据
            // 后面的数字是列号
            // 列好如果为0,代表的是主键的那列
            const char *name = (const char *)sqlite3_column_text(stmt, 1);
            const char *price = (const char *)sqlite3_column_text(stmt, 2);
            
            NSLog(@"name = %s, price = %s", name, price);
        }
    }
    
FMDB简单用法

FMDB是以0C的方式封装了SQLite的C语言,用起来更加的面相对象,但是SQL语句还是需要写的.这比起单纯的使用SQLite3去做数据存储已经方便了很多,而且FMDB还对数据的存取操作做了线程安全的保证.
下载下来后的FMDB目录结构

FMDB.png

在实际操作过程中,主要用到三个类
FMDatabase:一个FMDatabase对象就代表一个单独的SQLite数据库
FMResultSet:查询后的结果集.
FMDatabaseQueue:用于在多个线程中执行多个查询或更新,保证线程安全的.

// FMDB提供了两个方法去创建FMDatabase对象
// 通过指定数据库文件路径来创建FMDatabase对象
+ (instancetype)databaseWithPath:(NSString*)inPath;
- (instancetype)initWithPath:(NSString*)inPath;
// path文件路径
FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
    NSLog(@"数据库打开失败!");
}

// 关闭数据库
// [db close];

文件路径有三种情况(在FMDB文档里有说明的)
1,具体的系统文件路径,如果不存FMDatabase对象在会自动创建
2,数据库路径为空字符串@"", 会在零食目录创建一个空的数据库,当FMDatabase关闭连接时,数据库文件也会被删除
3,nil,会创建一个内存中临时数据库,当FMDatabase关闭连接时,  数据库也会被销毁.

执行更新

在FMDB中,除查询以外的所有操作,都称为“更新”create、drop、insert、update、delete等
// 下面这些方法在FMDatabase.h文件有详细的说明,建议查看头文件.
使用executeUpdate:方法执行更新,sql和format是数据库字符串
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 创表
NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement,  name text, price integer)";
[db executeUpdate:creatTableSql];
// 增加数据
[db executeUpdate:@"UPDATE t_shop SET price = ? WHERE name = ?;", @20, @"Jack"] 

执行查询

查询方法在FMDatabase.h文件中,返回的是FMResultSet结果集
常用的查询方法,sql和format是数据库字符串
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_shop"];
// 遍历结果集
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"name"];
    int price = [rs intForColumn:@"price"];
} 

// FMDatabaseQueue提供了三个对象方法
//1. 同步队列执行数据库操作。
- (void)inDatabase:(void (^)(FMDatabase *db))block;
//2.使用事务进行同步队列执行数据库操作
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
//3.使用递延交易
- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;

在项目中用FMDB做数据的存取的时候需要考虑到线程的安全问题,所以我在使用的时候是这样用的


//  数据库队列
static FMDatabaseQueue *queue = nil;

    if (!queue) {
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"fmdb.db"];
        // 该方法会根据路径去获取一个队列,如果路径不存在会自动创建一个数据库
        queue = [FMDatabaseQueue databaseQueueWithPath:path];
    }
    
    if (queue == nil) {
        NSLog(@"创建队列失败");
        return ;
    }

   // 创表
    [queue inDatabase:^(FMDatabase *db) {
        NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement, uid integer, name text, timeStamp integer)";
       [db executeUpdate:creatTableSql];
    }];

// 添加数据
    static int i = 0;
    time_t now;
    time_t timeStamp = time(&now);
    i++;
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdateWithFormat:@"INSERT INTO t_userTable (uid, name, timeStamp) VALUES (%d, %@, %ld)", i, @"jack", timeStamp];
    }];
    
// 查询数据
    [queue inDatabase:^(FMDatabase *db) {
        NSString *query = [NSString stringWithFormat:@"SELECT * FROM t_userTable"];
        FMResultSet *set = [db executeQuery:query];
        while ([set next]) {
            NSString *uid = [set stringForColumn:@"uid"];
            NSString *name = [set stringForColumn:@"name"];
            long int timeStamp = [set intForColumn:@"timeStamp"];
        }
    }];

// 修改数据
    NSString *update = [NSString stringWithFormat:@"UPDATE t_userTable set uid = 100 where uid > 10"];
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:update];
    }];

// 删除数据
    NSString *delete = [NSString stringWithFormat:@"DELETE FROM t_userTable where uid < %zd", 10];
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:delete];
    }];

关于数据库的详细的知识,后期不忙了再补上.大神们如有详细见解,期望您

上一篇下一篇

猜你喜欢

热点阅读