当前位置:首页 > 分类2 > 正文

iOS本地持久化储存(侧重数据库SQLite)

摘要: iOS本地持久化储存(侧重数据库SQLite iOS本地持久化储存(侧重数据库SQLite 原文链接:公司的项目存在已有两年,版...
iOS本地持久化储存(侧重数据库SQLite)

iOS本地持久化储存(侧重数据库SQLite)

原文链接:http://www.jianshu.com/p/10a26d01dc84
公司的项目存在已有两年,版本也到三点几了,但是本地持久化数据存储,始终用的是GVUserDefaults这个对NSUserDefaults进行了扩展的第三方库。但随着业务的发展,需要存储的地方越来越多,GVUserDefaults也越来也不能适应需求,当我们都忍受不了的时候,经过一番商讨之后,决定使用FMDB这个封装了SQLite3的第三方库。此篇文章以此为主线,理一理数据库和本地化储存的一些知识,但是此篇文章丝毫没有提到CoreData,喜欢使用CoreData的同学,这里先说声对不起了浪费了您20s宝贵的时间。
此篇文章的逻辑如下图所示:
图0-0 此篇文章的逻辑图
iOS本地持久化储存方式概述
说起iOS本地化储存的方式,大家估计在也熟悉不过了,NSUserDefault、File,Keychain、DataBase无非也就这几种方式。
NSUserDefault、File:这两种使用方式都很简单,需要注意的一点就是所存储的对象都需要遵守并实现NSCoding协议中的两个方法,适用的范围也都是一些小规模数据,其实NSUserDefault的底层实现还是以.plist文件进行储存的。
Keychain:用于储存一些私密信息,比如密码、证书等等,Keychain里保存的信息不会因App被删除而丢失,在用户重新安装App后依然有效。同样也适用于应用之间数据共享。
DataBase:说到储存,就不能不提DataBase技术,移动端所用的DBMS一般都是SQLite3,在iOS下,SQLite3的API是纯C语言的,这样我们一直以来面向对象开发的朋友们,突然找不到了对象,有点那么的惊慌失措。好在开源社区出现了像FMDB这样优秀的第三方框架帮我们找回了对象,同样苹果自己也出了新的技术就是所谓的CoreData(但CoreData并不是此篇文章介绍的重点)。数据库适合储存大规模数据,而且查找起来也比上述方式方便,所以大量储存的时候,还是要动用数据库,这也是我们放弃GVUserDefaults的原因。
数据库的基本知识
数据库
首先要有数据Data,然后数据多了名字就升级了称为数据库DataBase,这个时候就需要有管理系统去管理数据库也就是DataBaseManagerSystem,最后佐以DataBaseAdministrator及上述名称成为了一个系统就是所谓的DataBaseSystem。
Data --> DataBase --> DataBaseManagerSystem --> DataBaseSystem
Data: 数据库存储的基本对象。
DataBase: 说起来数据库,大家都有一个模模糊糊的概念,不就是一个存放数据的大仓库吗,这还有什么好说的。其实这样理解就已经很好了,但是说的更专业一点就是数据库是一个以某种有组织的方式存储的数据集合。
DataBaseManagerSystem: 数据库管理系统是位于用户与操作系统之间的一层数据管理软件。常见的DBMS有MySQL、PostgreSQL、Microsoft SQL Server、Oracle、Sybase、IBM DB2。当然这些都是用于服务端的DBMS,移动端用的DBMS是SQLite3,这也是本文讲述的重点。
DataBaseSystem: 在计算机系统中引入数据库后的系统,一般由数据库,数据库管理系统,应用系统,数据库管理成员(DBA)构成。一般情况下称数据库系统为数据库,所以有时候我们就会感到迷糊,你说DBS,他以为DB,你转到DB上了,他又开始讲DBS。所以真正想理解透一些知识的时候,基本概念一定要清晰一些,这样才不容易迷糊。
SQL语句
SQL(Structured Query Language),结构化查询语言,专门用来与数据库通信的语言。既然要操作数据库,SQL语句是必须要会写的。下面我简单列举几条简单的SQL语句仅供参考。想要学习更多的SQL语句的知识,请查阅其他资料,本篇在这儿不过多叙述。
这里以学生表为例写几条简单的SQL语句
// 创表(table)一张学生表 表名:student? 字段id:? 学生编号,作为主键,类型为整形? 字段name:学生名字,类型为字符串,并且不能为空值? 字段age: 学生年龄,类型为整形可为空。? 其中字段又称之为列(column)createtableifnotexistsstudent (idintegerprimarykeyautoincrement,nametextnotnull,? ? ageinteger);// 删除学生表droptableifexistsstudent;// 插入一条记录,主键id会自增长自动赋值,? 其中记录又称之为行(row)insertintostudent (name, age)values('小明',20);// 删除名字为小明的学生,? 这里的关键字where就是条件,如果无此条件,则影响整张表deletefromstudentwherename='小明';// 更新符合条件的记录updatestudentsetage =21wherename='小明';// 查询符合条件的记录select *fromstudentwherename='小明';
数据库的使用
如果你没有使用CoreData,那么无论你使用的是纯C语言的SQLite3库,还是使用对其进行封装了的FMDB,你都要设计出适合自己业务的表结构。关于表的设计,可以有两种设计模式。
数据库表的设计
第一种设计方式
以模型中的每个属性作为表的一个字段,这样在查询、读取放面操作起来更为方便,但是我个人感觉这种模式适合的业务是记录条数不多。而且字段尽可能的不要更改。创建好表之后,在去修改表结构还是听麻烦的一件事的。具体使用,可参考下面的代码:
@interfaceStudent:NSObject@property(nonatomic,copy)NSString*name;@property(nonatomic,assign)NSUIntegerage;@end// 数据储存的路径NSString*document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject];NSString*dbpath = [document stringByAppendingPathComponent:@"toyun.sqlite"];// 链接数据库self.db= [FMDatabase databaseWithPath:dbpath];// 打开数据库if([self.dbopen]) {? ? [self.dbexecuteUpdate:@"create table if not exists student ( id integer primary key autoincrement, name text not null, age integer not null)"];}// 实例化对象,即要储存的对象NSMutableArray*array = [NSMutableArrayarray];for(NSIntegeri =0; i <10; i++) {? ? Student *student = [[Student alloc] init];? ? student.name= [NSStringstringWithFormat:@"小明%ld", i];? ? student.age= arc4random() %20+10;? ? [array addObject:student];}// 将数据库插入表中for(Student *studentinarray) {? ? [self.dbexecuteUpdateWithFormat:@"insert into student (name, age) values (%@, %ld)", student.name, student.age];}[self.dbclose];NSLog(@"dbpath? ===? %@", dbpath);
第二种设计方式
这一种设计模式是将model作为一个字段直接将model转为NSData储在此字段中,在这里要指出的是model必须实现NSCoding协议,不过实际项目中我们字典转模型大都是使用第三方库,而现在比较流行的三个字典转模型的第三方库Mantle、MJExtension、YYModel都默认对此进行了处理,所以这儿稍微注意一下就好。第二种方法更适合于记录条数比较多,和业务的相关性不是太敏感,而且如果有查找排序这样的需求的话,可以从model挑出某些属性作为表中附带的一些字段。具体使用,参考下面的代码:
@interfaceStudent:NSObject@property(nonatomic,copy)NSString*name;@property(nonatomic,assign)NSUIntegerage;@end@implementationStudent- (void)encodeWithCoder:(NSCoder*)aCoder {? ? [aCoder encodeObject:_name forKey:@"name"];? ? [aCoder encodeInteger:_age forKey:@"age"];}- (nullable instancetype)initWithCoder:(NSCoder*)aDecoder {if(self= [superinit]) {? ? ? ? _name = [aDecoder decodeObjectForKey:@"name"];? ? ? ? _age = [aDecoder decodeIntegerForKey:@"age"];? ? }returnself;}@endNSString*document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject];NSString*dbpath = [document stringByAppendingPathComponent:@"yeemiao.sqlite"];self.db= [FMDatabase databaseWithPath:dbpath];if([self.dbopen]) {? ? ? ? ? ? [self.dbexecuteUpdate:@"create table if not exists student (id integer primary key autoincrement, model blob)"];}NSMutableArray*array = [NSMutableArrayarray];for(NSIntegeri =0; i <10; i++) {? ? Student *student = [[Student alloc] init];? ? student.name= [NSStringstringWithFormat:@"小明%ld", i];? ? student.age= arc4random() %20+10;? ? [array addObject:student];}// 转为NSData,存入数据库for(Student *studentinarray) {NSData*data = [NSKeyedArchiverarchivedDataWithRootObject:student];? ? [self.dbexecuteUpdateWithFormat:@"insert into student (model) values (%@)", data];}// 从数据库中查数据FMResultSet *set = [self.dbexecuteQuery:@"select * from student"];NSMutableArray*resultArray = [NSMutableArrayarray];while(set.next) {NSData*data = [set objectForColumnName:@"model"];? ? Student *student = [NSKeyedUnarchiverunarchiveObjectWithData:data];? ? [resultArray addObject:student];}[self.dbclose];
数据库使用中线程安全
在多线程中操作数据库,我们就要考虑线程安全问题,不然就有可能引起数据错乱的问题。解决办法有很多,可以自己去加锁,但是读写速度要求高的话就不太建议加锁了。SQLite3对于多线程是直接支持的,SQLite3库提供了三种方式:single thread、multi thread、serialized。同样FMDB自己也提供了多线程操作数据库的类FMDatabaseQueue,这个使用起来还是比较简单的。
删除缓存
现在大多数的App都是有本地化储存的,但是却不是每个App都对应有删除缓存的功能,起码我们自己的App是没有做删除缓存这个功能的,缓存是为了节省流量,删除缓存是为了节省空间我认为这两个功能同等重要,但是这个需求提了几次,产品不怎么重视啊!特别是在16G的版本下,这个功能还能能提升一些用户体验的。
此功能到也不难实现,搞清楚各种储存方式他们把文件存储到了那个文件夹下面,不同的数据我们储存的地方明显也是不同的,然后根据需求删除对应的数据就OK了,沙盒的具体目录如下所示:
// 沙盒下的文件夹目录Documents: iTunes会同步此文件夹,不应该删除Library? ? Caches: 缓存文件夹,删除缓存主要删除的文件夹? ? Preferences:NSUserDefault写入此文件夹下,iTunes会同步,不应该删除tmp:系统创建的临时文件夹,随时有可能被删除
做清除缓存功能时,可把Library/Caches文件夹下面的文件全部删除,也可以根据业务的需要删除指定的文件夹。
总结
此篇文章还是比较偏重于原理,对于具体的使用则没有说太多,具体使用各种各样,但是基本原理是比较统一的。关于数据库CURD由于我们不是做服务端开发,所以也没有深究,想要研究的话,可在查阅其他资料。除了上述所说的,可能在具体使用中还要考虑数据库版本迁移,数据库同步等需求。这些由于我的水平所限,也没有提到可以参考我写此文时的一篇参考博文iOS应用架构谈 本地持久化方案及动态部署。而NSUserDefault、File使用方法都很简单也都没有介绍。CoreData太过于重量级,我在项目中也没事实际运用过,这里也没有介绍。
参考
iOS应用架构谈 本地持久化方案及动态部署
在iOS开发中使用FMDB

干货教程!第四章:本地存储和图片缓存

上期APICloud和大家分享了《30天,App开发从0到1》一书中,数据云的用途和特点的相关知识,相信小伙伴们也有了一些收获。这次,小编又带着精彩内容来和大家见面了(请为无私奉献的小编点赞!)
在第三章中介绍了 APICloud 和后端交互的机制,并将商品列表根据数据进行了显示。本章将学习doT 模板引擎的基本使用、本地存储和图片缓存的使用、以及下拉刷新、上拉加载的实现。
● 学习目标
(1)学习 doT 模板引擎的使用。
(2)学习本地存储和图片缓存。
(3)下拉刷新、上拉加载的实现。
(4)理解 APICloud 应用沙箱结构。
(5)掌握 APICloud 资源访问协议使用。
(6)学习常用对话窗口的使用。
(7)学习窗口间的通信机制。
(8)学习 APICloud 平台的事件机制。
本期,APICloud和大家重点分享的是第二节:
本地存储和图片缓存
数据的本地存储和图片缓存可以极大地提高 app 的用户体验、提高 UI 响应速度、减少网络使用。
1.uzStorage
APICloud 提供了 uzStorage来提供类似 localStorage 的功能,但是比 localStorage 更适合混合app 开发。uzStorage 比标准的localStorage 更安全易用。
举个栗子: localStorage 有大小限制、异步会导致一些安全、不能存储对象等问题,但这些问题均在 uzStorage 中得到了解决。通过下面的 API 控制 uzStorage :
$api.getStorage("key"); 获取数据 $api.setStorage("key","value");存储数据 $api.rmStorage("key"); 移除保存的数据 $api.clearStorage(); 清空本地存储
2.偏好设置
APICloud 提供了针对系统原生偏好设置操作的 API(如 Android 的 preference 和 iOS 的 plist),使用键值对的方式存储。
3.文件
APICloud 提供了标准的文件操作接口,支持同步和异步的调用方式。使用下面的 API 操作文件:
api.readFile({sync:false,//是否同步,默认 falsepath:"PathToFile"// 文件路径,支持绝对路径和文件路径协议如 fs://、widget:// 等
}, function(ret,err){
//ret = {status:true,data:""}
}); //err = {code:0,msg:""}
api.writeFile({path:"PathToFile",//文件路径,支持绝对路径和文件路径协议如 fs://、cache:// 等,不支持 widget:// 目录,
该目录只读data:"data",// 文件内容
append:false// 是否以追加方式写入数据,默认 false,会清除之前文件内容 }, function(ret,err){
//ret = {status:true}
}); //err = {code:0,msg:""}
如果想获得更多对文件操作的能力请使用“fs”模块:
var fs = api.require('fs');
// 创建文件 fs.createFile({
path:'path/to/file'//文件路径 },function(ret,err){})//ret ={status: true} 是否成功
// 删除文件 fs.remove({
path:'path/to/file'//文件路径 },function(ret,err){})//ret ={status: true} 是否成功
// 获取文件数据的 MD5 值 fs.getMD5({
path:'path/to/file'//文件路径 },function(req,err){
//req = {// status:true,是否成功// md5Str:'' 文件数据的MD5值
}) //}
“fs”模块还提供了很多操作文件和目录的方法,请参阅相关文档。
4. database
使用“db”模块操作数据库,“db”模块封装了手机常用数据库 sqlite 的增删改查语句,可实现数据的本地存储,极大地简化了数据持久化问题,并且支持同步接口。“db”模块的使用如下:
var db = api.require('db');
// 打开数据库,若不存在则创建新的数据库 db.openDatabase({
name: 'name', //数据库名称
path:'path' // 数据库所在路径,不传时使用默认创建的路径,可选}, function(ret,err) {
//ret ={status:true} 是否创建成功 });
// 关闭数据库 db.closeDatabase({
name: 'name'//数据库名称 }, function(ret, err) {
//ret = {//status:true 是否成功
}); //}
//执行SQL语句 db.executeSql({
name: 'name',//数据库名称
sql: 'CREATE TABLEPersons(Id_P int, LastName varchar(255), FirstName varchar(255), A ddressvarchar(255), City varchar(255))'//要执行的SQL语句
}, function(ret,err) {//ret = {status:true} 是否执行成功
});
// 查询 SQL db.selectSql({
name: 'name',//数据库名称
sql: 'SELECT *FROM Persons'//查询SQL字符串 }, function(ret, err) {
//ret = {//status:true, 是否执行成功// data:[] 查询到的数据
}); //}
5. 存储容量
APICloud 提供了关于存储容量的 API,代码如下:
api.getFreeDiskSpace({sync:false//执行结果的返回方式。为 false 时通过 callback 返回,为 true 时直接返回,默认false
},function(ret,err){//ret = {size:1024} 剩余存储空间大小,单位为Byte,数字类型。(-1:无存储设备、-2:正在准备USB存
储设备、-3 :无法访问存储设备) });
api.getTotalSpace({sync:false//执行结果的返回方式。为false时通过callback返回,为true时直接返回,默认false
},function(ret,err){//ret = {size:1024} 总存储空间大小,单位为Byte,数字类型。(-1:无存储设备、-2:正在准备USB存储设备、-3 :无法访问存储设备) });
api.getCacheSize({sync:false//执行结果的返回方式。为 false 时通过 callback 返回,为 true 时直接返回,默认false},function(ret,err){//ret = {size} 缓存大小,单位为Byte,数字类型。(-1:无存储设备、-2:正在准备USB存储设备、-3:无法访问存储设备)});
api.clearCache({timeThreshold:10//(可选项)清除多少天前的缓存,默认 0
},function(ret,err){ }); //清除完成
6. 沙箱机制
在 Android 和 iOS 中均采用虚拟沙箱的机制来保障数据存储的安全和独立,app 只能访问自己文件系统的沙箱区域。沙箱位置如下:
? Android 的默认沙箱位置:sdcard/UZMap/appId。
? iOS 的默认沙箱位置:Documents/uzfs/appId。
可以通过修改 config.xml 中的 sandbox 属性来指定 Android 沙箱位置: <widget id="A1234567890123", sandbox="myBox"> 通过以上配置,app 将在 Android 手机的外部存储(如 SD 卡)的根目录中建立名为“myBox” 的目录,并以该目录作为本 app 的沙箱目录,app 运行过程中动态产生的资源文件将存储在该目录及其子目录下,并且这些资源不会随着 app 的卸载而清除。
7.资源访问协议
APICloud 资源被存放在 app 安装包(ipa 包或者 apk 包)中或应用沙箱中。沙箱分为 APICloud 应用虚拟沙箱和 Native 应用真实沙箱,真实沙箱是操作系统为 app 在设备内部存储上分配的空间,不可见,只允许 app 本身访问。访问这些位置的资源可以使用如下协议:
? widget://(访问安装包中的资源,根目录指向你的项目代码根路径,即 widget路径。只读属性);
?fs:/(/访问APICloud应用虚拟沙箱中资源,可读可写);
?cache://(访问本地缓存中的资源,存储在该路径下的资源,在调用 api.clearCache 时将 被清除。可读可写);
?box://(访问应用真实沙箱中的资源,私密数据建议使用本协议操作。可读可写)。 相关路径可以通过如下代码获取:
api.wgtDir(返回 widget 包根路径);
api.fsDir(返回 APICloud 应用虚拟沙箱根路径);
api.cacheDir(返回缓存根路径);
api.boxDir(返回应用真实沙箱根路径)。
8. 图片缓存
对于图片缓存,可使用如下代码:
var img = $api.byId("myImg");
api.imageCache({
url:'http://example.com/dir/file.png'
},function(ret,err){
if(ret && ret.status == true){
} img.src = ret.url;
else{} //处理错误 });
上述代码首先在参数中指定了要缓存的远程图片路径(url),在之后的回调函数中判断是否缓存成功(if(ret && ret.status == true){}),如果成功就可以使用 ret.url 来更新 <img> 标签。ret.url 是图片缓存到本地后的路径。
这里实现了图片缓存,app 在第一次从指定位置加载图片后会缓存到本地存储上面,下次使用时会直接调用缓存,以此提升加载速度和渲染效率。

发表评论