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

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

一直想总结一下关于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

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

相关文章
|
9天前
|
缓存 NoSQL API
分享大厂对于缓存操作的封装
作者shigen分享了关于Redis缓存的封装,以避免常见问题如穿透、击穿、雪崩。封装包括四个文件:CacheEnum、CacheLoader、CacheService和CacheServiceImpl。CacheEnum用于统一管理缓存名和过期时间,CacheService定义缓存操作接口,CacheServiceImpl是实现类,使用Semaphore解决缓存击穿问题。
12 1
分享大厂对于缓存操作的封装
|
10天前
|
存储 缓存 NoSQL
Java中的内存数据库与缓存技术
Java中的内存数据库与缓存技术
|
21天前
|
缓存 Java 数据库
springboot数据库及缓存常用依赖及配置
springboot数据库及缓存常用依赖及配置
46 9
|
18天前
|
缓存 负载均衡 NoSQL
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
34 1
|
1月前
|
缓存 NoSQL 中间件
应对数据库不断膨胀的数据:缓存和队列中间件
【6月更文挑战第5天】该文探讨了优化数据库使用以提升应用系统性能的策略。文中建议利用Redis缓存和MQ消息队列作为辅助工具,以进一步优化性能和减少资源消耗。
32 2
应对数据库不断膨胀的数据:缓存和队列中间件
|
3天前
|
缓存 数据库
高并发场景下,到底先更新缓存还是先更新数据库?
高并发场景下,到底先更新缓存还是先更新数据库?
|
28天前
|
存储 缓存 NoSQL
Redis是一种高性能的内存数据库,常用于高并发环境下的缓存解决方案
【6月更文挑战第18天】**Redis摘要:** 高性能内存数据库,擅长高并发缓存。数据存内存,访问迅速;支持字符串、列表等多元数据类型;具备持久化防止数据丢失;丰富命令集便于操作;通过节点集群实现数据分片与负载均衡,增强可用性和扩展性。理想的缓存解决方案。
33 1
|
10天前
|
网络协议 安全 网络安全
软考中级之数据库系统工程师笔记总结(五)网络基础
软考中级之数据库系统工程师笔记总结(五)网络基础
9 0
|
1月前
|
存储 canal 缓存
【高频】如何保证缓存和数据库一致
【高频】如何保证缓存和数据库一致