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

9,330 阅读20分钟

存储的基础知识

应用沙盒

应用沙盒文件夹
  • Application(应用程序包):包含了所有的资源文件和和可执行文件,上架前经过数字签名,上架后不可修改。
  • Documents:文档目录,要保存程序生成的数据,会自动被分到iCloud中。保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录。(注意点:不要保存从网络上下载的文件,否则会无法上架!)
  • Library:
    1. 用户偏好,使用 NSUserDefault 直接读写!
    2. 如果要想数据及时写入磁盘,还需要调用一个同步方法
    3. 保存临时文件,"后续需要使用",例如:缓存图片,离线数据(地图数据
    4. 系统不会清理 cache 目录中的文件,就要求程序开发时,"必须提供 cache 目录的清理解决方案"
    5. Caches:存放体积大又不需要备份的数据
    6. Preference:保存应用的所有偏好设置,iCloud会备份设置信息
  • Tmp:临时文件,系统会自动清理。重新启动就会清理。保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。
    1. 存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能
    2. 保存临时文件,"后续不需要使用"
    3. tmp 目录中的文件,系统会自动清理
    4. 重新启动手机,tmp 目录会被清空
    5. 系统磁盘空间不足时,系统也会自动清理
应用沙盒目录的常见获取方式
  • 沙盒根目录:NSString *home = NSHomeDirectory();
  • Documents:(2种方式)
    1. 利用沙盒根目录拼接”Documents”字符串(不建议采用,因为新版本的操作系统可能会修改目录名)
      NSString *home = NSHomeDirectory();
      NSString *documents = [home stringByAppendingPathComponent:@"Documents"];
      
    2. 利用 NSSearchPathForDirectoriesInDomains 函数
      // NSUserDomainMask 代表从用户文件夹下找
      // YES 代表展开路径中的波浪字符“~”
      NSArray *array =  NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, NO);
      // 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
      NSString *documents = [array objectAtIndex:0];
      
  • tmp:NSString *tmp = NSTemporaryDirectory();
  • Library/Caches:(跟Documents类似的2种方法)
    1. 利用沙盒根目录拼接”Caches”字符串
    2. 利用 NSSearchPathForDirectoriesInDomains 函数 (将函数的第2个参数改为:NSCachesDirectory即可)
  • Library/Preference:通过NSUserDefaults类存取该目录下的设置信息。

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

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

内存分区

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

  • 栈区stack:这一块区域系统会自己管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量,深递归,函数循环调用都可能导致内存耗尽而运行崩溃。
  • 堆区heap:与栈区相对,这一块一般由我们自己管理,比如alloc,free的操作,存储一些自己创建的对象。
  • 全局区(静态区static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放。
  • 常量区:存储常量字符串和const常量
  • 代码区:存储代码

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只是将数据保存在内存 的。

  • NSCache和NSMutableDictionary的区别
    NSCache是线程安全的,不需要加线程锁,而NSMutableDictionary线程不安全。

代码举例

@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

打印结果 从打印结果可以看看出:

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

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

持久化存储方式

  • Plist(NSArray\NSDictionary),只存储数组、字典,但是数组和字典里不能有自定义对象。
  • 偏好设置(Preference\NSUserDefaults),也不能存储自定义对象。
  • 归档NSCoding(NSKeyedArchiver\NSkeyedUnarchiver),存储自定义对象。
    局限:一次性读取和存储操作。
  • SQLite3
    • 操作数据比较快
    • 可以局部读取
    • 比较小型,占用的内存资源比较少
  • Core Data

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的存储和读取过程

偏好设置

很多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协议的对象才可以。

  • NSCoding 协议有2个方法:
    1. encodeWithCoder: 每次归档对象时,都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量,可以使用encodeObject:forKey:方法归档实例变量。
    2. initWithCoder: 每次从文件中恢复(解码)对象时,都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用decodeObject:forKey方法解码实例变量。
归档一个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];

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

Core Data

Core Data 中的三个对象
  • NSManagedObject
    只要定义一个类继承该类就会创建一张与之对应的表,也就是一个继承与该类的类就对应一张表,每一个通过继承该类创建出来的对象,都是该类对应的表中的一条数据。
  • NSManagedObjectContext
    用于操作数据库,只要有类它就能对数据库的表进行增删改查
  • NSPersistentStoreCoordinator
    决定存储的位置
Core data 多线程不安全

Core data 本身并不是一个并发安全的架构,所以在多线程中实现 Core data 会有问题。

  • 原因: CoreData 中的 NSManagedObjectContext 在多线程中不安全。
  • 如果想要多线程访问 CoreData,最好的方法是一个线程一个 NSManagedObjectContext。
  • 每个 NSManagedObjectContext 都可以使用同一个 NSPersistentStoreCoordinator 实例,因为 NSManagedObjectContext 会在使用 NSPersistentStoreCoordinator 前上锁。

钥匙串

keychain 存储在硬盘上,删除了应用,保存的数据还在。

每个APP的keychain相对来说是独立的,但是也可以实现APP之间keychain数据的共享,前提是同一个TeamID下、且设置了数据共享。

  • 实现数据的共享
    去工程中 capabilities -> keychain sharing 打开Keychain功能,在里面添加一个标识,同一个TeamID下的APP共用同一标识,即可访问存储在同一标识下的数据。

    备注:你会发现打开Keychain功能,xcode自动把我们的bundleID加进了第一个条目,如果你在写入数据时不指定条目,那系统就默认存入第一个(也就是我们的bundleID)所在的条目下。

账户密码自动填充

需要iOS11.0及更高的系统。

  • 要想存储在钥匙串的密码在不同设备间共享:

    1. 需要有一个支持HTTPS协议的域名,因为App实现用户名密码自动匹配要和该网站关联。
    2. 开启iPhone的iCloud钥匙串服务。
  • 自动填充开发配置步骤:

    1. 在App账号开启 Associated Domains
    2. 工程中开启 Associated Domains
    3. 创建文件:apple-app-site-association
    {
        "webcredentials": {
            "apps": ["teamID.BundleID"]
        }
    }
    
    1. 文件上传至Associated Domains所填的域名目录下或.well-know文件夹下,官方推荐使用文件夹。验证方法:可以以https方式访问到该文件:example.com/.wellhnow/a…
    2. 在Xib或代码中把输入框的textContentType填为.username和.password。
    3. 手机【设置】-【密码与账户】- 网站与应用密码】开启自动填充密码功能。
  • 自动填充的原理:
    用户安装了APP,系统会将应用与应用关联域名列表并从Associated Domains Entitlement获取每个域,将尝试下载该域的Apple App Site Association文件,如果上述步骤都顺利的话,系统会将应用程序与该域关联,并为该域的凭据启动密码自动填充。

icloud

iCloud 文档存储功能来满足应用数据云存储的需求,用户可以在自己iCloud账号下的任何设备访问或修改App的这部分数据。

iOS iCloud 存储中,苹果提供了三个功能:

  • Key-value storage
    类似于iOS里NSUserDefaults的通过键值对来保存简单数据的功能,这种Property-list数据格式适合存储一些非关键数据,如用户配置。

  • iCloud Documents
    提供了文件及目录的数据类型,这个特性决定了此功能相较于Key-value storage更适合进行关键数据的存储。

  • CloudKit
    相较于前两个,CloudKit就要复杂得多,这是苹果为开发者提供的一整套数据库工具。开发者通过苹果提供的Cloud dashboard网站可以配置所需的表结构,并通过在代码中导入CloudKit进行数据库操作。

SQLite3

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

  • 什么是 SQLite
    SQLite 是一款嵌入式的轻量关系型文件数据库。
    它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。
    它的处理速度比 Mysql、PostgreSQL 这两款著名的数据库都还快。

  • 什么是数据库
    数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。
    数据库可以分为2大种类:关系型数据库(主流)、对象型数据库。

  • 常用关系型数据库
    PC端:Oracle、MySQL、SQL Server、Access、DB2、Sybase。
    嵌入式\移动客户端:SQLite。

  • 数据库是如何存储数据的
    数据库的存储结构和excel很像,以表(table)为单位

  • 数据库存储数据的步骤
    新建一张表(table)。
    添加多个字段(column,列,属性)。
    添加多行记录(row,每行存放多个字段对应的值)。

一些名词

主键
  • 主键约束 如果t_student表中就name和age两个字段,而且有些记录的name和age字段的值都一样时,那么就没法区分这些数据,造成数据库的记录不唯一,这样就不方便管理数据。良好的数据库编程规范应该要保证每条记录的唯一性,为此,增加了主键约束。也就是说,每张表都必须有一个主键,用来标识记录的唯一性。
  • 什么是主键 主键(Primary Key,简称PK)用来唯一地标识某一条记录。 例如t_student可以增加一个id字段作为主键,相当于人的身份证。 主键可以是一个字段或多个字段。
  • 主键的设计原则
    1. 主键应当是对用户没有意义的
    2. 永远也不要更新主键
    3. 主键不应包含动态变化的数据
    4. 主键应当由计算机自动生成
  • 主键的声明
    1. 在创表的时候用primary key声明一个integer类型的id作为t_student表的主键: create table t_student (id integer primary key, name text, age integer);
    2. 主键字段 只要声明为primary key,就说明是一个主键字段。 主键字段默认就包含了not null 和 unique 两个约束。
    3. 如果想要让主键自动增长(必须是integer类型),应该增加 autoincrement create table t_student (id integer primary key autoincrement, name text, age integer);
外键
  • 外键约束 利用外键约束可以用来建立表与表之间的联系。 外键的一般情况是:一张表的某个字段,引用着另一张表的主键字段。
  • 新建一个外键 create table t_student (id integer primary key autoincrement, name text, age integer, class_id integer, constraint fk_t_student_class_id_t_class_id foreign key (class_id) references t_class (id); t_student表中有一个叫做fk_t_student_class_id_t_class_id的外键。 这个外键的作用是用t_student表中的class_id字段引用t_class表的id字段。
字段类型
  • SQLite 将数据划分为以下几种存储类型: integer: 整型数据,⼤大⼩小为4个字节。 bigint: 整型数据,⼤大⼩小为8个字节。 smallint: 整数数据,⼤大⼩小为 2 个字节。 tinyint: 从0到255的整数数据,存储⼤大⼩小为 1 字节。 float: 4字节浮点数。 double: 8字节浮点数。 real: 8字节浮点数。 text : 文本字符串。 blob : 二进制数据(比如文件)。

  • 其它类型 null: 空值。 blob: 二进制对象。 default: 缺省值 primary key: 主键 autoincrement: 主键⾃自动增⻓长

  • 实际上SQLite是无类型的 就算声明为integer类型,还是能存储字符串文本(主键除外)。 建表时声明啥类型或者不声明类型都可以,也就意味着创表语句可以这么写:create table t_student(name, age); 为了保持良好的编程规范、方便程序员之间的交流,编写建表语句的时候最好加上每个字段的具体类型。

SQL语句

  • SQL语句的种类

    1. 数据定义语句(DDL:Data Definition Language) 包括create和drop等操作 在数据库中创建新表或删除表(create table或 drop table)
    2. 数据操作语句(DML:Data Manipulation Language) 包括insert、update、delete等操作 上面的3种操作分别用于添加、修改、删除表中的数据
    3. 数据查询语句(DQL:Data Query Language) 可以用于查询获得表中的数据 关键字select是DQL(也是所有SQL)用得最多的操作
      其他DQL常用的关键字有where,order by,group by和having
  • SQL语句的特点 不区分大小写(比如数据库认为user和UsEr是一样的)。 每条语句都必须以分号 ; 结尾。

  • SQL中的常用关键字有 select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index 等等。

  • 数据库中不可以使用关键字来命名表、字段。

创表

格式: 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语句后加上一些条件

  • 条件语句的常见格式 where 字段 = 某个值 ; // 不能用两个 = where 字段 is 某个值 ; // is 相当于 = where 字段 != 某个值 ; where 字段 is not 某个值 ; // is not 相当于 != where 字段 > 某个值 ; where 字段1 = 某个值 and 字段2 > 某个值 ; // and相当于C语言中的 && where 字段1 = 某个值 or 字段2 = 某个值 ; // or 相当于C语言中的 ||

示例:

// 将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 '%张%';  
  • 起别名 格式(字段和表都可以起别名) select 字段1 别名 , 字段2 别名 , … from 表名 别名; select 字段1 别名, 字段2 as 别名, … from 表名 as 别名; select 别名.字段1, 别名.字段2, … from 表名 别名; 示例:

    // 给name起个叫做myname的别名,给age起个叫做myage的别名
    select name myname, age myage from t_student;
    // 给t_student表起个别名叫做s,利用s来引用表中的字段
    select s.name, s.age from t_student s;
    
  • 计算记录的数量 格式 select count (字段) from 表名; select count ( * ) from 表名; 示例:

    select count (age) from t_student ;
    select count ( * ) from t_student where score >= 60;
    select count(*) from t_student; 
    select avg(Age) from t_student; 
    select sum(Age) from t_student;
    
  • 排序 按照某个字段的值,进行排序搜索

    select * from t_student order by 字段;
    select * from t_student order by age;
    

    默认是按照升序排序(由小到大),也可以变为降序(由大到小)

    select * from t_student order by age desc; // 降序
    select * from t_student order by age asc;  // 升序(默认)
    

    也可以用多个字段进行排序

    // 先按照年龄排序(升序),年龄相等就按照身高排序(降序)
    select * from t_student order by age asc, height desc;
    
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
    FMDB是iOS平台的SQLite数据库框架。
    FMDB以OC的方式封装了SQLite的C语言API

  • FMDB的优点
    使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码。
    对比苹果自带的Core Data框架,更加轻量级和灵活。
    提供了多线程安全的数据库操作方法,有效地防止数据混乱。

FMDB有三个主要的类

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

FMDB 使用

  1. 打开数据库
    通过指定SQLite数据库文件路径来创建FMDatabase对象

    FMDatabase *db = [FMDatabase databaseWithPath:path];
    if (![db open]) {
        NSLog(@"数据库打开失败!");
    }
    
    • 文件路径有三种情况
      具体文件路径: 如果不存在会自动创建
      空字符串@"": 会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除。
      nil; 会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁。
  2. 执行更新
    在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"]
    
  3. 执行查询
    查询方法

    - (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;

Demo地址:demo->知识点->持久化存储、缓存