本文以仿微博的应用为基础,实现使用FMDB做离线缓存
仿微博下拉仿微博上拉
设计思路:
分析加载微博过程:
- 尝试从沙盒加载缓存数据
- 有缓存,直接加载缓存
-
无缓存,发送请求,展示返回的数据,将数据存入沙盒
加载微博过程.png
分析微博返回数据:
- 需要加载的微博多,数据量大,不适合用plist和NSCoding这类一次性加载和存储全部数据的方法,使用数据库则可以做到取一部分数据和存一部分的数据
- 由于微博模型多,如果按照后台服务器一样,一个模型建一张表,每个表(如:用户,微博,图片,文字)又相互之间通过外键联系,会导致客户端的数据库过于复杂,所以只建一张表
- 每条微博的字段非常多,在客户端数据库表中也创建相应数量的字段保存数据是不适合的,所以可以在数据库表中只创建一个blob类型的status字段保存每条微博的全部数据
- 每条微博是根据自身字段idstr大小来比较新旧的,把每条微博从数据库取出再取出idstr来比较新旧是不适合的,所以表中还应该增加idstr字段方便查询
- 最终确定建一张表,三个字段:id(系统默认),status,idstr
微博的字段非常多
表结构
实现步骤
步骤1.创建一个工具类StatusTool
步骤2.在StatusTool的initialize中初始化数据库
static FMDatabase *_db;
+ (void)initialize
{
// 1.打开数据库
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"statuses.sqlite"];
_db = [FMDatabase databaseWithPath:path];
[_db open];
// 2.创表
[_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_status (id integer PRIMARY KEY, status blob NOT NULL, idstr text NOT NULL);"];
}```
步骤3. StatusTool实现存储方法
- (void)saveStatuses:(NSArray *)statuses
{
// 要将一个对象存进数据库的blob字段,最好先转为NSData
// 一个对象要遵守NSCoding协议,实现协议中相应的方法,才能转成NSData
for (NSDictionary *status in statuses) {
// NSDictionary --> NSData
NSData *statusData = [NSKeyedArchiver archivedDataWithRootObject:status];
[_db executeUpdateWithFormat:@"INSERT INTO t_status(status, idstr) VALUES (%@, %@);", statusData, status[@"idstr"]];
}
}```
步骤4. StatusTool实现取缓存方法
+ (NSArray *)statusesWithParams:(NSDictionary *)params
{
// 根据请求参数生成对应的查询SQL语句
NSString *sql = nil;
if (params[@"since_id"]) { // 下拉刷新
sql = [NSString stringWithFormat:@"SELECT * FROM t_status WHERE idstr > %@ ORDER BY idstr DESC LIMIT 20;", params[@"since_id"]];
} else if (params[@"max_id"]) { // 上拉刷新
sql = [NSString stringWithFormat:@"SELECT * FROM t_status WHERE idstr <= %@ ORDER BY idstr DESC LIMIT 20;", params[@"max_id"]];
}
// 执行SQL
FMResultSet *set = [_db executeQuery:sql];
NSMutableArray *statuses = [NSMutableArray array];
while (set.next) {
NSData *statusData = [set objectForColumnName:@"status"];
NSDictionary *status = [NSKeyedUnarchiver unarchiveObjectWithData:statusData];
[statuses addObject:status];
}
return statuses;
}```
步骤5.方法调用:
/**
- 下拉刷新,加载最新的数据
*/
-
(void)loadNewStatus
{
// 1.拼接请求参数
。。。。。。。
params[@"since_id"] = firstStatusF.status.idstr;// 2.先尝试从数据库中加载微博数据
NSArray statuses = [StatusTool statusesWithParams:params];
if (statuses.count) { // 数据库有缓存数据
/ ……….处理数据, 展示返回的数据*/
} else { // 数据库没缓存数据
// 发送请求
[HttpTool get:@"https://api.weibo.com/2/statuses/friends_timeline.json" params:params success:^(id json) {
// 缓存新浪返回的字典数组
[StatusTool saveStatuses:json[@"statuses"]];/* ……….处理数据 , 展示返回的数据*/ } failure:^(NSError *error) { 。。。。 }];
}
}
/**
- 上拉刷新,加载更多的微博数据
*/
-
(void)loadMoreStatus
{
// 1.拼接请求参数
。。。。。。。
params[@"max_id"] = @(maxId);// 2. 先尝试从数据库中加载微博数据
NSArray statuses = [StatusTool statusesWithParams:params];
if (statuses.count) { // 数据库有缓存数据
/ ……….处理数据, 展示返回的数据*/
} else { // 数据库没缓存数据
// 发送请求
[HttpTool get:@"https://api.weibo.com/2/statuses/friends_timeline.json" params:params success:^(id json) {
// 缓存新浪返回的字典数组
[StatusTool saveStatuses:json[@"statuses"]];/* ……….处理数据, 展示返回的数据*/ } failure:^(NSError *error) { 。。。。。。 }];
}
}```
遇到问题:
如果把NSDictionary字典数据status直接通过[_db executeUpdateWithFormat:@"INSERT INTO t_status(status, idstr) VALUES (%@, %@);", status, status[@"idstr"]];
来存储,就会取不出来。
原因是通过%@来传入字典对象,相当于传入[status description];
,打印类型可以发现取出的是字符串,不是我们预期的字典对象。
解决方法:
把字典用NSData *statusData = [NSKeyedArchiver archivedDataWithRootObject:status];
转成NSData再存入数据库。
取出数据时,用NSDictionary *status = [NSKeyedUnarchiver unarchiveObjectWithData:statusData];
再转成字典。
为什么字典对象能转成NSData类型
字典对象能转成NSData类型的本质是遵守了NSCoding协议,所以如果想要把自定义对象转成NSData类型需要遵守NSCoding协议,实现encodeWithCoder和initWithCoder方法
例:自定义对象
@interface Shop : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@end
@implementation Shop
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeDouble:self.price forKey:@"price"];
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.price = [decoder decodeDoubleForKey:@"price"];
}
return self;
}
@end```
网友评论
我工作中真实的项目处理是:先直接展示缓存数据,然后执行下拉刷新,下拉刷新里就直接网络请求最新数据,请求成功直接删除数据库所有旧数据,再缓存这次的新数据,展示新数据。其实一般普通的应用差不多就是这样处理的。
以后我可能会在文章中详细补充这个问题。