如若不關心實現細節可直接點擊查看「ObjectBox 架構」、「總結」這兩部份內容。java
greenrobot 團隊(現有 EventBus、greenDAO 等開源產品)推出的又一數據庫開源產品,主打移動設備、支持跨平臺,最大的優勢是速度快、操做簡潔,目前已在實際項目中踩坑。下面將逐步分析這一堪稱超快數據庫的 SDK 源碼(Android 部分),一塊兒探個究竟。git
市面上已經有諸如 greenDAO、Realm、Room 等衆多開源產品,至於爲何還選擇 ObjectBox,暫不在本文討論範圍內。github
在開始源碼解析以前,先介紹一下用法。 一、項目配置依賴,根據官網介紹一步步操做便可,比較簡單。 二、建立業務實體類,添加@Entity
,同時經過@Id
指定主鍵,以後Build -> Make Project
。 數據庫
三、ObjectBox Gradle 插件會在項目的 build 目錄下生成 MyObjectBox
類,以及輔助類(如圖中的User_
、UserCursor
、Order_
、OrderCursor
),接下來直接調用MyObjectBox
。瀏覽器
MyObjectBox
類獲取數據庫(BoxStore),經過數據庫獲取對應的表(Box),進行 CRUD 操做。
總結:實際開發過程當中的感覺,使用簡單,配合 ObjectBrowser 直接在瀏覽器查看數據,開發體驗好。緩存
可是,爲何插件要自動建立MyObjectBox
和User_
、UserCursor
、Order_
、OrderCursor
類呢?他們又分別起什麼做用?SDK 內部如何運行?架構
要回答以上問題,先介紹一下 ObjectBox 架構。併發
從下往上看,主要分紅 Engine、Core、Extentions 三層。框架
Engine 層屬於 Native,是整個數據庫的引擎,可跨平臺。 目前已支持 Android(4.0+)、Linux(64位)、Windows(64位),而 macOS、iOS 的支持在開發中。 大部分 Java 層的數據庫操做都調用了 Native 方法,但 Native 部分目前沒有開源。ide
Core 和 Extentions 屬於 Java。 Core 層是核心,負責數據庫管理、CRUD 以及和 Native 通訊; Extentions 提供了諸如 Reactive、LiveData、Kotlin 等一系列的擴展。
下面將重點對 Core 層進行解析。
指的是添加了@Entity
註解的業務實體,如上文中提到的 User
類,一個 Entity 可看作一張數據庫表。從上文可知 Gradle 插件自動生成了對應的 User_
、UserCursor
類,其中 User_
就是 EntityInfo。
和 Entity 是成對出現的,目的是保存 Entity 的相關信息,如名稱、屬性(字段)等,用於後續的查詢等一系列操做。
除了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 的存在。
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 的做用:
Reactive
拓展模塊)其中,一、二、3 都在 BoxStore 構造方法中完成,來看看代碼:
BoxStore(BoxStoreBuilder builder) {
// 一、加載 Native
NativeLibraryLoader.ensureLoaded();
…… // 省略各類校驗
// 二、調用 Native 方法建立數據庫,並返回句柄(其實就是id)
// 後續一系列操做 Native 方法的調用都要回傳這個句柄
handle = nativeCreate(canonicalPath, builder.maxSizeInKByte, builder.maxReaders, builder.model);
……
for (EntityInfo entityInfo : builder.entityInfoList) {
……
// 三、調用 Native 方法依次註冊 Entity,並返回句柄
int entityId = nativeRegisterEntityClass(handle, entityInfo.getDbName(), entityInfo.getEntityClass());
entityTypeIdByClass.put(entityInfo.getEntityClass(), entityId);
}
……
}
複製代碼
構造函數執行完,數據庫就已準備就緒。
經過調用 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(遊標)。
Transaction(事務)是數據庫管理系統執行過程當中的一個邏輯單位,在 BoxStore 的介紹一節中提到其主要做用之一是「建立並管理 Transaction」。其實,在 ObjectBox 中,全部 Transaction 對象都是經過 BoxStore 的兩個內部方法 beginTx()
和 beginReadTx()
生成,後者生成一個只讀 Transaction(不容許寫入,可複用,性能會更好)。
@Internal
public Transaction beginTx() {
// 一、調用 Native 方法生成事務,並返回其句柄
long nativeTx = nativeBeginTx(handle);
// 二、生成 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)
方法。
有了事務,就能夠在其中進行一系列數據庫的操做,那麼怎麼建立「操做」?這些「操做」又是如何執行?。
上文中所說的「操做」,其實是 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();
// 一、調用 Native 建立遊標,傳入 transaction (事務句柄),dbName,entityClass 三個參數,並返回句柄(遊標ID)
// 經過這三個參數,把[遊標]和[事務]、[數據庫表名]、[EntityClass]進行綁定
long cursorHandle = nativeCreateCursor(transaction, entityInfo.getDbName(), entityClass);
// 二、調用 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 操做的呢?
下圖是開發者直接調用 Box 進行 CRUD 操做的全部接口。
咱們挑兩個例子來分析。
Box#get(long)
public T get(long id) {
// 一、獲取一個只讀遊標
Cursor<T> reader = getReader();
try {
// 二、調用遊標的 get 方法
return reader.get(id);
} finally {
// 三、釋放,只讀事務只會回收,以便複用
releaseReader(reader);
}
}
複製代碼
從「遊標」一節中咱們知道,遊標必須由事務建立,咱們來看看Box#getReader()
方法:
Cursor<T> getReader() {
// 一、判斷當前線程是否有可用事務和可用遊標(ThreadLocal<Cursor<T>>變量保存)
Cursor<T> cursor = getActiveTxCursor();
if (cursor != null) {
return cursor;
} else {
…… (省略緩存處理邏輯)
// 二、當前線程無可用遊標,調用 BoxStore 啓動只讀事務、建立遊標
cursor = store.beginReadTx().createCursor(entityClass);
// 三、緩存遊標,下次使用
threadLocalReader.set(cursor);
}
return cursor;
}
複製代碼
因此 Box 全部查詢操做,先去 BoxStore 獲取一個只讀遊標,隨後調用其 Cursor#get(long)
方法並返回結果,最後再回收該遊標及其對應的事務。
Box#put(T)
public long put(T entity) {
// 一、獲取遊標(默承認以讀寫)
Cursor<T> cursor = getWriter();
try {
// 二、調用遊標的 put 方法
long key = cursor.put(entity);
// 三、事務提交
commitWriter(cursor);
return key;
} finally {
// 四、釋放,讀寫事務會被銷燬,沒法複用
releaseWriter(cursor);
}
}
複製代碼
和 getReader
方法不一樣,由於「寫事務」沒法複用,因此getWriter
少了緩存事務的邏輯,完整代碼:
Cursor<T> getWriter() {
// 一、和 getReader 同樣,判斷當前線程是否有可用事務和可用遊標
Cursor<T> cursor = getActiveTxCursor();
if (cursor != null) {
return cursor;
} else {
// 二、當前線程無可用遊標,調用 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 類。
咱們先來看看 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 層各核心類的做用及其關係,總結起來就是:
參考資料