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

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

关于XLNetworkApi

XLNetworkApi的一些功能和说明:

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

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

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

  • 自定义一些回调的block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
请求成功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部分实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "XLNetworkRequest.h"
#import "AFNetworking.h"
@implementation XLNetworkRequest
+ (void)getRequest:(NSString *)url params:(NSDictionary *)params success:(requestSuccessBlock)successHandler failure:(requestFailureBlock)failureHandler {
//网络不可用
   if  (![self checkNetworkStatus]) {
       successHandler(nil);
       failureHandler(nil);
       return ;
   }
   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);
   }];
}
  • 下载部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//下载文件,监听下载进度
+ (void)downloadRequest:(NSString *)url successAndProgress:(progressBlock)progressHandler complete:(responseBlock)completionHandler {
   if  (![self checkNetworkStatus]) {
       progressHandler(0, 0, 0);
       completionHandler(nil, nil);
       return ;
   }
   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];
}
  • 上传部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//上传文件,监听上传进度
+ (void)updateRequest:(NSString *)url params:(NSDictionary *)params fileConfig:(XLFileConfig *)fileConfig successAndProgress:(progressBlock)progressHandler complete:(responseBlock)completionHandler {
   if  (![self checkNetworkStatus]) {
       progressHandler(0, 0, 0);
       completionHandler(nil, nil);
       return ;
   }
   NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@ "POST"  URLString:url parameters:params constructingBodyWithBlock:^(id  _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部分实现

1
2
3
4
5
6
7
8
9
+ (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的子类重写,将转化为模型的代码写在这里,相似业务的网络数据请求都可以用这个子类去请求数据,直接返回对应的模型数组。

1
2
3
4
5
6
/**
数组、字典转化为模型
*/
+ (id)modelTransformationWithResponseObj:(id)responseObj modelClass:(Class)modelClass {
      return  nil;
}

关于离线数据缓存

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

  • 进入展示页面

blob.png

下拉刷新最新数据

blob.png

上拉加载更多数据

blob.png

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

  • 首先定义一个工具类XLDataBase来做数据库相关的操作,这里用的是第三方的FMDB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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的子类的数据库存储和模型转换的逻辑代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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;
}

下面是一些控制器的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#import "ViewController.h"
#import "GetTableViewData.h"
#import "Item.h"
#import "XLDataBase.h"
#import "ItemCell.h"
#import "MJRefresh.h"
@interface ViewController () {
     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

希望大家能提出一些意见,很乐意与大家互相交流。

Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值