前言:如果你准备看这篇文章,并且在此之前并没有使用过SQLite,建议先看 SQLite数据库框架之FMDB ,其中有用到SQL语句,如果不会的可以先可以看 iOS数据库操作中的常见SQL语句 。
封装思想
在讲解SQLite之前,先聊聊封装思想。。写代码的都知道,面向对象三大思想(有些说四大,这个不是重点)之一的封装性,这里就举一个栗子来说明一下什么是封装。
比如控制器需要数据,控制器就跟模型说,我要数据,请给我数据。对于数据是怎么来的,控制器并不关心,控制器只关心模型有没有给他数据。
然后模型呢?控制器找模型要数据,模型也没有啊,所以模型去找数据访问层要数据,数据访问层的数据是怎么来的,模型也不关心,他关心的也只是有没有给他数据。
然后数据访问层呢?数据访问层自己也是没有数据的,他也得去找他的上一级要数据啊。
找谁要呢?这里就要分两种情况,第一种是本地有数据,他将本地数据直接返回给模型,模型再返回给我。第二种情况是本地没有数据,他就去网络请求类要,最终网络请求类才和后端交互,去后端请求数据,并将数据请求回来后保存到本地缓存,并返回给数据访问层,数据访问层再返回给模型,模型返回给控制器。
所以,这样就形成了一个数据请求和数据响应的链条:
上面栗子中提到了数据访问层(Data Access Layer),可能有些朋友对这个词比较陌生,你就当做是模型和数据之间的桥梁就行了。
需求分析
这里我以 六阿哥客户端 的(首页列表数据)为例,来一步步实现数据缓存。
需求:列表数据如果本地有缓存,就直接加载本地数据,如果没有才去请求网络。并且在下拉刷新的时候清除列表的缓存数据,并把新请求回来的数据缓存到本地。
这里涉及到数据处理的类有,网络工具类、数据访问层类、列表模型类、控制器。当然我们这里并不是直接使用SQLite类库来缓存数据,而是通过FMDB(对SQLite进行封装的第三方类库),你也可以通过系统的CoreData来缓存数据。不过,我个人更喜欢FMDB,直接通过SQL语言管理数据。
代码实现
在进行数据缓存前,我们最好先对FMDB进行一次封装,让我们用起来更方便。这里我创建一个工具类JFSQLiteManager,直接贴代码,如果不熟悉FMDB的,建议先看 SQLite数据库框架之FMDB 。
import UIKit import FMDB let NEWS_LIST_HOME_LIST = "jf_newslist_homelist" // 首页 列表页 的 列表 数据表 class JFSQLiteManager: NSObject { /// FMDB单例 static let shareManager = JFSQLiteManager() /// sqlite数据库名 private let newsDBName = "news.db" let dbQueue: FMDatabaseQueue override init() { let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! let dbPath = "\(documentPath)/\(newsDBName)" print(dbPath) // 根据路径创建并打开数据库,开启一个串行队列 dbQueue = FMDatabaseQueue(path: dbPath) super.init() // 创建数据表 createNewsListTable(NEWS_LIST_HOME_LIST) } private func createNewsListTable(tbname: String) { let sql = "CREATE TABLE IF NOT EXISTS \(tbname) ( \n" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \n" + "classid INTEGER, \n" + "news TEXT, \n" + "createTime VARCHAR(30) DEFAULT (datetime('now', 'localtime')) \n" + ");" dbQueue.inDatabase { (db) in if db.executeStatements(sql) { print("创建 \(tbname) 表成功") } else { print("创建 \(tbname) 表失败") } } } }
控制器向模型请求数据:
private func loadNews(classid: Int, pageIndex: Int, method: Int) { JFArticleListModel.loadNewsList(classid, pageIndex: pageIndex, type: 1) { (articleListModels, error) in self.tableView.mj_header.endRefreshing() self.tableView.mj_footer.endRefreshing() guard let list = articleListModels where error != true else { return } // id越大,文章越新 let maxId = self.articleList.first?.id ?? "0" let minId = self.articleList.last?.id ?? "0" if method == 0 { // 0下拉加载最新 - 会直接覆盖数据,用最新的10条数据 if Int(maxId) < Int(list[0].id!) { self.articleList = list } } else { // 1上拉加载更多 - 拼接数据 if Int(minId) > Int(list[0].id!) { self.articleList = self.articleList + list } else { self.tableView.mj_footer.endRefreshingWithNoMoreData() } } self.tableView.reloadData() } }
模型向数据访问层请求数据:
class func loadNewsList(classid: Int, pageIndex: Int, type: Int, finished: (articleListModels: [JFArticleListModel]?, error: NSError?) -> ()) { // 模型找数据访问层请求数据 - 然后处理数据回调给调用者直接使用 JFNewsDALManager.shareManager.loadNewsList(classid, pageIndex: pageIndex, type: type) { (result, error) in // 请求失败 if error != nil || result == nil { finished(articleListModels: nil, error: error) return } // 没有数据了 if result?.count == 0 { finished(articleListModels: [JFArticleListModel](), error: nil) return } let data = result!.arrayValue var articleListModels = [JFArticleListModel]() // 遍历转模型添加数据 for article in data { let postModel = JFArticleListModel(dict: article.dictionaryObject!) articleListModels.append(postModel) } finished(articleListModels: articleListModels, error: nil) } }
数据访问层判断是去本地还是网络请求数据:
func loadNewsList(classid: Int, pageIndex: Int, type: Int, finished: (result: JSON?, error: NSError?) -> ()) { // 先从本地加载数据 loadNewsListFromLocation(classid, pageIndex: pageIndex, type: type) { (success, result, error) in // 本地有数据直接返回 if success == true { finished(result: result, error: nil) print("加载了本地数据 \(result)") return } // 本地没有数据才从网络中加载 JFNetworkTool.shareNetworkTool.loadNewsListFromNetwork(classid, pageIndex: pageIndex, type: type) { (success, result, error) in if success == false || error != nil || result == nil { finished(result: nil, error: error) return } // 缓存数据到本地 self.saveNewsListData(classid, data: result!, type: type) finished(result: result, error: nil) print("加载了远程数据 \(result)") } } }
本地请求数据方法:
private func loadNewsListFromLocation(classid: Int, pageIndex: Int, type: Int, finished: NetworkFinished) { var sql = "" // 计算分页 let pre_count = (pageIndex - 1) * 20 let oneCount = 20 if classid == 0 { sql = "SELECT * FROM \(NEWS_LIST_HOME_LIST) ORDER BY id ASC LIMIT \(pre_count), \(oneCount)" } JFSQLiteManager.shareManager.dbQueue.inDatabase { (db) in var array = [JSON]() let result = try! db.executeQuery(sql, values: nil) while result.next() { let newsJson = result.stringForColumn("news") let json = JSON.parse(newsJson) array.append(json) } if array.count > 0 { finished(success: true, result: JSON(array), error: nil) } else { finished(success: false, result: nil, error: nil) } } }
远程请求数据的方法:
func loadNewsListFromNetwork(classid: Int, pageIndex: Int, type: Int, finished: NetworkFinished) { var parameters = [String : AnyObject]() if type == 1 { parameters = [ "classid" : classid, "pageIndex" : pageIndex, // 页码 "pageSize" : 20 // 单页数量 ] } else { parameters = [ "classid" : classid, "query" : "isgood", "pageSize" : 3 ] } JFNetworkTool.shareNetworkTool.get(ARTICLE_LIST, parameters: parameters) { (success, result, error) -> () in guard let successResult = result where success == true else { finished(success: false, result: nil, error: error) return } finished(success: true, result: successResult["data"], error: nil) } }
本地缓存数据的方法:
private func saveNewsListData(saveClassid: Int, data: JSON, type: Int) { var sql = "" if saveClassid == 0 { sql = "INSERT INTO \(NEWS_LIST_HOME_LIST) (classid, news) VALUES (?, ?)" } JFSQLiteManager.shareManager.dbQueue.inTransaction { (db, rollback) in guard let array = data.arrayObject as! [[String : AnyObject]]? else { return } // 每一个字典是一条资讯 for dict in array { // 资讯分类id let classid = Int(dict["classid"] as! String)! // 单条资讯json数据 let newsData = try! NSJSONSerialization.dataWithJSONObject(dict, options: NSJSONWritingOptions(rawValue: 0)) let newsJson = String(data: newsData, encoding: NSUTF8StringEncoding)! if db.executeUpdate(sql, withArgumentsInArray: [classid, newsJson]) { print("缓存数据成功 - \(classid)") } else { print("缓存数据失败 - \(classid)") rollback.memory = true break } } } }
下拉刷新的时候,需要清理当前分类的数据,这里也是调用模型的清理方法:
@objc private func updateNewData() { // 有网络的时候下拉会自动清除缓存 if true { JFArticleListModel.cleanCache(classid!) } loadNews(classid!, pageIndex: 1, method: 0) }
然后模型调用数据访问层的清理方法:
class func cleanCache(classid: Int) { JFNewsDALManager.shareManager.cleanCache(classid) }
最终在数据访问层完成清理操作:
func cleanCache(classid: Int) { var sql = "" if classid == 0 { sql = "DELETE FROM \(NEWS_LIST_HOME_TOP); DELETE FROM \(NEWS_LIST_HOME_LIST);" } JFSQLiteManager.shareManager.dbQueue.inDatabase { (db) in if db.executeStatements(sql) { // print("清空表成功 classid = \(classid)") } else { // print("清空表失败 classid = \(classid)") } } }
代码有些多,请把 六阿哥客户端 下载到本地,根据本文思路去理清比较好。只要理解了上面那张数据请求和数据响应的流程,剩下的都是一些基本代码了。而关于缓存策略,缓存清除和缓存保存,则是按照自己的项目需求来针对性实现。