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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
Android 数据库 ObjectBox 源码解析的更多相关文章
- Android 开源项目源码解析(第二期)
Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...
- Android MIFARE NFCA源码解析
Android MIFARE NFCA源码解析TagTechnology定义了所有标签的共有接口类BasicTagTechnology 实现了TagTechnology的一些接口 再有具体的标签协议继 ...
- 时序数据库 Apache-IoTDB 源码解析之文件数据块(四)
上一章聊到行式存储.列式存储的基本概念,并介绍了 TsFile 是如何存储数据以及基本概念.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件格式简介(三) 打一波广告,欢迎大家访问Io ...
- 时序数据库 Apache-IoTDB 源码解析之文件格式简介(三)
上一章聊到在车联网或物联网中对数据库的需求,以及 IoTDB 的整体架构,详情请见: 时序数据库 Apache-IoTDB 源码解析之系统架构(二) 打一波广告,欢迎大家访问IoTDB 仓库,求一波 ...
- 时序数据库 Apache-IoTDB 源码解析之系统架构(二)
上一章聊到时序数据是什么样,物联网行业中的时序数据的特点:存量数据大.新增数据多(采集频率高.设备量多).详情请见: 时序数据库 Apache-IoTDB 源码解析之前言(一) 打一波广告,欢迎大家访 ...
- 时序数据库 Apache-IoTDB 源码解析之文件索引块(五)
上一章聊到 TsFile 的文件组成,以及数据块的详细介绍.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件数据块(四) 打一波广告,欢迎大家访问IoTDB 仓库,求一波 Star. ...
- 时序数据库 Apache-IoTDB 源码解析之元数据索引块(六)
上一章聊到 TsFile 索引块的详细介绍,以及一个查询所经过的步骤.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件索引块(五) 打一波广告,欢迎大家访问 IoTDB 仓库,求一波 ...
- 【Android】EventBus 源码解析
EventBus 源码解析 本文为 Android 开源项目实现原理解析 中 EventBus 部分项目地址:EventBus,分析的版本:ccc2771,Demo 地址:EventBus Demo分 ...
- [原创]android开源项目源码解析(一)----CircleImageView的源码解析
CircleImageView的代码很简洁,因此先将此工程作为源码解析系列的第一篇文章. 解析说明都在代码里了. /* * Copyright 2014 - 2015 Henning Dodenhof ...
随机推荐
- C++ 类的两种定义方式
类内定义 class Teacher { private: string _name; int _age; public: Teacher() { printf("create techer ...
- 【转载】ARM MMU详解
一.MMU的产生 许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以 ...
- Jmeter(四)测试webservice脚本
1.有些非标准的wsdl文件导入到loadrunner时候会报错,这时候我们就能利用jmeter进行性能测试2.在soapui中新建已经soap项目,File---->new soapUI Pr ...
- 数据库之ADO
ADO是一种跨多种语言的数据库访问技术. 在MFC里面微软公司将这些函数封装为以下几个类. 在VS2013版本的MFC中,这些类是如下定义的. CDaoDatabase Class:https://m ...
- class-dump安装与使用
简介 class-dump is a command-line utility for examining the Objective-C segment of Mach-O files. It ge ...
- Delphi程序调用C#.Net编译的DLL并打开窗体(详解)
Delphi程序调用C#.Net编译的DLL并打开窗体(详解)最近用C#.Net写了一个公用模块, 本以为仅提供给.Net程序使用, 但是领导要求把这些功能提供给旧系统使用, 天啦, 几套旧系统全是D ...
- C# WinForm 父窗体 子窗体 传值
C# WinForm 父窗体 子窗体 传值 本次示例效果如下:Form1为父窗体(包含textBox1.button1)Form2为子窗体(包含textBox2.button2) 父窗体给子窗体传值= ...
- java容器的数据结构-ArrayList,LinkList,HashMap
ArrayList: 初始容量为10,底层实现是一个数组,Object[] elementData 自动扩容机制,当添加一个元素时,数组长度超过了elementData.leng,则会按照1.5倍进行 ...
- sparkSQL实战详解
摘要 如果要想真正的掌握sparkSQL编程,首先要对sparkSQL的整体框架以及sparkSQL到底能帮助我们解决什么问题有一个整体的认识,然后就是对各个层级关系有一个清晰的认识后,才能真正的 ...
- Java-性能调优-理解GC日志
[ ~]# cat gc.log.0 | grep 'Full GC' 1.652: [Full GC (System) 1.652: [CMS: 0K->21718K(262144K), 0. ...