微信終端開源資料庫 WCDB - Swift 版本

張三華發表於2018-01-03

WCDB 作為微信的終端資料庫,從 2017.6 開源至今,共迭代了 5 個版本。我們一直關注開發者們的需求,並不斷優化效能,新增如全文搜尋等常用的功能。而這其中,呼聲最高的莫過於 對 Swift 的支援。

WCDB ObjC 版本的實現中,由於引入了 C++ 程式碼,並不能直接 bridge 到 Swift。因此,我們從 9 月份開始就著手使用原生的 Swift,重寫 WCDB。並於 10.10 和 11.8 分別在開發者群內釋出了 alpha 和 beta 版進行測試。

今天,終於可以正式釋出 WCDB Swift 的第一個正式版本了。

WCDB Swift 約有 1.5w 行程式碼,使用 Pure Swift 編寫,幾乎不包含 Cocoa 的程式碼。且與 ObjC 版保持完全一致的功能。

模型繫結

WCDB Swift 的模型繫結,基於 Swift 4.0 的 Codable 協議實現。通過建立 Swift 型別與資料庫表之間的對映關係,使得開發者可以通過類物件直接運算元據庫。

//Sample.swift
class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil 
var unused: Int? = nil
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping =
TableBinding(CodingKeys.self)
        case identifier
        case description
    }
}

//main.swift
let tableName = "sampleTable" 
try database.create(table: tableName,of: Sample.self)
let object = Sample() 
object.identifier = 1
object.description = "sample_insert"
try database.insert(objects: object, intoTable: tableName)
複製程式碼

語言整合查詢

語言整合查詢深度結合了 Swift 和 SQL 的語法,使得純字串的 SQL 可以以程式碼的形式表達出來。結合程式碼提示及糾錯,極大地提高開發效率。

同時,由於 Swift 的語法 比 Objective-C 更加簡潔,並有更強大的範型和型別推導,使得 WCDB 介面不僅更易編寫,而且更易讀易維護。

let objects: [Sample] = try database.getObjects(fromTable: tableName,
                                                where: Sample.Properties.identifier > 0 && Sample.Properties.description.isNotNull())
複製程式碼

類似 Sample.Properties.identifier > 0 的語法,其返回值並不為 Bool,而是語言整合查詢的 Expression 物件,WCDB 會根據這個語句,去進行 SQL 的查詢。同時,通過型別的定義,Swift 即可推匯出 WCDB 查詢的結果為 Sample 類。

語言整合查詢同時內建了反注入機制,可以避免第三方從輸入框注入 SQL,進行預期之外的惡意操作。

深入 SQLite 原始碼的效能優化

WCDB 基於 SQLite 開發,我們在之前的文章介紹過其對 SQLite 原始碼進行的效能優化,以適配移動終端的場景。同樣地,這部分優化 Swift 版本也能享受到。

執行緒安全且併發

WCDB Swift 不僅可以安全地在任意執行緒進行資料庫操作,且其內部會智慧地根據操作型別調配資源,使其能夠併發執行,進一步提升效率。

加密

基於 SQLCipher 的加密機制,可以為客戶端資料安全提供一定程度的保障。

欄位升級

資料庫模型與類定義繫結,使得欄位的增加、刪除、修改都與類變數的定義保持一致,不需要開發者額外地管理欄位的版本。

class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil 
var newColumn: Date? = nil
var unused: Int? = nil
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping =
TableBinding(CodingKeys.self)
        case identifier
        case description
        case newColumn
    }
}

let tableName = "sampleTable" 
try database.create(table: tableName,of: Sample.self)
複製程式碼

模型繫結中新增了 newColumn 欄位,該欄位也會被自動建立到資料庫表中,開發者不需要手動管理。

全文搜尋

WCDB Swift 提供簡單易用的全文搜尋介面,幷包含適配多種語言的分詞器,使得資料搜尋更精準。

損壞修復

內建的修復工具可以在系統錯誤、磁碟故障等情況下,盡最大限度地將損壞的資料找回並匯出。

Pure Swift

模型繫結對語言的依賴性很大。由於 ObjC 其強大的訊息轉發機制,使得 WCDB 實現起來並沒有太大的問題。然而,動態性卻恰恰是 Swift 一直為人詬病的地方。

最省事的解決方案就是,直接引入 Cocoa,所有的問題都將不再是問題。然而,這並不是我們所期望的。

理性分析可以得出,一方面,全面的動態化會拖累 Swift 的效能,另一方面,這也會使得 Swift 的原生型別難以享受到模型繫結。

但我們的理由可能更感性一些 --- 情懷。稱之為強迫症也好,程式碼潔癖也罷,Swift with Cocoa 總讓人心裡有那麼一絲彆扭。因此,我們決定尋找 Swift 原生的解決方案。

WCDB 的模型繫結對語言有兩點依賴:

  1. Accessor。ObjC 版本使用 selectorIMP 指標,使得 WCDB 可以獲取變數的值,並插入到資料庫中,或從資料庫中獲取資料寫入到變數。
  2. 資料庫欄位的對映。ObjC 版本使用巨集定義,使得 WCDB 可以通過 className.propertyName 的方式進行語言整合查詢的操作。

KeyPath

我們最初盯上的是 Swift 的 KeyPath 的機制,它通過 \ 的語法,可以直接對變數進行讀寫操作,且語法上也與 className.propertyName 類似。

let sample=Sample()
sample[keyPath: \Sample.identifier] = 1
print(sample[keyPath: \Sample.identifier]) // 輸出 1
複製程式碼

一個難題是,KeyPath 在不引入 Cocoa 的情況下,是並不提供 property 的名稱,這就無法通過 KeyPath 直接對映資料庫的欄位。

微信終端開源資料庫 WCDB - Swift 版本

Swift 也有一個相關的 SR 在討論這個問題。

顯然,我們不可能等待這個特性實現了再去做 WCDB Swift。因此我們嘗試使用“不常規”的方法,獲取到 KeyPath 對應的 property 名稱。

Mirror 是 Swift 裡的反射型別,它可以遍歷每個變數,獲取其名稱和值,但不能對變數寫入資料。因此我們可以通過 KeyPath 對變數設一個獨一無二的特徵值,然後再通過 Mirror 遍歷變數,匯出與特徵值相同的 property 名稱。

sample[keyPath: \Sample.identifier] = 0x539D7C2 // 不易衝突的特徵值
let mirror = Mirror(reflecting: sample)
for child in mirror.children {
    if child.value == 0x539D7C2 {
        print(child.label)
        break
    }
}
複製程式碼

這個“不常規”的用法在大部分情況下能夠生效,但對於 classstruct 相互巢狀的變數,容易因為記憶體混亂導致 crash。

Codable

KeyPath 的方案不夠完善的情況下,我們轉投了 Codable 協議。它是 Swift 4.0 新增的特性,本質是編譯前根據定義生成程式碼,以完成序列化和反序列化的任務。

// Swift 官方文件中的 Codable 示例
struct Landmark: Codable {
    var name: String
    var foundingYear: Int
    var location: Coordinate
    var vantagePoints: [Coordinate]
    enum CodingKeys: String, CodingKey {
        case name = "title"
        case foundingYear = "founding_date"
        case location
        case vantagePoints
    }
} 
let data = try JSONEncoder().encode(landmark)
複製程式碼

對應到 WCDB,將資料庫的欄位讀寫到變數中,其本質就是一個序列化和反序列化的過程,而 CodingKeys 也可能可以用於語言整合查詢中的欄位對映。

然而,由於這個特性還很新,還沒有太多文件對其進行深入介紹,尤其是自定義 EncoderDecoder 這部分。

所幸的是,Swift 本身就是開源的。因此,我們參考 swift-corelibs-foundation 中的 JSONEncoderJSONDecoder,實現了 TableEncoderTableDecoder,並通過 CodingKeys 的定義,對映資料庫中的欄位。

最終維護了我們對 Pure Swift 的堅持。

class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    var offset: Int = 0
    var debugDescription: String? = nil
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping =
TableBinding(CodingKeys.self)
        case identifier = "id"
        case description
        case offset = "db_offset"
    }
} 
let sample: Sample = try database.getObject(fromTable: tableName, 
                                            where: Sample.Properties.identifier==1)
複製程式碼

微信也轉向 Swift 開發了嗎?

相信這會是大家非常關心的問題。然而,很遺憾,目前還沒有。不僅微信,國內外大部分 app 都還沒有完全轉向 Swift,但顯然這是個趨勢。

微信終端開源資料庫 WCDB - Swift 版本

Google 在 11 月 fork 了 Swift。

大家猶豫不定的原因都大同小異:ABI 不穩定,需要將二進位制打包進去,增大 app 體積;某些方面效能還不夠好,而且現在多數是與 ObjC 混編,將進一步拉低效能 等等。

而這其中一個很重要的原因就是,Swift 的基礎設施還不完善,還難以支撐其大型 app 的開發。而 WCDB Swift 就是這類基礎設施之一。

因此,先有 WCDB Swift,未來才有用 Swift 編寫微信的可能,這邏輯沒毛病。

另一方面,沒有微信的上線機制的保護和龐大的使用者量的驗證,我們需要確保 WCDB Swift 的穩定性。因此,在 WCDB Swift 的第一版本,我們就提供了相對完善的測試用例,用例的程式碼覆蓋率為 91.34%,能夠觸達絕大部分使用場景。


更多 WCDB Swift 的教程文件、程式碼樣例,包括原始碼,都直接到 Github 的 Tencent/wcdb 瞭解。

我們一起期待 Swift 成為開發者的首選的那一天。

相關文章