将来跳槽用

iOS 数据存储(持久化存储、缓存)

2020-11-08  本文已影响0人  原来是泽镜啊

存储的基础知识

应用沙盒

应用沙盒文件夹

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

应用沙盒目录的常见获取方式

内存缓存和磁盘缓存的区别

缓存分为内存缓存和磁盘缓存两种。
其中内存是指当前程序的运行空间,缓存速度快容量小,是临时存储文件用的,供CPU直接读取,比如说打开一个程序,他是在内存中存储,关闭程序后内存就又回到原来的空闲空间;
磁盘是程序的存储空间,缓存容量大、速度慢、可持久化。与内存不同的是磁盘是永久存储东西的,只要里面存放东西,不管运行不运行 ,他都占用空间!磁盘缓存是存在Library/Caches

内存分区

iOS内存分为5个区:栈区,堆区,全局区,常量区,代码区(从左到右由高地址像低地址)。

NSCache

NSCache是苹果提供的一套缓存机制,用法和NSMutableDictionary类似,在AFNetworking,SDWebImage,Kingfisher中都有用到。

当内存不足时NSCache会自动释放内存。 NSCache设置缓存对象数量和占用的内存大小,当缓存超出了设置会自动释放内存。
NSCache是Key-Value数据结构,其中key是强引用,不实现NSCoping协议,作为key的对象不会被拷贝。

NSCache的属性

countLimit: 能够缓存对象的最大数量,默认值是0,没有限制。
totalCostLimit: 设置缓存占用的内存大小 evictsObjectsWithDiscardedContent: 是否回收废弃内容,默认YES

NSCache的方法

objectForKey: 通过key获得缓存对象。
setObject: forKey: 缓存对象。
setObject: forKey: cost: 缓存对象,并指定key值对应的成本,用于计算缓存中所有对象的总成本。
removeObjectForKey: 删除指定对象。
removeAllObjects: 删除所有缓存对象。

NSCacheDelegate代理

willEvictObject: 缓存对象即将被清理时调用,一般开发者用来调试,不能在此方法中修改缓存。

在下列场景中会被调用:

  1. removeObjectForKey
  2. 缓存对象超过NSCache的countLimit和otalCostLimit属性设置的限制
  3. App进入后台
  4. 系统发出内存警告
  5. cache这个实例的生命周期结束前

NSCache需要注意的点

当收到内存警告,而我们又调用removeAllObjects,则无法再继续往缓存中添加数据。
不提供缓存总的大小,想知道NSCache占用的内存大小,只有通过添加缓存的cost自己计算。
NSCache自动释放内存的算法是不确定的,有时是按照LRU(最近最久未使用)释放,有时随机释放。
NSCache中的数据在APP重启后会消失,因为NSCache只是将数据保存在内存 的。

代码举例

@interface NSCacheVC ()<NSCacheDelegate>
@property (nonatomic, strong) NSCache *myCache;
@end

@implementation NSCacheVC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor purpleColor];

    self.myCache = [[NSCache alloc]init];
    self.myCache.delegate = self;

    for (int i = 0; i<10; i++) {
        [self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i) cost: 1];
    }

    for (int i = 0; i<10; i++) {
        NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
    }

    /// 清除缓存
    [self.myCache removeAllObjects];
    /// 设置缓存限制
    self.myCache.totalCostLimit = 5;

    NSLog(@"设置缓存限制后=================");

    for (int i = 0; i<10; i++) {
        // 设置成本数为1
        [self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i) cost: 1];
    }

    for (int i = 0; i<10; i++) {
        NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
    }

    /// 清除缓存
    [self.myCache removeAllObjects];
    NSLog(@"设置缓存限制后但未设置成本数cost=================");

    for (int i = 0; i<10; i++) {
        [self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i)];
    }

    for (int i = 0; i<10; i++) {
        NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
    }

    /// 清除缓存
    [self.myCache removeAllObjects];

}

// 即将回收对象的时候进行调用,实现代理方法之前要遵守NSCacheDelegate协议。
- (void)cache:(NSCache *)cache willEvictObject:(id)obj{
    NSLog(@"NSCache回收---%@", obj);
}

@end
复制代码
打印结果 image

从打印结果可以看看出:

  1. 设置缓存限制且知道缓存成本数时,超出是会自动回收。但是设置缓存限制但不知道缓存成本数时不会自动回收。

  2. 回收时会调用 willEvictObject 的代理方法。

持久化存储方式

Plist

属性列表是一种XML格式的文件,拓展名为plist如果对是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用writeToFile:atomically:方法直接将对象写到属性列表文件中。

属性列表-归档NSDictionary将一个NSDictionary对象归档到一个plist属性列表中

// 将数据封装成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"张三" forKey:@"name"];
[dict setObject:@"155xxxxxxx" forKey:@"phone"];
[dict setObject:@"27" forKey:@"age"];
// 将字典持久化到Documents/stu.plist文件中
[dict writeToFile:path atomically:YES];
复制代码

属性列表-恢复NSDictionary读取属性列表,恢复NSDictionary对象

// 读取Documents/stu.plist的内容,实例化
NSDictionaryNSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@"name:%@", [dict objectForKey:@"name"]);
NSLog(@"phone:%@", [dict objectForKey:@"phone"]);
NSLog(@"age:%@", [dict objectForKey:@"age"]);
复制代码
属性列表-NSDictionary的存储和读取过程 image

偏好设置

很多iOS应用都支持偏好设置,比如保存用户名、密码、字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能。
每个应用都有个NSUserDefaults实例,通过它来存取偏好设置。

比如,保存用户名、字体大小、是否自动登录:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"张三" forKey:@"username"];
[defaults setFloat:18.0f forKey:@"text_size"];
[defaults setBool:YES forKey:@"auto_login"];
复制代码

读取上次保存的设置

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *username = [defaults stringForKey:@"username"];
float textSize = [defaults floatForKey:@"text_size"];
BOOL autoLogin = [defaults boolForKey:@"auto_login"];
复制代码

注意: UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法[defaults synchornize];强制写入。

归解档

NSKeyedArchiver如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复。 不是所有的对象都可以直接用这种方法进行归档,只有遵守了NSCoding协议的对象才可以。

归档一个NSArray对象到Documents/array.archive
NSArray *array = [NSArray arrayWithObjects:@”a”,@”b”,nil];
[NSKeyedArchiver archiveRootObject:array toFile:path];
复制代码
NSKeyedArchiver-归档Person对象(Person.h)
@interface Person : NSObject, NSCoding
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@end

@implementation Person
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeInt:self.age forKey:@"age"];
    [encoder encodeFloat:self.height forKey:@"height"];
}
- (id)initWithCoder:(NSCoder *)decoder {
    self.name = [decoder decodeObjectForKey:@"name"];
    self.age = [decoder decodeIntForKey:@"age"];
    self.height = [decoder decodeFloatForKey:@"height"];
    return self;
}
@end

// 归档(编码)
Person *person = [[Person alloc] init];
person.name = @"xxx";
person.age = 27;
person.height = 1.83f;
[NSKeyedArchiver archiveRootObject:person toFile:path];
// 恢复(解码)
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
复制代码

如果父类也遵守了NSCoding协议,请注意: 应该在encodeWithCoder:方法中加上一句[super encodeWithCode:encode];确保继承的实例变量也能被编码,即也能被归档。 应该在initWithCoder:方法中加上一句self = [super initWithCoder:decoder];确保继承的实例变量也能被解码,即也能被恢复。

NSData -- 归档

使用 archiveRootObject:toFile: 方法可以将一个对象直接写入到一个文件中,但有时候可能想将多个对象写入到同一个文件中,那么就要使用NSData来进行归档对象。 NSData可以为一些数据提供临时存储空间,以便随后写入文件,或者存放从磁盘读取的文件内容。可以使用[NSMutableData data]创建可变数据空间。

归档(编码)

// 新建一块可变数据区
NSMutableData *data = [NSMutableData data];
// 将数据区连接到一个NSKeyedArchiver对象
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
// 开始存档对象,存档的数据都会存储到NSMutableData中
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];
// 存档完毕(一定要调用这个方法)
[archiver finishEncoding];
// 将存档的数据写入文件
[data writeToFile:path atomically:YES];
复制代码
image

NSData-从同一文件中恢复2个Person对象恢复(解码)

// 从文件中读取数据
NSData *data = [NSData dataWithContentsOfFile:path];
// 根据数据,解析成一个NSKeyedUnarchiver对象
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
// 恢复完毕
[unarchiver finishDecoding];
复制代码

利用归档实现深复制比如对一个Person对象进行深复制

// 临时存储person1的数据
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person1];
// 解析data,生成一个新的Person对象
Student *person2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// 分别打印内存地址
NSLog(@"person1:0x%x", person1);  // person1:0x7177a60
NSLog(@"person2:0x%x", person2); // person2:0x7177cf0
复制代码
image

Core Data

Core Data 中的三个对象
Core data􏲅􏰀􏰴􏰭􏱸􏰔􏰝􏰴􏱤􏱱􏱢􏰌􏰢􏰇􏲆􏱌􏲀􏱃􏱕􏰽􏰐􏱘􏱦 本身并不是一个并发安全的架构,所以在多线程中实现 Core data􏰋􏱡􏱈􏱗 会有问题

SQLite3

在 iOS 中使用 SQLite3,首先要添加库文件 libsqlite3.dylib 和导入主头文件 #import<splite3.h>

一些名词

主键
外键
字段类型

SQL语句

创表

格式: create table 表名 (字段名1 字段类型1, 字段名2 字段类型2, …); create table if not exists 表名 (字段名1 字段类型1, 字段名2 字段类型2, …); 示例:

create table t_student (id integer, name text, age inetger, score real);
create table if not exists Student (
    ID integer primary key autoincrement,
    Name varchar(128),
    Age integer,
    Class interger default 0,
    RegisterTime datetime,
    Money float default 0,
    Birthday date
);
复制代码
删表

格式: drop table 表名; drop table if exists 表名; 示例:

drop table t_student;
复制代码
插入数据(insert)

格式: insert into 表名 (字段1, 字段2, …) values (字段1的值, 字段2的值, …); 示例:

insert into t_student (name, age) values (‘张三’, 10);
复制代码

注意: 数据库中的字符串内容应该用单引号 ’ 括住; 所有字符串必须要加 ' ' 单引号; 整数,浮点数不⽤用加 ' ‘; 日期需要加单引号 ' ‘; 字段顺序没有关系; 对于⾃自动增⻓长的主键不需要插⼊入字段;

更新数据(update)

格式: update 表名 set 字段1 = 字段1的值, 字段2 = 字段2的值, … ; 示例:

update t_student set name = ‘jack’, age = 20;
复制代码

注意: 上面的示例会将t_student表中所有记录的name都改为jack,age都改为20

删除数据(delete)

格式: delete from 表名; 示例:

// 删除指定ID值为2的记录
delete from t_student where ID=2; 
// 删除t_student表中所有的记录(慎重)
delete from t_student;
复制代码

注意: 上面的示例会将t_student表中所有记录都删掉

DQL语句 -- 条件语句where

如果只想更新或者删除某些固定的记录,那就必须在DML语句后加上一些条件

示例:

// 将t_student表中年龄大于10 并且 姓名不等于jack的记录,年龄都改为 5
update t_student set age = 5 where age > 10 and name != ‘jack’;
// 删除t_student表中年龄小于等于10 或者 年龄大于30的记录
delete from t_student where age <= 10 or age > 30;
// 将t_student表中名字等于jack的记录,score字段的值 都改为 age字段的值  
update t_student set score = age where name = ‘jack’ ;
复制代码
DQL语句 -- select

格式: select 字段1, 字段2, … from 表名; select * from 表名; // 查询所有的字段 示例:

select name, age from t_student ;
select * from t_student ;
// 条件查询
select * from t_student where age > 10 ; 
// 模糊查询
select * from t_student where name  like  '%张%' or phone like '%张%';  
复制代码
DQL语句 -- limit

使用limit可以精确地控制查询结果的数量,比如每次只查询10条数据 格式: select * from 表名 limit 数值1, 数值2; 示例:

// 跳过最前面4条语句,然后取8条记录
select * from t_student limit 4, 8;
// 取最前面的7条记录
select * from t_student limit 7; // 相当于select * from t_student limit 0, 7 ;
复制代码

limit常用来做分页查询,比如每页固定显示5条数据,那么应该这样取数据:

第1页:limit 0, 5
第2页:limit 5, 5
第3页:limit 10, 5
…
第n页:limit 5*(n-1), 5
复制代码
简单约束

建表时可以给特定的字段设置一些约束条件,常见的约束有 not null :规定字段的值不能为null unique :规定字段的值必须唯一 default :指定字段的默认值 (建议:尽量给字段设定严格的约束,以保证数据的规范性) 示例: name字段不能为null,并且唯一 age字段不能为null,并且默认为1 create table t_student (id integer, name text not null unique, age integer not null default 1);

表连接查询

需要联合多张表才能查到想要的数据 表连接的类型: 内连接:inner join 或者 join (显示的是左右表都有完整字段值的记录) 左外连接:left outer join (保证左表数据的完整性) 示例:

查询0316iOS班的所有学生
select s.name, s.age from t_student s, t_class c where s.class_id = c.id and c.name = ‘0316iOS’;
复制代码

数据库操作语句

创建或打开数据库
// path是数据库文件的存放路径
sqlite3 *db = NULL;
int result = sqlite3_open([path UTF8String], &db);
复制代码

代码解析: sqlite3_open()将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库。如果result等于常量SQLITE_OK,则表示成功打开数据库。 sqlite3 *db:一个打开的数据库实例。 数据库文件的路径必须以C字符串(而非NSString)传入。

关闭数据库

sqlite3_close(db);

执行不返回数据的SQL语句
char *errorMsg = NULL;  // 用来存储错误信息
char *sql = "create table if not exists t_person(id integer primary key autoincrement, name text, age integer);"; // 执行创表语句
int result = sqlite3_exec(db, sql, NULL, NULL, &errorMsg);
复制代码

代码解析: sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据 sqlite3_exec()还可以执行的语句:

  1. 开启事务:begin transaction;
  2. 回滚事务:rollback;
  3. 提交事务:commit;
带占位符插入数据
char *sql = "insert into t_person(name, age) values(?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, "母鸡", -1, NULL);
    sqlite3_bind_int(stmt, 2, 27);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
    NSLog(@"插入数据错误");
}
sqlite3_finalize(stmt);
复制代码

代码解析: sqlite3_prepare_v2()返回值等于SQLITE_OK,说明SQL语句已经准备成功,没有语法问题。 sqlite3_bind_text()大部分绑定函数都只有3个参数:

  1. 第1个参数是sqlite3_stmt *类型。
  2. 第2个参数指占位符的位置,第一个占位符的位置是1,不是0。
  3. 第3个参数指占位符要绑定的值。
  4. 第4个参数指在第3个参数中所传递数据的长度,对于C字符串,可以传递-1代替字符串的长度。
  5. 第5个参数是一个可选的函数回调,一般用于在语句执行后完成内存清理工作。

sqlite_step():执行SQL语句,返回SQLITE_DONE代表成功执行完毕 sqlite_finalize():销毁sqlite3_stmt *对象

查询数据
char *sql = "select id,name,age from t_person;";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int _id = sqlite3_column_int(stmt, 0);
        char *_name = (char *)sqlite3_column_text(stmt, 1);
        NSString *name = [NSString stringWithUTF8String:_name];
        int _age = sqlite3_column_int(stmt, 2);
        NSLog(@"id=%i, name=%@, age=%i", _id, name, _age);
    }
}
sqlite3_finalize(stmt);
复制代码

代码解析:
sqlite3_step(): 返回SQLITE_ROW代表遍历到一条新记录。
sqlite3_column_*(): 用于获取每个字段对应的值,第2个参数是字段的索引,从0开始。

后期增加数据库中的字段

􏰛􏰜􏰝􏰞􏰟􏰠􏰝􏰡􏰋􏰌􏰍􏰎1. 增加表字段 ALTER TABLE 表名􏰡􏰢 ADD COLUMN 􏰍􏰎􏰢字段名 字段类型􏰍􏰎􏰣􏰤; 2. 􏰥􏰦􏰡􏰍􏰎删除表字段 ALTER TABLE 􏰡􏰢表名􏰡􏰢 DROP COLUMN 􏰍字段名 字段类型􏰍􏰎􏰣􏰤; 3. 􏰧􏰨􏰡􏰍􏰎修改表字段 ALTER TABLE 􏰡􏰢表名􏰡􏰢 RENAME COLUMN 􏰩􏰍􏰎􏰢旧字段名 TO 新字段名􏰪􏰍􏰎􏰢;

FMDB

FMDB有三个主要的类

FMDatabase:一个FMDatabase对象就代表一个单独的SQLite数据库 用来执行SQL语句。
FMResultSet:使用FMDatabase执行查询后的结果集。
FMDatabaseQueue:用于在多线程中执行多个查询或更新,它是线程安全的。

FMDB 使用

  1. 打开数据库

通过指定SQLite数据库文件路径来创建FMDatabase对象 FMDatabase *db = [FMDatabase databaseWithPath:path]; if (![db open]) { NSLog(@"数据库打开失败!"); }

  1. 执行更新
    在FMDB中,除查询以外的所有操作,都称为“更新”。create、drop、insert、update、delete等。
    使用executeUpdate:方法执行更新:

    - (BOOL)executeUpdate:(NSString*)sql, ...
    - (BOOL)executeUpdateWithFormat:(NSString*)format, ...
    - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
    复制代码
    

    示例

    [db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"]
    复制代码
    
  2. 执行查询
    查询方法

    - (FMResultSet *)executeQuery:(NSString*)sql, ...
    - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
    - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
    复制代码
    

    示例

    // 查询数据
    FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];
    
    // 遍历结果集
    while ([rs next]) {
        NSString *name = [rs stringForColumn:@"name"];
        int age = [rs intForColumn:@"age"];
        double score = [rs doubleForColumn:@"score"];
    }
    复制代码
    

FMDatabaseQueue

FMDatabase这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。
为了保证线程安全,FMDB提供方便快捷的FMDatabaseQueue类。

FMDatabaseQueue 简单使用

// 创建
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];

//简单使用
[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jim"];

    FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    while ([rs next]) {
        // …
    }
}];
复制代码

使用事务

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jim"];

    FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    while ([rs next]) {
        // …
    }
}];

// 事务回滚
*rollback = YES;

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

以下资料在群文件可自行下载!

作者:orilme
链接:https://juejin.im/post/6891187709760110599

上一篇 下一篇

猜你喜欢

热点阅读