微信终端开源数据库 WCDB - Swift 版本

WCDB 做为微信的终端数据库,从 2017.6 开源至今,共迭代了 5 个版本。咱们一直关注开发者们的需求,并不断优化性能,新增如全文搜索等经常使用的功能。而这其中,呼声最高的莫过于 对 Swift 的支持。git

WCDB ObjC 版本的实现中,因为引入了 C++ 代码,并不能直接 bridge 到 Swift。所以,咱们从 9 月份开始就着手使用原生的 Swift,重写 WCDB。并于 10.10 和 11.8 分别在开发者群内发布了 alpha 和 beta 版进行测试。github

今天,终于能够正式发布 WCDB Swift 的第一个正式版本了。数据库

WCDB Swift 约有 1.5w 行代码,使用 Pure Swift 编写,几乎不包含 Cocoa 的代码。且与 ObjC 版保持彻底一致的功能。swift

模型绑定

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,进行预期以外的恶意操做。app

深刻 SQLite 源码的性能优化

WCDB 基于 SQLite 开发,咱们在以前的文章介绍过其对 SQLite 源码进行的性能优化,以适配移动终端的场景。一样地,这部分优化 Swift 版本也能享受到。ide

线程安全且并发

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 直接映射数据库的字段。

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,但显然这是个趋势。

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 成为开发者的首选的那一天。

相关文章
相关标签/搜索