iOS数据库离线缓存思路和网络层封装

一直想总结一下关于iOS的离线数据缓存的方面的问题,然后最近也简单的对AFN进行了再次封装,所有想把这两个结合起来写一下。数据展示型的页面做离线缓存可以有更好的用户体验,用户在离线环境下仍然可以获取一些数据,这里的数据缓存首选肯定是SQLite,轻量级,对数据的存储读取相对于其他几种方式有优势,这里对AFN的封装没有涉及太多业务逻辑层面的需求,主要还是对一些方法再次封装方便使用,解除项目对第三方的耦合性,能够简单的快速的更换底层使用的网络请求代码。这篇主要写离线缓存思路,对AFN的封装只做简单的介绍。

关于XLNetworkApi

XLNetworkApi的一些功能和说明:

  • 使用XLNetworkRequest做一些GET、POST、PUT、DELETE请求,与业务逻辑对接部分直接以数组或者字典的形式返回。

  • 以及网络下载、上传文件,以block的形式返回实时的下载、上传进度,上传文件参数通过模型XLFileConfig去存取。

  • 通过继承于XLDataService来将一些数据处理,模型转化封装起来,于业务逻辑对接返回的是对应的模型,减少Controllor处理数据处理逻辑的压力。

  • 自定义一些回调的block

/**
 请求成功block
 */
typedef void (^requestSuccessBlock)(id responseObj);
/**
 请求失败block
 */
typedef void (^requestFailureBlock) (NSError *error);
/**
 请求响应block
 */
typedef void (^responseBlock)(id dataObj, NSError *error);
/**
 监听进度响应block
 */
typedef void (^progressBlock)(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite);
  • XLNetworkRequest.m部分实现
#import "XLNetworkRequest.h"
#import "AFNetworking.h"
@implementation XLNetworkRequest
 + (void)getRequest:(NSString *)url params:(NSDictionary *)params success:(requestSuccessBlock)successHandler failure:(requestFailureBlock)failureHandler {

    AFHTTPRequestOperationManager *manager = [self getRequstManager];
    
    [manager GET:url parameters:params success:^(AFHTTPRequestOperation * _Nonnull operation, id  _Nonnull responseObject) {
        successHandler(responseObject);
    } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
        XLLog(@"------请求失败-------%@",error);
        failureHandler(error);
    }];
}
  • 下载部分代码
 //下载文件,监听下载进度
 + (void)downloadRequest:(NSString *)url successAndProgress:(progressBlock)progressHandler complete:(responseBlock)completionHandler {
   
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:sessionConfiguration];
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    NSProgress *kProgress = nil;
    
    NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:&kProgress destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        
        NSURL *documentUrl = [[NSFileManager defaultManager] URLForDirectory :NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
        
        return [documentUrl URLByAppendingPathComponent:[response suggestedFilename]];
        
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nonnull filePath, NSError * _Nonnull error){
        if (error) {
            XLLog(@"------下载失败-------%@",error);
        }
        completionHandler(response, error);
    }];
    
    [manager setDownloadTaskDidWriteDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
        
        progressHandler(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
        
    }];
    [downloadTask resume];
}
  • 上传部分代码
 //上传文件,监听上传进度
  + (void)updateRequest:(NSString *)url params:(NSDictionary *)params fileConfig:(XLFileConfig *)fileConfig successAndProgress:(progressBlock)progressHandler complete:(responseBlock)completionHandler {

    NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:url parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        
        [formData appendPartWithFileData:fileConfig.fileData name:fileConfig.name fileName:fileConfig.fileName mimeType:fileConfig.mimeType];
        
    } error:nil];
    
    //获取上传进度
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    
    [operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
        
        progressHandler(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
        
    }];
    
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation * _Nonnull operation, id  _Nonnull responseObject) {
        completionHandler(responseObject, nil);
    } failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
        
        completionHandler(nil, error);
        if (error) {
            XLLog(@"------上传失败-------%@",error);
        }
    }];
    
    [operation start];
}

  • XLDataService.m部分实现
  + (void)getWithUrl:(NSString *)url param:(id)param modelClass:(Class)modelClass responseBlock:(responseBlock)responseDataBlock {
        [XLNetworkRequest getRequest:url params:param success:^(id responseObj) {
        //数组、字典转化为模型数组
           
        dataObj = [self modelTransformationWithResponseObj:responseObj modelClass:modelClass];
        responseDataBlock(dataObj, nil);
        
    } failure:^(NSError *error) {
        responseDataBlock(nil, error);
    }];
}
  • (关键)下面这个方法提供给继承XLDataService的子类重写,将转化为模型的代码写在这里,相似业务的网络数据请求都可以用这个子类去请求数据,直接返回对应的模型数组。
/**
 数组、字典转化为模型
 */
  + (id)modelTransformationWithResponseObj:(id)responseObj modelClass:(Class)modelClass {
       return nil;
}
关于离线数据缓存

当用户进入程序的展示页面,有三个情况下可能涉及到数据库存取操作,简单画了个图来理解,思路比较简单,主要是一些存取的细节处理。

  • 进入展示页面

    进入页面.png
  • 下拉刷新最新数据


    下拉刷新.png
  • 上拉加载更多数据


    上拉加载更多.png
  • 需要注意的是,上拉加载更多的时候,每次从数据库返回一定数量的数据,而不是一次性将数据全部加载,否则会有内存问题,直到数据库中没有更多数据时再发生网络请求,再次将新数据存入数据库。这里存储数据的方式是将服务器返回每组数据的字典归档成二进制作为数据库字段直接存储,这样存储在模型属性比较多的情况下更有优势,避免每一个属性作为一个字段,另外增加了一个idStr字段用来判断数据的唯一性,避免重复存储。
    首先定义一个工具类XLDataBase来做数据库相关的操作,这里用的是第三方的FMDB。

#import "XLDataBase.h"
#import "FMDatabase.h"
#import "Item.h"
#import "MJExtension.h"

@implementation XLDataBase

static FMDatabase *_db;

+ (void)initialize {
    
    NSString *path = [NSString stringWithFormat:@"%@/Library/Caches/Data.db",NSHomeDirectory()];
    _db = [FMDatabase databaseWithPath:path];
    [_db open];
    [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_item (id integer PRIMARY KEY, itemDict blob NOT NULL, idStr text NOT NULL)"];
}

//存入数据库
+ (void)saveItemDict:(NSDictionary *)itemDict {
    //此处把字典归档成二进制数据直接存入数据库,避免添加过多的数据库字段
    NSData *dictData = [NSKeyedArchiver archivedDataWithRootObject:itemDict];
    
    [_db executeUpdateWithFormat:@"INSERT INTO t_item (itemDict, idStr) VALUES (%@, %@)",dictData, itemDict[@"id"]];
}

//返回全部数据
+ (NSArray *)list {

    FMResultSet *set = [_db executeQuery:@"SELECT * FROM t_item"];
    NSMutableArray *list = [NSMutableArray array];
    
    while (set.next) {
        // 获得当前所指向的数据
        
        NSData *dictData = [set objectForColumnName:@"itemDict"];
        NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:dictData];
        [list addObject:[Item mj_objectWithKeyValues:dict]];
    }
    return list;
}

//取出某个范围内的数据
+ (NSArray *)listWithRange:(NSRange)range {
    
    NSString *SQL = [NSString stringWithFormat:@"SELECT * FROM t_item LIMIT %lu, %lu",range.location, range.length];
    FMResultSet *set = [_db executeQuery:SQL];
    NSMutableArray *list = [NSMutableArray array];
    
    while (set.next) {
        NSData *dictData = [set objectForColumnName:@"itemDict"];
        NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:dictData];
        [list addObject:[Item mj_objectWithKeyValues:dict]];
    }
    return list;
}

//通过一组数据的唯一标识判断数据是否存在
+ (BOOL)isExistWithId:(NSString *)idStr
{
    BOOL isExist = NO;
    
    FMResultSet *resultSet= [_db executeQuery:@"SELECT * FROM t_item where idStr = ?",idStr];
    while ([resultSet next]) {
        if([resultSet stringForColumn:@"idStr"]) {
            isExist = YES;
        }else{
            isExist = NO;
        }
    }
    return isExist;
}
@end

  • 一些继承于XLDataService的子类的数据库存储和模型转换的逻辑代码
#import "GetTableViewData.h"
#import "XLDataBase.h"

@implementation GetTableViewData

//重写父类方法
+ (id)modelTransformationWithResponseObj:(id)responseObj modelClass:(Class)modelClass {
    NSArray *lists = responseObj[@"data"][@"list"];
    NSMutableArray *array = [NSMutableArray array];
    for (NSDictionary *dict in lists) {
        [modelClass mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
            return @{ @"ID" : @"id" };
        }];
        [array addObject:[modelClass mj_objectWithKeyValues:dict]];
        
        //通过idStr先判断数据是否存储过,如果没有,网络请求新数据存入数据库
        if (![XLDataBase isExistWithId:dict[@"id"]]) {
            //存数据库
            NSLog(@"存入数据库");
            [XLDataBase saveItemDict:dict];
        }
    }
    return array;
}

  • 下面是一些控制器的代码实现:
#import "ViewController.h"
#import "GetTableViewData.h"
#import "Item.h"
#import "XLDataBase.h"
#import "ItemCell.h"
#import "MJRefresh.h"
#define URL_TABLEVIEW @"https://api.108tian.com/mobile/v3/EventList?cityId=1&step=10&theme=0&page=%lu"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
{
    NSMutableArray *_dataArray;
    UITableView *_tableView;
    NSInteger _currentPage;//当前数据对应的page
}
@end

@implementation ViewController
#pragma mark Life cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self createTableView];
    _dataArray = [NSMutableArray array];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSRange range = NSMakeRange(0, 10);
    //如果数据库有数据则读取,不发送网络请求
    if ([[XLDataBase listWithRange:range] count]) {
        [_dataArray addObjectsFromArray:[XLDataBase listWithRange:range]];
        NSLog(@"从数据库加载");
    }else{
        [self getTableViewDataWithPage:0];
    }
}

#pragma mark UI
- (void)createTableView {
    _tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
    _tableView.delegate = self;
    _tableView.dataSource = self;
    _tableView.rowHeight = 100.0;
    [self.view addSubview:_tableView];
    
    _tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        [self loadNewData];
    }];
    _tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
        [self loadMoreData];
    }];
}

#pragma mark GetDataSoure
- (void)getTableViewDataWithPage:(NSInteger)page {
    NSLog(@"发送网络请求!");
    NSString *url = [NSString stringWithFormat:URL_TABLEVIEW, page];
    [GetTableViewData getWithUrl:url param:nil modelClass:[Item class] responseBlock:^(id dataObj, NSError *error) {
        [_dataArray addObjectsFromArray:dataObj];
        [_tableView reloadData];
        [_tableView.mj_header endRefreshing];
        [_tableView.mj_footer endRefreshing];
    }];
}

- (void)loadNewData {
    NSLog(@"下拉刷新");
    _currentPage = 0;
    [_dataArray removeAllObjects];
    [self getTableViewDataWithPage:_currentPage];
}

- (void)loadMoreData {
    NSLog(@"上拉加载");
    _currentPage ++;
    NSRange range = NSMakeRange(_currentPage * 10, 10);
    if ([[XLDataBase listWithRange:range] count]) {
        [_dataArray addObjectsFromArray:[XLDataBase listWithRange:range]];
        [_tableView reloadData];
        [_tableView.mj_footer endRefreshing];
        NSLog(@"数据库加载%lu条更多数据",[[XLDataBase listWithRange:range] count]);
    }else{
        //数据库没更多数据时再网络请求
        [self getTableViewDataWithPage:_currentPage];
    }
}

#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ItemCell *cell = [ItemCell itemCellWithTableView:tableView];
    cell.item = _dataArray[indexPath.row];
    return cell;
}
@end

最后附上代码的下载地址,重要的部分代码中都有相应的注释和文字打印,运行程序可以很直观的表现。

https://github.com/ShelinShelin/OffLineCache.git
有考虑不周的地方,希望大家能提出一些意见,很乐意与大家互相交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 自己整理看
    AmoyFeng阅读 393评论 0 0
  • 当说话人对自己的想法不能做出断定时,可以使用「だろう」和「と思う」来表示推测。 だろう 「だろう」前面可以接名词,...
    不帅任你踹阅读 5,256评论 1 6
  • 拗口令问题 身边有几个小毛孩 我念了一则拗口令 是这样的: 洪小波和白小果 拿着笸箩拔萝卜 洪小波拔的是白萝卜 白...
    女儿滩阅读 972评论 0 0
  • 夜游乌镇 艄公摇橹穿石桥, 微波惊扰小鱼跳。 水阁秀窗灯静照, 江南远比紫禁好。 苏杭水秀人更娇, 历代...
    日月武阅读 248评论 0 3
  • 应该是第一次这样叫你吧,哈儿 看到你回去了,都不能安心的和你聊天了。 本来今天是想请彬彬吃饭的,但是彬彬出去打牌了...
    xiao钱钱阅读 392评论 0 0