鍍金池/ 教程/ iOS/ 用 SQLite 和 FMDB 替代 Core Data
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

用 SQLite 和 FMDB 替代 Core Data

憑良心講,我不能告訴你不去使用 Core Data。它不錯(cuò),而且也在變得更好,并且它被很多其他 Cocoa 開發(fā)者所理解,當(dāng)有新人加入你的團(tuán)隊(duì)或者需要?jiǎng)e人接手你的 app 的時(shí)候,這點(diǎn)很重要。

更重要的是,不值得花時(shí)間和精力去寫自己的系統(tǒng)去代替它。使用 Core Data 吧。真的。

為什么我不使用Core Data

Mike Ash 寫到

就個(gè)人而言,我不是個(gè)狂熱粉絲。我發(fā)現(xiàn) (Core Data 的) API 是笨拙的,并且框架本身對(duì)于超過一定數(shù)量級(jí)的數(shù)據(jù)的處理是極其緩慢的。

一個(gè)實(shí)際的例子:10,000 個(gè)條目

想象一個(gè) RSS 閱讀器,一個(gè)用戶可以在一個(gè) feed 上點(diǎn)擊右鍵,并且選擇標(biāo)記所有為已讀。

實(shí)際實(shí)現(xiàn)上,我們有一個(gè)帶有 read 屬性的 Article 實(shí)體。把所有條目標(biāo)記為已讀,app 需要加載這個(gè) feed 的所有文章 (可能通過一對(duì)多的關(guān)系),然后設(shè)置 read 屬性為 YES。

大部分時(shí)候這樣是沒問題的。但是設(shè)想那個(gè) feed 有 200 篇文章,為了避免阻塞主線程,你可能考慮在后臺(tái)線程里做這個(gè)工作 (尤其是如果這個(gè) app 是一個(gè) iPhone app)。一旦你開始使用 Core Data 多線程的時(shí)候,事情就開始變的不好處理了。

這可能還沒這么糟糕,至少不值得拋棄使用 Core Data。

但是,再添加同步。

我用過兩個(gè)不同的 RSS 同步 API,它們返回已讀文章的 uniqueID 數(shù)組。其中一個(gè)返回近 10,000 個(gè) ID。

你不會(huì)打算在主線程中加載 10,000 篇文章,然后設(shè)置 read 為 NO。你大概也不會(huì)想在后臺(tái)線程里加載 10,000 篇文章,即使很小心地管理內(nèi)存。這里有太多的工作(如果你頻繁的這么做,想一下對(duì)電池壽命的影響)。

概念上來說,你真正想要做的是,讓數(shù)據(jù)庫將 uniqueID 列表里的每一篇文章的 read 設(shè)置為 YES。

SQLite 可以做到這個(gè),只用一次調(diào)用。如果 uniqueID 上有索引,這會(huì)很快。而且你可以在后臺(tái)線程執(zhí)行,這和在主線程執(zhí)行一樣容易。

另一個(gè)例子:快速啟動(dòng)

我的另一個(gè) app,我想減少啟動(dòng)時(shí)間 — 不只是 app 的啟動(dòng)時(shí)間,還有數(shù)據(jù)顯示之前所需要的時(shí)間。

這是個(gè)類似 Twitter 的 app (雖然它不是):它顯示消息的時(shí)間軸。顯示時(shí)間軸意味著獲取消息,并加載相關(guān)用戶。它很快,但是在啟動(dòng)的時(shí)候,會(huì)填充 UI,然后填充數(shù)據(jù)。

關(guān)于 iPhone app(或者所有應(yīng)用),我的理論是,啟動(dòng)時(shí)間比其他大部分開發(fā)者想的都要重要。啟動(dòng)時(shí)間很慢的 app 是不太可能被啟動(dòng)的,因?yàn)槿藗儩撘庾R(shí)里會(huì)記住,并且在啟動(dòng)那個(gè)應(yīng)用這件事情上形成一種抵抗心理。減少啟動(dòng)時(shí)間可以減少這種阻力,用戶也會(huì)更愿意使用你的應(yīng)用,并且把它推薦給其他人。這是你讓你的 app 成功的一部分。

因?yàn)槲也皇褂?Core Data,我手頭有一個(gè)簡單的,保守的解決方案。我把時(shí)間軸(消息和人物對(duì)象)通過 NSCoding 保存到一個(gè) plist 文件中。啟動(dòng)的時(shí)候它讀取這個(gè)文件,創(chuàng)建消息和人物對(duì)象,UI 一出現(xiàn)就顯示時(shí)間軸。

這明顯的減少了延遲。

把消息和人物對(duì)象作為 NSManagedObject 的實(shí)例對(duì)象,這是不可能的。(假設(shè)我已經(jīng)編碼并且存儲(chǔ)對(duì)象的 IDs,但是那意味著讀取 plist 文件,之后再涉及數(shù)據(jù)庫。這種方式我完全避免了數(shù)據(jù)庫)。

(在更新更快的機(jī)器出來后, 我去掉了那些代碼。回顧過去,我希望我可以把它留下來。)

我怎么考慮這個(gè)問題

當(dāng)考慮是否使用 Core Data,我考慮下面這些事情:

會(huì)有難以置信數(shù)量的數(shù)據(jù)嗎?

對(duì)于一個(gè) RSS 閱讀器或者 Twitter app,答案顯而易見:是的。有些人關(guān)注上百個(gè)人。一個(gè)人可能訂閱了上千個(gè) feed。

即使你的應(yīng)用不從網(wǎng)絡(luò)獲取數(shù)據(jù),用戶仍然有可能自動(dòng)添加數(shù)據(jù)。如果你用一個(gè)支持 AppleScript 的 Mac,有人會(huì)寫腳本去加載非常多的數(shù)據(jù)。如果通過 web API 去添加數(shù)據(jù)也是一樣的。

會(huì)有一個(gè) Web API 包含類似于數(shù)據(jù)庫的結(jié)果嗎(對(duì)比于類似對(duì)象的結(jié)果)?

一個(gè) RSS 同步 API 能夠返回一個(gè)已讀文章的 uniqueID 列表。一個(gè)筆記的應(yīng)用的一個(gè)同步 API 可能返回已存檔的和已刪除的筆記的 uniqueID 列表。

用戶可能通過操作處理大量對(duì)象嗎?

在底層,需要考慮和之前一樣的問題。當(dāng)有人刪除所有已經(jīng)下載的 5,000 個(gè)面食食譜,你的食譜 app 性能如何?(在 iPhone 上?)

如果我決定使用 Core Data(我已經(jīng)發(fā)布過使用 Core Data 的應(yīng)用),我會(huì)特別注意我如何使用它。結(jié)果為了得到好的性能,我發(fā)現(xiàn)我把它當(dāng)做了一個(gè)奇怪接口的 SQL 數(shù)據(jù)庫在使用,然后我就知道了,我應(yīng)該舍棄 Core Data,而去直接使用 SQLite。

我如何使用 SQLite

我通過 FMDB Wrapper 來使用 SQLite,F(xiàn)MDB 來自 Flying Meat Software,由 Gus Mueller 開發(fā)。

基本操作

在使用 iPhone 和 Core Data 之前,我就使用過 SQLite。這里有關(guān)于它如何工作的要點(diǎn):

  • 所有數(shù)據(jù)庫訪問 - 讀和寫 - 發(fā)生在一個(gè)后臺(tái)線程的連續(xù)的隊(duì)列里。在主線程中觸及數(shù)據(jù)庫是從來不被允許的。使用一個(gè)連續(xù)隊(duì)列來保證每一件事是按順序發(fā)生的。
  • 我大量使用 blocks 使得異步編程容易些。
  • 模型對(duì)象只存在在主線程(但有兩個(gè)重要的例外),改變會(huì)觸發(fā)一個(gè)后臺(tái)保存。
  • 模型對(duì)象列出來它們?cè)跀?shù)據(jù)庫中存儲(chǔ)的屬性。這可能在代碼里或者在 plist 文件里。
  • 有些模型對(duì)象是唯一的,有些不是。取決于 app 的需要(大部分情況是唯一的)。
  • 對(duì)關(guān)系型數(shù)據(jù),我盡可能避免創(chuàng)建查詢表。
  • 一些對(duì)象類型在啟動(dòng)的時(shí)候就完全讀入內(nèi)存,另一些對(duì)象類型我可能創(chuàng)建和維護(hù)的只有它們 uniqueID 的一個(gè) NSMutableSet,所以我可以在不去碰數(shù)據(jù)庫的情況下就知道什么存在、什么不存在。
  • Web API 的調(diào)用發(fā)生在后臺(tái)線程,它們使用“分離“的模型對(duì)象。

我會(huì)使用我目前的 app 的代碼來描述。

數(shù)據(jù)庫更新

在我最近的 app 中,有一個(gè)單一的數(shù)據(jù)庫控制器 - VSDatabaseController,它通過 FMDB 來與 SQLite 對(duì)話。

FMDB 區(qū)分更新和查詢。更新數(shù)據(jù)庫,app 調(diào)用:

-[VSDatabaseController runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock]

VSDatabaseUpdateBlock很簡單:

typedef void (^VSDatabaseUpdateBlock)(FMDatabase *database);

runDatabaseBlockInTransaction也很簡單:

- (void)runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock {
    dispatch_async(self.serialDispatchQueue, ^{
        @autoreleasepool {
            [self beginTransaction];
            databaseBlock(self.database);
            [self endTransaction];
        }
    });
}

(注意我用的自己的連續(xù) dispatch 隊(duì)列。Gus 建議看一下 FMDatabaseQueue,這也是一個(gè)連續(xù)調(diào)度隊(duì)列。因?yàn)樗?FMDB 剩下的其他東西都要新,所以我自己還沒有去看過。)

beginTransactionendTransaction 的調(diào)用是可嵌套的(在我的數(shù)據(jù)庫控制器里)。在合適的時(shí)候他們會(huì)調(diào)用 -[FMDatabase beginTransaction]-[FMDatabase commit]。(使用 transactions 是讓 SQLite 變快的一大關(guān)鍵。)提示:我在 -[NSThread threadDictionary] 中存儲(chǔ)當(dāng)前的 transaction 的計(jì)數(shù)。這對(duì)于針對(duì)每個(gè)線程的數(shù)據(jù)來說是很方便的,我也幾乎從不用它做其他的事情。

這兒有個(gè)調(diào)用更新數(shù)據(jù)庫的簡單例子:

- (void)emptyTagsLookupTableForNote:(VSNote *)note {
    NSString *uniqueID = note.uniqueID;
    [self runDatabaseBlockInTransaction:^(FMDatabase *database) {
        [database executeUpdate:
            @"delete from tagsNotesLookup where noteUniqueID = ?;", uniqueID];
    }];
}

這說明了不少事情。首先, SQL 并不可怕。即使你從沒見過它,你也知道這行代碼做了什么。

VSDatabaseController 的所有其他公共接口一樣,emptyTagsLookupTableForNote 也應(yīng)該在主線程中被調(diào)用。模型對(duì)象只能在主線程中被引用,所以在 block 中使用 uniqueID ,而不是 VSNote 對(duì)象。

注意在這種情況下,我更新了一個(gè)查詢表。Notes 和 tags 有一個(gè)多對(duì)多關(guān)系,一種表現(xiàn)方式是用一個(gè)數(shù)據(jù)庫表映射 note uniqueIDs 和 tag uniqueIDs。這些表不會(huì)很難維護(hù),但是如果可能,我盡量避免使用它們。

注意在更新字符串中的 ?。-[FMDatabase executeUpdate:] 是一個(gè)可變參數(shù)函數(shù)。SQLite 支持使用占位符 - ? 字符 - 所以你不需要把實(shí)際的值放入字符串中去。這是一個(gè)安全上的考量:它可以守護(hù)程序避免 SQL 注入。它也可以幫助你減少必須 escape 值這樣的不必要的麻煩。

最后,注意在 tagsNotesLookup 表中,有一個(gè) noteUniquelID 的索引(索引是 SQLite 性能的又一個(gè)關(guān)鍵)。這行代碼在每次啟動(dòng)時(shí)都調(diào)用:

[self.database executeUpdate:
    @"CREATE INDEX if not exists noteUniqueIDIndex on tagsNotesLookup (noteUniqueID);"];

數(shù)據(jù)庫獲取

要獲取對(duì)象,app 調(diào)用:

-[VSDatabaseController runFetchForClass:(Class)databaseObjectClass 
                             fetchBlock:(VSDatabaseFetchBlock)fetchBlock 
                      fetchResultsBlock:(VSDatabaseFetchResultsBlock)fetchResultsBlock];

這兩行代碼做了大部分工作:

FMResultSet *resultSet = fetchBlock(self.database);
NSArray *fetchedObjects = [self databaseObjectsWithResultSet:resultSet 
                                                       class:databaseObjectClass];

用 FMDB 查找數(shù)據(jù)庫返回一個(gè) FMResultSet. 通過 resultSet 你可以逐句循環(huán),創(chuàng)建模型對(duì)象。

我建議寫通用的代碼去將數(shù)據(jù)庫中的行轉(zhuǎn)換為對(duì)象。一種我已經(jīng)使用的方法是在 app 中用一個(gè) plist 文件,將列的名字映射到模型對(duì)象的屬性上去。它也包含類型,所以你知道是調(diào)用 -[FMResultSet dateForColumn:]還是 -[FMResultSet stringForColumn:]或是其他方法。

在我的最新 app 里我做的事情更簡單。數(shù)據(jù)庫行剛好對(duì)應(yīng)模型對(duì)象屬性的名字。除了那些名字以 “Date” 結(jié)尾的屬性以外,所有屬性都是字符串。簡單,但是你可以看到所需要明顯清晰的對(duì)應(yīng)關(guān)系。

唯一對(duì)象

創(chuàng)建模型對(duì)象的操作和從數(shù)據(jù)庫獲取數(shù)據(jù)操作在同樣的后臺(tái)線程進(jìn)行。一但獲取到,app 會(huì)把它們轉(zhuǎn)到主線程。

通常我會(huì)使用唯一對(duì)象。數(shù)據(jù)庫里的同一行,始終對(duì)應(yīng)著同樣的一個(gè)對(duì)象。

為了做到唯一,我使用 NSMapTable 創(chuàng)建了一個(gè)對(duì)象緩存,在 init 函數(shù)里:_objectCache = [NSMapTable weakToWeakObjectsMapTable]。我來解釋一下:

例如,當(dāng)你進(jìn)行一個(gè)數(shù)據(jù)庫獲取操作并且把對(duì)象轉(zhuǎn)交給一個(gè)視圖控制器時(shí),你希望在這個(gè)視圖控制器使用完這些對(duì)象后,或者在一個(gè)不一樣的視圖控制器被顯示后,這些對(duì)象可以消失。

如果你的對(duì)象緩存是一個(gè) NSMutableDictionary,那你將需要做一些額外的工作來清空緩存中的對(duì)象。保證它只引用了那些其他地方有引用的對(duì)象是一件非常讓人蛋疼的事情。而使用配合弱引用的NSMapTable,這個(gè)問題就被自動(dòng)處理掉了。

所以:我們?cè)谥骶€程中讓對(duì)象唯一。如果一個(gè)對(duì)象已經(jīng)在對(duì)象緩存中存在,我們就用那個(gè)存在的對(duì)象。(因?yàn)橹骶€程中對(duì)象可能有改變,因此在沖突時(shí)我們使用主線程的對(duì)象。)如果對(duì)象緩存中沒有,它會(huì)被加上。

保持對(duì)象在內(nèi)存中

有很多次,把整個(gè)對(duì)象類型保留在內(nèi)存中是有道理的。我最新的 app 有一個(gè) VSTag 對(duì)象。雖然可能有成百上千篇筆記,但 tags 的數(shù)量很小,基本少于十個(gè)。一個(gè) tag 只有 6 個(gè)屬性:三個(gè) BOOL,兩個(gè)很小的 NSstring,還有一個(gè) NSDate。

啟動(dòng)的時(shí)候,app 獲取所有 tags 并且把它們保存在兩個(gè)字典里,其中一個(gè)的鍵是 tag 的 uniqueID,另一個(gè)的鍵是 tag 名字的小寫。

這簡化了很多事,比如 tag 自動(dòng)補(bǔ)全系統(tǒng),就可以完全在內(nèi)存中操作,而不需要從數(shù)據(jù)庫獲取了。

但是很多次,把所有數(shù)據(jù)保留在內(nèi)存中是不實(shí)際的。比如我們不會(huì)在內(nèi)存中保留所有筆記。

但是也有很多次,把所有對(duì)象保存在內(nèi)存中是不可行的。當(dāng)不能在內(nèi)存中保留一個(gè)對(duì)象類型時(shí),你可能會(huì)希望在內(nèi)存中保留所有 uniqueID,你可以進(jìn)行這樣一個(gè)獲取操作:

FMResultSet *resultSet = [self.database executeQuery:@"select uniqueID from some_table"];

resultSet 只包含了 uniqueIDs, 你可以存儲(chǔ)到一個(gè) NSMutableSet 里。

我發(fā)現(xiàn)有時(shí)這個(gè)對(duì) web APIs 很有用。想象一個(gè) API 返回從某個(gè)確定的時(shí)間以后所創(chuàng)建筆記的 uniqueIDs 列表。如果我本地已經(jīng)有了一個(gè)包含所有筆記 uniqueIDs 的 NSMutableSet,我可以 (通過 -[NSMutableSet minusSet]) 快速檢查是否有漏掉的筆記,然后去調(diào)用另一個(gè) API 下載那些漏掉的筆記。這些完全不需要觸及數(shù)據(jù)庫。

但是,像這樣的事情應(yīng)該小心處理。app 可以提供足夠的內(nèi)存嗎?它真的簡化編程并且提高性能了嗎?

使用 SQLite 和 FMDB 來代替 Core Data,會(huì)給你帶來大量的靈活性和使用更聰明的辦法來解決問題的空間。記住有的時(shí)候聰明是好的,也有的時(shí)候聰明是一個(gè)大錯(cuò)誤。

Web APIs

我的 API 調(diào)用都跑在后臺(tái)進(jìn)程里(通常是用一個(gè) NSOperationQueue,這樣我可以取消操作)。模型對(duì)象只在主線程,然后將模型對(duì)象傳遞給我的 API 調(diào)用。

具體這么做:一個(gè)數(shù)據(jù)庫對(duì)象有一個(gè) detachedCopy 方法,可以復(fù)制數(shù)據(jù)庫對(duì)象。這個(gè)復(fù)制的對(duì)象不會(huì)被我用來做唯一化的對(duì)象緩存所引用。唯一引用這個(gè)對(duì)象的地方是 API 調(diào)用,當(dāng) API 調(diào)用結(jié)束時(shí),這個(gè)復(fù)制的對(duì)象也就消失了。

這是一個(gè)好的系統(tǒng),因?yàn)樗馕吨铱梢栽?API 調(diào)用里使用模型對(duì)象。方法看起來像這樣:

- (void)uploadNote:(VSNote *)note {
    VSNoteAPICall *apiCall = [[VSNoteAPICall alloc] initWithNote:[note detachedCopy]];
    [self enqueueAPICall:apiCall];
}

VSNoteAPICall 從分離出來的 VSNote 中獲取值,并且創(chuàng)建 HTTP 請(qǐng)求,而不是將 note 包裝成一個(gè)字典或其他表現(xiàn)形式。

處理 Web API 的返回值

我對(duì) web 的返回值做了一些類似的處理。我會(huì)對(duì)返回的 JSON 或者 XML 創(chuàng)建一個(gè)模型對(duì)象,這個(gè)模型對(duì)象也是分離的。它沒有存儲(chǔ)在唯一化模型緩存里。

這里有些事情是不確定的。有時(shí)我們需要用那個(gè)模型對(duì)象在在內(nèi)存緩存以及數(shù)據(jù)庫兩個(gè)地方做本地修改。

數(shù)據(jù)庫通常是容易的部分。比如:我的 app 已經(jīng)有一個(gè)方法來保存筆記對(duì)象。它使用 SQL 的 insert or replace 命令。我只需用從 web API 返回值所生成的筆記對(duì)象來進(jìn)行調(diào)用,數(shù)據(jù)庫就會(huì)更新。

但是可能同樣的對(duì)象在內(nèi)存中還有一個(gè)版本,幸運(yùn)的是我們很容易找到它:

VSNote *cachedNote = [self.mapTable objectForKey:downloadedNote.uniqueID];

如果 cachedNote 存在,我會(huì)讓它從 downloadedNote中 獲取值(這部分可以共享 detachedCopy 方法的代碼。),而不是直接替換它(這樣可能違反唯一性)。

一旦 cachedNote 更新了,觀察者會(huì)通過 KVO 察覺到變化,或者我會(huì)發(fā)送一個(gè) NSNotification,或者兩者都做。

Web API 調(diào)用也會(huì)返回一些其他值。我提到過 RSS 閱讀器可能獲得一個(gè)已讀條目的大列表。這種情況下,我選擇通過那個(gè)列表創(chuàng)建一個(gè) NSSet,在內(nèi)存的緩存中更新每一個(gè)緩存文章的 read 屬性,然后調(diào)用 -[FMDatabase executeUpdate:]。

完成這個(gè)工作的關(guān)鍵是 NSMapTable 的查找是快速的。如果你找的對(duì)象在一個(gè) NSArray 里,我們就得重新考慮考慮了。

數(shù)據(jù)庫遷移

當(dāng)正常工作的時(shí)候,Core Data 的數(shù)據(jù)庫遷移功能還是蠻酷的。

但是不可避免的,它在代碼和數(shù)據(jù)庫中加入了一層。如果你更直接一點(diǎn),去使用 SQLite,那么更新數(shù)據(jù)庫也就變得越直接。

你可以安全容易的做到這點(diǎn)。

比如加一個(gè)表:

[self.database executeUpdate:@"CREATE TABLE if not exists tags "
    "(uniqueID TEXT UNIQUE, name TEXT, deleted INTEGER, deletedModificationDate DATE);"];

或添加一個(gè)索引

[self.database executeUpdate:@"CREATE INDEX if not exists "
    "archivedSortDateIndex on notes (archived, sortDate);"];

或添加一列:

[self.database executeUpdate:@"ALTER TABLE tags ADD deletedDate DATE"];

app 應(yīng)該用類似上面這樣的代碼來首先對(duì)數(shù)據(jù)庫進(jìn)行設(shè)置。以后的改變就是添加對(duì) executeUpdate 的調(diào)用 — 我讓他們按順序執(zhí)行。因?yàn)槲业臄?shù)據(jù)庫是我設(shè)計(jì)的,所以這不會(huì)有什么問題(我從沒碰到性能問題,它很快)。

當(dāng)然大的改變需要更多代碼。如果你的數(shù)據(jù)通過 web 獲取,有時(shí)你可以從一個(gè)新數(shù)據(jù)庫模型開始,重新下載你需要的數(shù)據(jù)。

性能技巧

SQLite 可以非常非??欤且部梢苑浅B?。完全取決于你怎么使用它。

事務(wù)

把更新包裝在事務(wù)里。在更新前調(diào)用 -[FMDatabase beginTransaction],更新后調(diào)用 -[FMDatabase commit]。

如果你不得不反規(guī)范化( Denormalize)

反規(guī)范化讓人很不爽。這個(gè)方法是,為了加速檢索而添加冗余數(shù)據(jù),但是它意味著你需要維護(hù)冗余數(shù)據(jù)。

我總是盡力避免它,直到這樣能有嚴(yán)重的性能差異。然后我會(huì)盡可能少得這么做。

使用索引

我的 app 中 tags 表的創(chuàng)建語句像這樣:

CREATE TABLE if not exists tags 
  (uniqueID TEXT UNIQUE, name TEXT, deleted INTEGER, deletedModificationDate DATE);

uniqueID 列是自動(dòng)索引的,因?yàn)樗x為 unique。但是如果我想用 name 來查詢表,我可能會(huì)在name上創(chuàng)建一個(gè)索引,像這樣:

CREATE INDEX if not exists tagNameIndex on tags (name);

你可以一次性在多列上創(chuàng)建索引,像這樣:

CREATE INDEX if not exists archivedSortDateIndex on notes (archived, sortDate);

但是注意太多索引會(huì)降低你的插入速度。你只需要足夠數(shù)量并且是正確的那些。

使用命令行應(yīng)用

當(dāng)我的 app 在模擬器里運(yùn)行時(shí),我會(huì)用 NSLog 輸出數(shù)據(jù)庫的路徑。我可以通過 sqlite3 的命令行來打開數(shù)據(jù)庫。(通過 man sqlite3 命令來了解這個(gè)應(yīng)用的更多信息)。

打開數(shù)據(jù)庫的命令:sqlite3 path/to/database。

打開以后,你可以輸入 .schema 來查看 schema。

你可以更新和查詢,這是在你的 app 使用 SQL 之前就將它們正確地準(zhǔn)備妥當(dāng)?shù)暮芎玫姆绞健?/p>

這里面最酷的一部分是,SQLite Explain Query Plan 命令,你會(huì)希望確保你的語句執(zhí)行的盡可能快。

真實(shí)的例子

我的 app 顯示所有沒有歸檔筆記的標(biāo)簽列表。每當(dāng)筆記或者標(biāo)簽有變化,這個(gè)查詢就會(huì)重新執(zhí)行一次,所以它需要很快。

我可以用 SQL join 來查詢,但是這會(huì)很慢(join 都很慢)。

所以我放棄 sqlite3 并開始嘗試別的方法。我又檢查了一次我的 schema,意識(shí)到我可以反規(guī)范化。一個(gè)筆記的歸檔狀態(tài)可以存儲(chǔ)在 notes 表里,它也可以存儲(chǔ)在 tagsNotesLookup 表。

然后我可以執(zhí)行一個(gè)查詢:

select distinct tagUniqueID from tagsNotesLookup where archived=0;

我已經(jīng)有了一個(gè)在 tagUniqueID 上的索引。所以我用 explain query plan 來告訴我當(dāng)我執(zhí)行這個(gè)查詢的時(shí)候會(huì)發(fā)生什么。

sqlite> explain query plan select distinct tagUniqueID from tagsNotesLookup where archived=0;
0|0|0|SCAN TABLE tagsNotesLookup USING INDEX tagUniqueIDIndex (~100000 rows)

它用了一個(gè)索引,這很不錯(cuò),但是 SCAN TABLE 聽起來不太好,最好是一個(gè) SEARCH TABLE 加上覆蓋索引的方式。

我在 tagUniqueID 和 archive 上建了索引:

CREATE INDEX archivedTagUniqueID on tagsNotesLookup(archived, tagUniqueID);

再次執(zhí)行 explain query plan:

sqlite> explain query plan select distinct tagUniqueID from tagsNotesLookup where archived=0;
0|0|0|SEARCH TABLE tagsNotesLookup USING COVERING INDEX archivedTagUniqueID (archived=?) (~10 rows)

現(xiàn)在好多了。

更多性能提示

FMDB 的某處加了緩存 statements 的能力,所以當(dāng)創(chuàng)建或打開一個(gè)數(shù)據(jù)庫的時(shí)候,我總是調(diào)用 [self.database setShouldCacheStatements:YES]。這意味著對(duì)每個(gè)調(diào)用你不需要再次編譯每個(gè) statement。

我從來沒有找到關(guān)于使用 vacuum 的好的指引。如果數(shù)據(jù)庫沒有定期壓縮,它會(huì)變得越來越慢。我的 app 會(huì)每周跑一次 vacuum。(在 NSUserDefaults 里存儲(chǔ)上次 vacuum 的時(shí)間,然后在開始的時(shí)候檢查是否過了一周)。

使用 auto_vacuum 可能會(huì)更好,可以參看 pragma statements supported by SQLite 列表。

其他酷的東西

Gus Mueller 讓我講講自定義 SQLite 方法的內(nèi)容。我并沒有真的使用過這些東西,不過既然他指出了,我可以放心的說我能找到它的用處。因?yàn)樗芸帷?/p>

在 Gus 的這個(gè) gist 里,有一個(gè)查詢是這樣的:

select displayName, key from items where UTTypeConformsTo(uti, ?) order by 2;

SQLite 完全不知道 UTTypes 的事情。但是你可以通過代碼塊來添加核心方法,感興趣的話,可以看看 -[FMDatabase makeFunctionNamed:maximumArguments:withBlock:] 方法。

你可以執(zhí)行一個(gè)大的查詢來替代,然后評(píng)估每個(gè)對(duì)象 - 但是那需要更多工作。最好在 SQL 級(jí)就過濾,而不是在將表格行轉(zhuǎn)為對(duì)象以后再做這件事情。

最后

你真的應(yīng)該使用 Core Data,我不是在開玩笑。

我用 SQLite 和 FMDB 一段時(shí)間了,我對(duì)多得到的好處感到很興奮,也得到非同一般的性能。

但是記住設(shè)備在不斷變快。也請(qǐng)記住,其他看你代碼的人期望看到 Core Data,這是他們已經(jīng)了解的 - 他們不打算看你的數(shù)據(jù)庫代碼如何工作。

所以請(qǐng)把這整篇文章看做一個(gè)瘋子的叫喊,關(guān)于他為自己建立了充滿細(xì)節(jié)又瘋狂的世界 - 并把自己鎖在了里面。

有點(diǎn)難過的搖頭,并且請(qǐng)享受這個(gè)話題下那些超贊的 Core Data 的文章吧。

而對(duì)我來說,接下來在研究完 Gus 指出的自定義 SQLite 方法特性后,我會(huì)研究 SQLite 的 全文搜索擴(kuò)展。 總有更多的內(nèi)容需要不斷去學(xué)習(xí)。

上一篇:測試下一篇:字符串解析