作者:shelin 投稿。
一直想總結一下關於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 { //網絡不可用 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); }]; }
下載部分代碼
//下載文件,監聽下載進度 + (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]; }
上傳部分代碼
//上傳文件,監聽上傳進度 + (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部分實現
+ (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; }
關於離線數據緩存
當用戶進入程序的展示頁面,有三個情況下可能涉及到數據庫存取操作,簡單畫了個圖來理解,思路比較簡單,主要是一些存取的細節處理。
進入展示頁面
下拉刷新最新數據
上拉加載更多數據
需要注意的是,上拉加載更多的時候,每次從數據庫返回一定數量的數據,而不是一次性將數據全部加載,否則會有內存問題,直到數據庫中沒有更多數據時再發生網絡請求,再次將新數據存入數據庫。這裡存儲數據的方式是將服務器返回每組數據的字典歸檔成二進制作為數據庫字段直接存儲,這樣存儲在模型屬性比較多的情況下更有好處,避免每一個屬性作為一個字段,另外增加了一個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 () { 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
希望大家能提出一些意見,很樂意與大家互相交流。