Android 数据库 ObjectBox 源码解析

作者: Cavabiao | 来源:发表于2017-12-14 09:24 被阅读587次

    如若不关心实现细节可直接查看“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 文档

    相关文章

      网友评论

        本文标题:Android 数据库 ObjectBox 源码解析

        本文链接:https://www.haomeiwen.com/subject/rzbjixtx.html