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

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

相关文章
|
5月前
|
存储 缓存 数据库
解决缓存与数据库的数据一致性问题的终极指南
解决缓存与数据库的数据一致性问题的终极指南
282 63
|
6月前
|
消息中间件 canal 缓存
项目实战:一步步实现高效缓存与数据库的数据一致性方案
Hello,大家好!我是热爱分享技术的小米。今天探讨在个人项目中如何保证数据一致性,尤其是在缓存与数据库同步时面临的挑战。文中介绍了常见的CacheAside模式,以及结合消息队列和请求串行化的方法,确保数据一致性。通过不同方案的分析,希望能给大家带来启发。如果你对这些技术感兴趣,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
360 6
项目实战:一步步实现高效缓存与数据库的数据一致性方案
|
6月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
2天前
|
SQL Java 数据库连接
|
7月前
|
缓存 应用服务中间件 nginx
Web服务器的缓存机制与内容分发网络(CDN)
【8月更文第28天】随着互联网应用的发展,用户对网站响应速度的要求越来越高。为了提升用户体验,Web服务器通常会采用多种技术手段来优化页面加载速度,其中最重要的两种技术就是缓存机制和内容分发网络(CDN)。本文将深入探讨这两种技术的工作原理及其实现方法,并通过具体的代码示例加以说明。
698 1
|
2月前
|
存储 缓存 NoSQL
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应
|
2月前
|
缓存 NoSQL 关系型数据库
云端问道21期实操教学-应对高并发,利用云数据库 Tair(兼容 Redis®)缓存实现极速响应
本文介绍了如何通过云端问道21期实操教学,利用云数据库 Tair(兼容 Redis®)缓存实现高并发场景下的极速响应。主要内容分为四部分:方案概览、部署准备、一键部署和完成及清理。方案概览中,展示了如何使用 Redis 提升业务性能,降低响应时间;部署准备介绍了账号注册与充值步骤;一键部署详细讲解了创建 ECS、RDS 和 Redis 实例的过程;最后,通过对比测试验证了 Redis 缓存的有效性,并指导用户清理资源以避免额外费用。
|
3月前
|
SQL 关系型数据库 API
HarmonyOs开发:关系型数据库封装之增删改查
每个方法都预留了多种调用方式,比如使用callback异步回调或者使用Promise异步回调,亦或者同步执行,大家在使用的过程中,可以根据自身业务需要进行选择性调用,也分别暴露了成功和失败的方法,可以针对性的判断在执行的过程中是否执行成功。
138 13
|
3月前
|
缓存 NoSQL Serverless
云数据库Tair:从稳定低延时缓存到 Serverless KV
本次分享聚焦云数据库Tair的使用,涵盖三部分内容:1) Tair概览,介绍其作为稳定低延时缓存及KV数据库服务的特点和优势;2) 稳定低延迟缓存技术,探讨如何通过多线程处理、优化内核等手段提升性能与稳定性;3) 从缓存到Serverless KV的演进,特别是在AI大模型时代,Tair如何助力在线服务和推理缓存加速。Tair在兼容性、性能优化、扩缩容及AI推理加速方面表现出色,满足不同场景需求。
|
3月前
|
缓存 物联网 数据库
InfluxDB vs TDengine :2025 年了,谁家用的数据库还不能高效读缓存?
在工业互联网和物联网的大数据应用场景中,实时数据的写入和查询性能至关重要。如何快速获取最新设备状态并实时处理数据,直接影响到业务的高效运转。本文将深入分析 TDengine 和 InfluxDB 在缓存机制上的差异,帮助读者更好地理解这两款主流时序数据库在性能优化方面的优劣。
288 1

热门文章

最新文章

  • 1
    uniapp云打包ios应用证书的获取方法,生成指南
    31
  • 2
    iOS|解决 setBrightness 调节屏幕亮度不生效的问题
    118
  • 3
    iOS|记一名 iOS 开发新手的前两次 App 审核经历
    21
  • 4
    iOS各个证书生成细节
    36
  • 5
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    169
  • 6
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    56
  • 7
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    76
  • 8
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    57
  • 9
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    67
  • 10
    uniapp开发ios打包Error code = -5000 Error message: Error: certificate file(p12) import failed!报错问题如何解决
    176