Android 数据库 ObjectBox 源码解析

如若不关心实现细节可直接查看“ObjectBox 架构”、“总结”这两部分内容。(简书不支持锚点(;′⌒`))

一、ObjectBox 是什么?

greenrobot 团队(现有 EventBusgreenDAO 等开源产品)推出的又一数据库开源产品,主打移动设备、支持跨平台,最大的优点是速度快、操作简洁,目前已在实际项目中踩坑。下面将逐步分析这一堪称超快数据库的 SDK 源码(Android 部分),一起探个究竟。

ObjectBox Android 介绍

市面上已经有诸如 greenDAO、Realm、Room 等众多开源产品,至于为什么还选择 ObjectBox,暂不在本文讨论范围内。

二、ObjectBox 怎么用?

在开始源码解析之前,先介绍一下用法。
1、项目配置依赖,根据官网介绍一步步操作即可,比较简单。
2、创建业务实体类,添加@Entity,同时通过@Id指定主键,之后Build -> Make Project

创建业务实体,添加 ObjectBox 注解

3、ObjectBox Gradle 插件会在项目的 build 目录下生成 MyObjectBox 类,以及辅助类(如图中的User_UserCursorOrder_OrderCursor),接下来直接调用MyObjectBox

插件自动生成数据库辅助类

4、通过 MyObjectBox 类获取数据库(BoxStore),通过数据库获取对应的表(Box),进行 CRUD 操作。
创建数据库,获取表,增删改查

总结:实际开发过程中的感受,使用简单,配合 ObjectBrowser 直接在浏览器查看数据,开发体验好。

但是,为什么插件要自动创建MyObjectBoxUser_UserCursorOrder_OrderCursor类呢?他们又分别起什么作用?SDK 内部如何运行?

三、ObjectBox 架构

要回答以上问题,先介绍一下 ObjectBox 架构。

ObjectBox 架构

从下往上看,主要分成 Engine、Core、Extentions 三层。

  1. Engine 层属于 Native,是整个数据库的引擎,可跨平台。
    目前已支持 Android(4.0+)、Linux(64位)、Windows(64位),而 macOS、iOS 的支持在开发中。
    大部分 Java 层的数据库操作都调用了 Native 方法,但 Native 部分目前没有开源。

  2. CoreExtentions 属于 Java。
    Core 层是核心,负责数据库管理、CRUD 以及和 Native 通信;
    Extentions 提供了诸如 Reactive、LiveData、Kotlin 等一系列的扩展。

下面将重点对 Core 层进行解析

四、ObjectBox 源码解析

4.1 Entity

指的是添加了@Entity 注解的业务实体,如上文中提到的 User 类,一个 Entity 可看做一张数据库表。从上文可知 Gradle 插件自动生成了对应的 User_UserCursor 类,其中 User_ 就是 EntityInfo

User

4.2 EntityInfo

和 Entity 是成对出现的,目的是保存 Entity 的相关信息,如名称、属性(字段)等,用于后续的查询等一系列操作。


User_(User 类的 EntityInfo)

4.3 MyObjectBox

除了User_,插件还自动生成MyObjectBox 类,它只对外提供了 builder 方法返回 BoxStoreBuilder,用来构造数据库。

    /**
     * 创建 BoxStore 构造器
     *
     * @return 构造器
     */
    public static BoxStoreBuilder builder() {
        BoxStoreBuilder builder = new BoxStoreBuilder(getModel());
        builder.entity(User_.__INSTANCE);
        builder.entity(Order_.__INSTANCE);
        return builder;
    }

主要是做了两件事情,一个是getModel返回 Model,注意这里的 Model 是给 Native 层创建数据库用的,数据格式是 byte[]

创建 Model

另一个是通过entity把所有 EntityInfo 保存起来,后续 Java 层的一系列操作都会用到。

可见插件把 @Entity 生成为 EntityInfo 和 Model,前者是给 Java 层用,后者是给 Native 层用。开发者会经常和 EntityInfo 打交道,但却不会感知到 Model 的存在。

4.4 BoxStore

BoxStore 代表着整个数据库,由 BoxStoreBuilder#build 生成(通过 BoxStoreBuilder 可以进行一些定制化配置,如最大读并发数、最大容量、数据库文件名等),从源码中可以看出 BoxStoreBuilder#build 方法 new 了一个 BoxStore 对象并返回:

    public BoxStore build() {
        if (directory == null) {
            name = dbName(name);
            directory = getDbDir(baseDirectory, name);
        }
        return new BoxStore(this);
    }

BoxStore 的作用:

  1. 加载所有 Native 库
  2. 调用 Native 方法创建数据库
  3. 调用 Native 方法依次创建 Entity
  4. 创建并管理 Box(和 Entity对应,下文介绍)
  5. 创建并管理 Transaction(所有数据库操作都会放到事务中,下文介绍)
  6. 提供数据订阅(有兴趣可自行分析 Reactive 拓展模块)

其中,1、2、3 都在 BoxStore 构造方法中完成,来看看代码:

    BoxStore(BoxStoreBuilder builder) {
        // 1、加载 Native
        NativeLibraryLoader.ensureLoaded();
         …… // 省略各种校验
        // 2、调用 Native 方法创建数据库,并返回句柄(其实就是id)
        // 后续一系列操作 Native 方法的调用都要回传这个句柄
        handle = nativeCreate(canonicalPath, builder.maxSizeInKByte, builder.maxReaders, builder.model);
        ……
        for (EntityInfo entityInfo : builder.entityInfoList) {
                ……
                // 3、调用 Native 方法依次注册 Entity,并返回句柄
                int entityId = nativeRegisterEntityClass(handle, entityInfo.getDbName(), entityInfo.getEntityClass());
                entityTypeIdByClass.put(entityInfo.getEntityClass(), entityId);
        }
        ……
    }

构造函数执行完,数据库就已准备就绪。

4.5 Box

通过调用 public <T> Box<T> boxFor(Class<T> entityClass) 方法,BoxStore 会为对应的 EntityClass 生成并管理 Box(和 EntityClass 一一对应):

    /**
     * Returns a Box for the given type. Objects are put into (and get from) their individual Box.
     */
    public <T> Box<T> boxFor(Class<T> entityClass) {
        Box box = boxes.get(entityClass);
        if (box == null) {
            …… // 省略
            synchronized (boxes) {
                box = boxes.get(entityClass);
                if (box == null) {
                    // 创建 Box,传入 BoxStore 实例,以及 EntityClass
                    box = new Box<>(this, entityClass);
                    boxes.put(entityClass, box);
                }
            }
        }
        return box;
    }

Box 的职责就是进行 Entity 的 CRUD 操作,在深入分析其 CRUD 操作之前,必须先了解两个概念:Transaction(事务)Cursor(游标)

4.6 Transaction

Transaction(事务)是数据库管理系统执行过程中的一个逻辑单位,在 BoxStore 的介绍一节中提到其主要作用之一是“创建并管理 Transaction”。其实,在 ObjectBox 中,所有 Transaction 对象都是通过 BoxStore 的两个内部方法 beginTx()beginReadTx() 生成,后者生成一个只读 Transaction(不允许写入,可复用,性能会更好)。

    @Internal
    public Transaction beginTx() {
        // 1、调用 Native 方法生成事务,并返回其句柄
        long nativeTx = nativeBeginTx(handle);
        // 2、生成 Transaction 对象,传入 BoxStore、Native 事务句柄、已提交事务数量(当该事务准备提交时,用来判断有没有被其他事务抢先提交,有点绕哈,可以不管)
        Transaction tx = new Transaction(this, nativeTx, initialCommitCount);
        synchronized (transactions) {
            transactions.add(tx);
        }
        return tx;
    }
    @Internal
    public Transaction beginReadTx() {
        ……
        // 唯一不同的是,这里调用了 nativeBeginReadTx 生成只读事务
        long nativeTx = nativeBeginReadTx(handle);
        ……
    }

从以上两个方法中,可以发现所有的事务最终都是调用 Native 生成,Transaction 对象只是持有其句柄(一个类型为 long 的变量),以便后续各个操作时回传给 Native,如:

    /** 调用 Transaction 对象的提交方法 */
    public void commit() {
        checkOpen();
        // 交由 Native 进行事务提交
        int[] entityTypeIdsAffected = nativeCommit(transaction);
        store.txCommitted(this, entityTypeIdsAffected);
    }
    /** 调用 Transaction 对象的中断方法 */
    public void abort() {
        checkOpen();
        // 交由 Native 进行事务中断
        nativeAbort(transaction);
    }

此外,在 ObjectBox 中,事务分为两类“显式事务”和“隐式事务”。

“显式事务”是指开发者直接调用以下方法运行的事务:
BoxStore#runInTx(Runnable)
BoxStore#runInReadTx(Runnable)
BoxStore#runInTxAsync(Runnable,TxCallback)
BoxStore#callInTx(Callable)
BoxStore#callInReadTx(Callable)
BoxStore#callInTxAsync(Callable,TxCallback)

“隐式事务”是指对开发者透明的,框架隐式创建和管理的事务,如下面会分析到的Box#get(long)方法。

有了事务,就可以在其中进行一系列数据库的操作,那么怎么创建“操作”?这些“操作”又是如何执行?。

4.7 Cursor

上文中所说的“操作”,实际上是 Cursor (游标)。

我们再来回顾一下,文章一开始我们提到 Gradle 插件会为 User 这个 Entity 生成一个叫做UserCursor的文件,这就是所有针对User 的 CRUD 操作真正发生的地方——游标,来看看其内容。

UserCursor 文件

UserCursor 继承了 Cursor<T> ,提供 Factory 供创建时调用,同时实现了 getId 方法,以及put 方法实现写入数据库操作。

上文中提到 Box 的职责是 CRUD,其实最终都落实到了游标身上。虽然开发过程中不会直接调用 Cursor 类,但是有必要弄明白其中原理。

首先,所有游标的创建,必须调用 Transation 的 createCursor 方法(注意看注释):

    public <T> Cursor<T> createCursor(Class<T> entityClass) {
        checkOpen();
        EntityInfo entityInfo = store.getEntityInfo(entityClass);
        CursorFactory<T> factory = entityInfo.getCursorFactory();

        // 1、调用 Native 创建游标,传入 transaction (事务句柄),dbName,entityClass 三个参数,并返回句柄(游标ID)
        // 通过这三个参数,把[游标]和[事务]、[数据库表名]、[EntityClass]进行绑定
        long cursorHandle = nativeCreateCursor(transaction, entityInfo.getDbName(), entityClass);

        // 2、调用 factory 创建 Cursor 对象,传入游标句柄(后续一系列操作会回传给 Native)
        return factory.createCursor(this, cursorHandle, store);
    }

其次,拿到游标,就可以调用相关方法,进行 CRUD 操作:

// Cursor<T> 抽象类

    public T get(long key) {
        // Native 查询,传入游标句柄、ID值
        return (T) nativeGetEntity(cursor, key);
    }

    public T next() {
        // Native 查询下一条,传入游标句柄
        return (T) nativeNextEntity(cursor);
    }

    public T first() {
        // Native 查询第一条,传入游标句柄
        return (T) nativeFirstEntity(cursor);
    }

    public void deleteEntity(long key) {
        // Native 删除,传入游标句柄、ID值
        nativeDeleteEntity(cursor, key);
    }
// UserCursor 类 (extends Cursor<User>)

    @Override
    public final long put(User entity) {
        ……
        // Native 进行插入/更新,传入游标句柄
        long __assignedId = collect313311(cursor, entity.getId(),……);
        ……
        return __assignedId;
    }

Cursor 类提供了一系列 collectXXXXXX 的方法供数据插入/更新,比较有意思的思路,感兴趣的可以自行阅读。

而游标的 CRUD 操作(如写),最终都是要依靠事务才能完成提交。

那么,又回到 Box 一节的问题,Box 是如何把TransactionCursor结合起来完成 CRUD 操作的呢?

4.8 Box 的 CRUD 操作

下图是开发者直接调用 Box 进行 CRUD 操作的所有接口。

Box CRUD 接口

我们挑两个例子来分析。

4.8.1 查询 Box#get(long)

public T get(long id) {
    // 1、获取一个只读游标
    Cursor<T> reader = getReader();
    try {
        // 2、调用游标的 get 方法
        return reader.get(id);
    } finally {
        // 3、释放,只读事务只会回收,以便复用
        releaseReader(reader);
    }
}

从“游标”一节中我们知道,游标必须由事务创建,我们来看看Box#getReader()方法:

Cursor<T> getReader() {
    // 1、判断当前线程是否有可用事务和可用游标(ThreadLocal<Cursor<T>>变量保存)
    Cursor<T> cursor = getActiveTxCursor();
    if (cursor != null) {
        return cursor;
    } else {
        …… (省略缓存处理逻辑)
        // 2、当前线程无可用游标,调用 BoxStore 启动只读事务、创建游标
        cursor = store.beginReadTx().createCursor(entityClass);
        // 3、缓存游标,下次使用
        threadLocalReader.set(cursor);
    }
    return cursor;
}

所以 Box 所有查询操作,先去 BoxStore 获取一个只读游标,随后调用其 Cursor#get(long) 方法并返回结果,最后再回收该游标及其对应的事务。

4.8.2 添加 Box#put(T)

public long put(T entity) {
    // 1、获取游标(默认可以读写)
    Cursor<T> cursor = getWriter();
    try {
        // 2、调用游标的 put 方法
        long key = cursor.put(entity);
        // 3、事务提交
        commitWriter(cursor);
        return key;
    } finally {
        // 4、释放,读写事务会被销毁,无法复用
        releaseWriter(cursor);
    }
}

getReader 方法不同,因为“写事务”无法复用,所以getWriter 少了缓存事务的逻辑,完整代码:

Cursor<T> getWriter() {
    // 1、和 getReader 一样,判断当前线程是否有可用事务和可用游标
    Cursor<T> cursor = getActiveTxCursor();
    if (cursor != null) {
        return cursor;
    } else {
        // 2、当前线程无可用游标,调用 BoxStore 启动事务、创建游标
        Transaction tx = store.beginTx();
        try {
            return tx.createCursor(entityClass);
        } catch (RuntimeException e) {
            tx.close();
            throw e;
        }
    }
}

所以 Box 所有添加操作,先去 BoxStore 获取一个游标,随后调用其 Cursor#put(T) 方法并返回 id,最后再销毁该游标及其对应的事务。

当我们调用 Box 相关 CRUD 操作时,事务、游标的处理都在 Box 及 BoxStore 内部处理完成,对开发者是透明的,也就是上面说到的“隐式事务”。

另外,Box 只能够满足根据“主键”的查询,如果查询条件涉及到“过滤”、“多属性联合”、“聚合”等比较复杂的,得借助 Query 类。

4.9 Query

我们先来看看 Query 用法:

Query 用法

首先通过 Box#query() 调用 Native 方法获取 QueryBuilder 对象(持有 Native 句柄)。针对 QueryBuilder 可以设置各种查询条件,比如 equal(Property,long)

public QueryBuilder<T> equal(Property property, long value) {
    ……
    // 调用 Native 方法,设置 equal 查询条件,传入属性 id 及目标数值
    checkCombineCondition(nativeEqual(handle, property.getId(), value));
    return this;
}

再通过 QueryBuilder#build() 调用 Native 方法生成 Query 对象(持有 Native 句柄),最后,通过 Query#find() 返回所需数据,且 Query 对象可以重复使用。

在理解了事务、游标等概念后,很容易理解 QueryBuilder 以及 Query,更多代码就不贴出来了。

五、总结

以上,我们逐一分析了 ObjectBox 架构 Core 层各核心类的作用及其关系,总结起来就是:

Core 层关系图

参考资料

ObjectBox 官网

ObjectBox 文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342