一直想總結一下關於iOS的離線資料快取的方面的問題,然後最近也簡單的對AFN進行了再次封裝,所有想把這兩個結合起來寫一下。資料展示型的頁面做離線快取可以有更好的使用者體驗,使用者在離線環境下仍然可以獲取一些資料,這裡的資料快取首選肯定是SQLite,輕量級,對資料的儲存讀取相對於其他幾種方式有優勢,這裡對AFN的封裝沒有涉及太多業務邏輯層面的需求,主要還是對一些方法再次封裝方便使用,解除專案對第三方的耦合性,能夠簡單的快速的更換底層使用的網路請求程式碼。這篇主要寫離線快取思路,對AFN的封裝只做簡單的介紹。
關於XLNetworkApi
XLNetworkApi的一些功能和說明:
- 使用XLNetworkRequest做一些GET、POST、PUT、DELETE請求,與業務邏輯對接部分直接以陣列或者字典的形式返回。
- 以及網路下載、上傳檔案,以block的形式返回實時的下載、上傳進度,上傳檔案引數通過模型XLFileConfig去存取。
- 通過繼承於XLDataService來將一些資料處理,模型轉化封裝起來,於業務邏輯對接返回的是對應的模型,減少Controllor處理資料處理邏輯的壓力。
- 自定義一些回撥的block
12345678910111213141516/**請求成功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部分實現
12345678910111213141516171819#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);}];} - 下載部分程式碼
1234567891011121314151617181920212223242526272829303132333435//下載檔案,監聽下載進度+ (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];} - 上傳部分程式碼
123456789101112131415161718192021222324252627282930313233343536//上傳檔案,監聽上傳進度+ (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部分實現
1234567891011+ (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的子類重寫,將轉化為模型的程式碼寫在這裡,相似業務的網路資料請求都可以用這個子類去請求資料,直接返回對應的模型陣列。
123456/**陣列、字典轉化為模型*/+ (id)modelTransformationWithResponseObj:(id)responseObj modelClass:(Class)modelClass {return nil;}關於離線資料快取
當使用者進入程式的展示頁面,有三個情況下可能涉及到資料庫存取操作,簡單畫了個圖來理解,思路比較簡單,主要是一些存取的細節處理。
- 進入展示頁面
進入頁面.png
- 下拉重新整理最新資料
下拉重新整理.png
- 上拉載入更多資料
上拉載入更多.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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
#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 (%<a href="http://www.jobbole.com/members/uz441800">@,</a> %@)",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 22 23 24 |
#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 88 89 90 91 92 93 94 95 96 97 98 |
#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
有考慮不周的地方,希望大家能提出一些意見,很樂意與大家互相交流。