Android 資料庫 ObjectBox 原始碼解析

Cavabiao發表於2017-12-19

如若不關心實現細節可直接點選檢視“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 文件

相關文章