Android 数据库 ObjectBox 源码解析
一、ObjectBox 是什么?
greenrobot 团队(现有 EventBus、greenDAO 等开源产品)推出的又一数据库开源产品,主打移动设备、支持跨平台,最大的优点是速度快、操作简洁,目前已在实际项目中踩坑。下面将逐步分析这一堪称超快数据库的 SDK 源码(Android 部分),一起探个究竟。
市面上已经有诸如 greenDAO、Realm、Room 等众多开源产品,至于为什么还选择 ObjectBox,暂不在本文讨论范围内。
二、ObjectBox 怎么用?
在开始源码解析之前,先介绍一下用法。
1、项目配置依赖,根据官网介绍一步步操作即可,比较简单。
2、创建业务实体类,添加@Entity
,同时通过@Id
指定主键,之后Build -> Make Project
。
3、ObjectBox Gradle 插件会在项目的 build 目录下生成 MyObjectBox
类,以及辅助类(如图中的User_
、UserCursor
、Order_
、OrderCursor
),接下来直接调用MyObjectBox
。
4、通过
MyObjectBox
类获取数据库(BoxStore),通过数据库获取对应的表(Box),进行 CRUD 操作。总结:实际开发过程中的感受,使用简单,配合 ObjectBrowser 直接在浏览器查看数据,开发体验好。
但是,为什么插件要自动创建MyObjectBox
和User_
、UserCursor
、Order_
、OrderCursor
类呢?他们又分别起什么作用?SDK 内部如何运行?
三、ObjectBox 架构
要回答以上问题,先介绍一下 ObjectBox 架构。
从下往上看,主要分成 Engine、Core、Extentions 三层。
-
Engine 层属于 Native,是整个数据库的引擎,可跨平台。
目前已支持 Android(4.0+)、Linux(64位)、Windows(64位),而 macOS、iOS 的支持在开发中。
大部分 Java 层的数据库操作都调用了 Native 方法,但 Native 部分目前没有开源。 -
Core 和 Extentions 属于 Java。
Core 层是核心,负责数据库管理、CRUD 以及和 Native 通信;
Extentions 提供了诸如 Reactive、LiveData、Kotlin 等一系列的扩展。
下面将重点对 Core 层进行解析。
四、ObjectBox 源码解析
4.1 Entity
指的是添加了@Entity
注解的业务实体,如上文中提到的 User
类,一个 Entity 可看做一张数据库表。从上文可知 Gradle 插件自动生成了对应的 User_
、UserCursor
类,其中 User_
就是 EntityInfo。
4.2 EntityInfo
和 Entity 是成对出现的,目的是保存 Entity 的相关信息,如名称、属性(字段)等,用于后续的查询等一系列操作。
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[]
;
另一个是通过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 的作用:
- 加载所有 Native 库
- 调用 Native 方法创建数据库
- 调用 Native 方法依次创建 Entity
- 创建并管理 Box(和 Entity对应,下文介绍)
- 创建并管理 Transaction(所有数据库操作都会放到事务中,下文介绍)
- 提供数据订阅(有兴趣可自行分析
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
继承了 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 是如何把Transaction
和Cursor
结合起来完成 CRUD 操作的呢?
4.8 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 用法:
首先通过 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 层各核心类的作用及其关系,总结起来就是:
参考资料
作者:Cavabiao
链接:https://www.jianshu.com/p/b9f459927ddb
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。