iOS數據庫離線緩存思路和網絡層封裝
阿新 • • 發佈:2017-08-17
integer 離線 super mat mode dict 數據緩存 req center
一直想總結一下關於iOS的離線數據緩存的方面的問題,然後近期也簡單的對AFN進行了再次封裝。全部想把這兩個結合起來寫一下。數據展示型的頁面做離線緩存能夠有更好的用戶體驗,用戶在離線環境下仍然能夠獲取一些數據。這裏的數據緩存首選肯定是SQLite,輕量級。對數據的存儲讀取相對於其它幾種方式有優勢,這裏對AFN的封裝沒有涉及太多業務邏輯層面的需求。主要還是對一些方法再次封裝方便使用。解除項目對第三方的耦合性。能夠簡單的高速的更換底層使用的網絡請求代碼。這篇主要寫離線緩存思路。對AFN的封裝僅僅做簡單的介紹。
關於XLNetworkApi
XLNetworkApi的一些功能和說明:
- 使用XLNetworkRequest做一些GET、POST、PUT、DELETE請求,與業務邏輯對接部分直接以數組或者字典的形式返回。
- 以及網絡下載、上傳文件。以block的形式返回實時的下載、上傳進度,上傳文件參數通過模型XLFileConfig去存取。
-
通過繼承於XLDataService來將一些數據處理,模型轉化封裝起來。於業務邏輯對接返回的是相應的模型,降低Controllor處理數據處理邏輯的壓力。
-
自己定義一些回調的block
/** 請求成功block */ typedef void (^requestSuccessBlock)(id responseObj); /** 請求失敗block */
-
XLNetworkRequest.m部分實現
#import "XLNetworkRequest.h"
-
下載部分代碼
//下載文件,監聽下載進度 + (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 - 須要註意的是。上拉載入很多其它的時候。每次從數據庫返回一定數量的數據,而不是一次性將數據所有載入,否則會有內存問題。直到數據庫中沒有很多其它數據時再發生網絡請求,再次將新數據存入數據庫。這裏存儲數據的方式是將server返回每組數據的字典歸檔成二進制作為數據庫字段直接存儲,這樣存儲在模型屬性比較多的情況下更有優勢,避免每個屬性作為一個字段,另外添加了一個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
有考慮不周的地方,希望大家能提出一些意見,非常樂意與大家互相交流。
iOS數據庫離線緩存思路和網絡層封裝