一、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 文档

作者:Cavabiao
链接:https://www.jianshu.com/p/b9f459927ddb
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Android 数据库 ObjectBox 源码解析的更多相关文章

  1. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  2. Android MIFARE NFCA源码解析

    Android MIFARE NFCA源码解析TagTechnology定义了所有标签的共有接口类BasicTagTechnology 实现了TagTechnology的一些接口 再有具体的标签协议继 ...

  3. 时序数据库 Apache-IoTDB 源码解析之文件数据块(四)

    上一章聊到行式存储.列式存储的基本概念,并介绍了 TsFile 是如何存储数据以及基本概念.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件格式简介(三) 打一波广告,欢迎大家访问Io ...

  4. 时序数据库 Apache-IoTDB 源码解析之文件格式简介(三)

    上一章聊到在车联网或物联网中对数据库的需求,以及 IoTDB 的整体架构,详情请见: 时序数据库 Apache-IoTDB 源码解析之系统架构(二) 打一波广告,欢迎大家访问IoTDB 仓库,求一波 ...

  5. 时序数据库 Apache-IoTDB 源码解析之系统架构(二)

    上一章聊到时序数据是什么样,物联网行业中的时序数据的特点:存量数据大.新增数据多(采集频率高.设备量多).详情请见: 时序数据库 Apache-IoTDB 源码解析之前言(一) 打一波广告,欢迎大家访 ...

  6. 时序数据库 Apache-IoTDB 源码解析之文件索引块(五)

    上一章聊到 TsFile 的文件组成,以及数据块的详细介绍.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件数据块(四) 打一波广告,欢迎大家访问IoTDB 仓库,求一波 Star. ...

  7. 时序数据库 Apache-IoTDB 源码解析之元数据索引块(六)

    上一章聊到 TsFile 索引块的详细介绍,以及一个查询所经过的步骤.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件索引块(五) 打一波广告,欢迎大家访问 IoTDB 仓库,求一波 ...

  8. 【Android】EventBus 源码解析

    EventBus 源码解析 本文为 Android 开源项目实现原理解析 中 EventBus 部分项目地址:EventBus,分析的版本:ccc2771,Demo 地址:EventBus Demo分 ...

  9. [原创]android开源项目源码解析(一)----CircleImageView的源码解析

    CircleImageView的代码很简洁,因此先将此工程作为源码解析系列的第一篇文章. 解析说明都在代码里了. /* * Copyright 2014 - 2015 Henning Dodenhof ...

随机推荐

  1. 关于android:windowNoTitle不起作用的解决办法

    今天测试一个新功能的时候,在styles.xml设置<item name="android:windowNoTitle">true</item>并没有生效, ...

  2. bat命令运行java程序

    注意空格 本文主要介绍在window下bat批处理文件调用java的方法. @echo off echo 正在加密,请稍后....echo path:%~dp0 set base=%~dp0 set ...

  3. Using RUNDLL32.exe to call a function within a dll

    Using RUNDLL32.exe to call a function within a dll        Rundll32 is a utility included with Window ...

  4. contentSize、contentInset和contentOffset 是 scrollView三个基本的属性区别和使用

    contentSize.contentInset和contentOffset 是 scrollView三个基本的属性. contentSize: 其实就是scrollview可以滚动的区域,比如fra ...

  5. java 并发——线程

    一.前言 前一篇文章总结了对 java 并发中的内置锁的理解,这篇文章来说说线程 ,并发与线程总有剪不断理还乱的关系.关于 java 线程的基本概念.线程与进程的关系以及如何创建线程,想必大家都很清楚 ...

  6. 2016-2017 National Taiwan University World Final Team Selection Contest C - Crazy Dreamoon

    题目:Statements Dreamoon likes algorithm competitions very much. But when he feels crazy because he ca ...

  7. POJ 3905 Perfect Election (2-SAT 判断可行)

    题意:有N个人参加选举,有M个条件,每个条件给出:i和j竞选与否会只要满足二者中的一项即可.问有没有方案使M个条件都满足. 分析:读懂题目即可发现是2-SAT的问题.因为只要每个条件中满足2个中的一个 ...

  8. 【android】来电悬浮窗

    先看下效果图 说下思路: 1:监听来电广播 2:根据来电号码,和本地数据库做匹配,有记录的,则提取出头像.名字.职位,生成悬浮窗 3:监听来电广播,如果当前行为是空闲的(没有任何通话行为),则删除掉悬 ...

  9. hive报错汇总

    1.需要注意的是,要在namenode(超级用户)上操作,貌似是 hive> insert into table record_partition partition(trancation_da ...

  10. JavaScript 引用【转】

    从一个例子说起: var m ={a:’a’, b:’b’}; var n=m; n.c=’c’; 那么在这个时候 , m.c 也会变成 ’c’! 这个问题在我最开始学习 JS 语言时个人一直处于概念 ...