使用 FMDB 做离线缓存的例子 (iOS)

3,157 阅读4分钟
原文链接: www.jianshu.com

本文以仿微博的应用为基础,实现使用FMDB做离线缓存


仿微博下拉


仿微博上拉

设计思路:

分析加载微博过程:
  1. 尝试从沙盒加载缓存数据
  2. 有缓存,直接加载缓存
  3. 无缓存,发送请求,展示返回的数据,将数据存入沙盒

    加载微博过程.png

分析微博返回数据:
  1. 需要加载的微博多,数据量大,不适合用plist和NSCoding这类一次性加载和存储全部数据的方法,使用数据库则可以做到取一部分数据和存一部分的数据
  2. 由于微博模型多,如果按照后台服务器一样,一个模型建一张表,每个表(如:用户,微博,图片,文字)又相互之间通过外键联系,会导致客户端的数据库过于复杂,所以只建一张表
  3. 每条微博的字段非常多,在客户端数据库表中也创建相应数量的字段保存数据是不适合的,所以可以在数据库表中只创建一个blob类型的status字段保存每条微博的全部数据
  4. 每条微博是根据自身字段idstr大小来比较新旧的,把每条微博从数据库取出再取出idstr来比较新旧是不适合的,所以表中还应该增加idstr字段方便查询
  5. 最终确定建一张表,三个字段: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 
@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