上一节(一一五)利用NSKeyedArchiver实现任意对象转为二进制介绍了将任意对象转化为二进制数据和还原的方法,可用于实现本节介绍的微博数据离线缓存。
通过新浪官方的API可以发现,返回的微博数据如下样式:
{ "statuses": [ { "created_at": "Tue May 31 17:46:55 +0800 2011", "id": 11488058246, "text": "求关注。", "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>", "favorited": false, "truncated": false, "in_reply_to_status_id": "", "in_reply_to_user_id": "", "in_reply_to_screen_name": "", "geo": null, "mid": "5612814510546515491", "reposts_count": 8, "comments_count": 9, "annotations": [], "user": { "id": 1404376560, "screen_name": "zaku", "name": "zaku", "province": "11", "city": "5", "location": "北京 朝阳区", "description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。", "url": "http://blog.sina.com.cn/zaku", "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1", "domain": "zaku", "gender": "m", "followers_count": 1204, "friends_count": 447, "statuses_count": 2908, "favourites_count": 0, "created_at": "Fri Aug 28 00:00:00 +0800 2009", "following": false, "allow_all_act_msg": false, "remark": "", "geo_enabled": true, "verified": false, "allow_all_comment": true, "avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1", "verified_reason": "", "follow_me": false, "online_status": 0, "bi_followers_count": 215 } }, ... ], "ad": [ { "id": 3366614911586452, "mark": "AB21321XDFJJK" }, ... ], "previous_cursor": 0, // 暂未支持 "next_cursor": 11488013766, // 暂未支持 "total_number": 81655 }其中的statuses是微博字典数组,微博客户端通过字典数组转为微博模型WeiboStatus,然后通过微博模型创建微博尺寸模型StatusFrame模型,最后Cell通过尺寸模型StatusFrame拿到应该显示的内容和尺寸。这是客户端实现的功能。
获取数据的关键,在于statuses的抓取上,通常情况下我们向新浪的服务器发送请求,接收到响应体,取出statuses字典数组后进行处理,现在我们要实现的是进入客户端时先判断有没有缓存过数据,缓存过则直接显示已经缓存的数据,而不直接通过网络请求。
离线缓存通过iOS自带的SQLite3实现。
关于数据库的使用请参考文章使用FMDB操作SQLite数据库,本文重点是缓存思路。
微博有一个唯一标志idstr用于标识微博的先后顺序,idstr越大的微博越新,我们的缓存思路是把微博字典数组中的每一条微博都用NSKeyedArchiver转化为二进制存入数据库的status字段,然后再把idstr存储到idstr字段,通过对idstr排序、和当前要求的id范围比较就可以正确判断出数据的新旧关系。
在给新浪发送数据时,如果是加载新数据,会在参数中加入一个since_id,表示只加载这个id之后的微博;如果是加载旧数据,会在参数中加入一个max_id,表示只加载这个之前的。
因此在缓存数据时,只接收微博字典数组即可;在读取缓存时,传入请求参数,判断参数中有无since_id、max_id即可。缓存方法均为类方法,为了能拿到数据库,使用一个静态成员变量管理,并且在类的公共初始化方法中打开数据库,如果没有创建过表则创建。
数据库的打开:
static FMDatabase *_db; + (void)initialize{ // 公共类初始化方法 NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"statuses.sqlite"]; _db = [FMDatabase databaseWithPath:path]; [_db open]; [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_status (id integer PRIMARY KEY, status blob NOT NULL, idstr text NOT NULL)"]; }
+ (NSArray *)statusesWithParams:(NSDictionary *)params{ 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"]]; }else{ // 无条件加载最前面20条 sql = [NSString stringWithFormat:@"SELECT * FROM t_status ORDER BY idstr DESC LIMIT 20"]; } // 执行查询 FMResultSet *set = [_db executeQuery:sql]; NSMutableArray *statuses = [NSMutableArray array]; while (set.next) { NSData *dictData = [set objectForColumnName:@"status"]; NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:dictData]; [statuses addObject:dict]; } return statuses; } + (void)saveStatuses:(NSArray *)statuses{ for (NSDictionary *statusDict in statuses) { NSData *dictData = [NSKeyedArchiver archivedDataWithRootObject:statusDict]; [_db executeUpdateWithFormat:@"INSERT INTO t_status (status, idstr)VALUES(%@,%@)",dictData,statusDict[@"idstr"]]; } }