Realm Java 原理介紹以及常見問題
Realm 簡介
Realm 與 MVCC
Realm 是一個 MVCC 數據庫 ,底層用 C++ 編寫。MVCC 指的是多版本并發控制。
MVCC 解決了一個重要的并發問題:在所有的數據庫中都有這樣的時候,當有人正在寫數據庫的時候有人又想讀取數據庫了(例如,不同的線程可以同時讀取或者寫入同一個數據庫)。這會導致數據的不一致性 - 可能當你讀取記錄的時候一個寫操作才部分結束。如果數據庫允許這種事情發生,你就會得到和最終數據庫里的數據不一致的數據。
有很多的辦法可以解決讀、寫并發的問題,最常見的就是給數據庫加鎖。在之前的情況下,我們在寫數據的時候就會加上一個鎖。在寫操作完成之前,所有的讀操作都會被阻塞。這就是眾所周知的讀-寫鎖。這常常都會很慢。
類似 Realm 的 MVCC 的數據庫采用了另外的一個方法:每一個連接的線程都會有數據在一個特定時刻的快照。

如上圖所示:假設線程1正在讀取 Realm 數據庫的 V1 版本,與此同時,線程2需要寫入數據庫,創建一個新的 R1 節點以修改 V1 版本中的 R 節點;R1 節點的右子樹仍然指向原 B 節點,左子樹指向新建的 A1 節點;A1 節點的右子樹仍然指向原 D 節點,左子樹指向新創建的 C1 節點。
在線程2寫入的過程中,線程1的讀取操作并不會被阻塞,其仍然能夠正常訪問數據庫版本 V1 的所有節點。
請看上圖中的第三部分,當線程2寫入完成,線程1之前的讀取操作也完成,于是線程1決定刷新以得到最新的數據庫更改。這時線程1也同步到了數據庫的 V2 版本,所有在第二部中線程2對數據庫的更改都對線程1可見。R 和其他相應的節點都替換成了線程2寫入的新信息,同時原節點 C、A 和 R 不再被任何線程需要,變成了垃圾節點,將會在之后的寫操作中被回收。
Realm 的懶加載
大部分的時候,你都把數據存在磁盤上的數據庫文件中。開發者發起一個從持久化機制(比如 ORM 或者 Core Data)中獲取數據的請求,數據格式會是和本地平臺密切相關的(比如安卓或者蘋果)。這個時候,持久化機制會把請求轉換成一系列的 SQL 語句,創建一個數據庫連接(如果沒有創建的話),發送到磁盤上,執行查詢,讀取命中查詢的每一行的數據,然后存到內存里(這里有內存消耗)。之后你需要把數據序列化成可在內存里面存儲的格式,這意味著比特對齊,這樣 CPU 才能處理它們。
最后,數據需要轉換成語言層面的類型,然后它會以對象的形式返回,這樣平臺才能用(POJO, NSManagedObject 等等)來處理它。如果你在你的持續化機制中有子引用或者列表引用的話,這個過程會更復雜。這個過程會一遍一遍的執行(取決于你的持續化機制和配置)。如果你使用自產自銷的機制,情況也大致相同。
Realm 的方法不一樣。這就是我們零拷貝架構起作用的地方。
Realm 跳過了整個拷貝過程,因為數據庫文件是 memory-mapped。Realm 在訪問文件偏移的時候就好像文件已經在內存中一樣,實際上不是,而是虛擬內存。這是個 Realm 核心文件格式的重要設計決定。它允許文件能在沒有做任何反序列化的情況下可以在內存中讀取。
Realm 跳過了所有這些開銷很大的步驟,而這些步驟在傳統的持久化機制中必須執行。Realm 只需要簡單地計算偏移來找到文件中的數據,然后從原始訪問點返回數據結構(POJO/NSManagedObject/等等)的值 。這更有效而且更快。
Realm Java 介紹
上文中所提到的 Realm 與 MVCC 相關的概念在所有的 Realm 產品中都適用,接下來我們介紹一下在 Realm Java 中這些概念是怎么與 Java 語言和 安卓框架相結合并實現的。
線程
在 Realm Java 中你可以使用 Realm.getInstance()(或者Realm.getDefaultInstance())來在當前線程中獲得一個 Realm 實例。Realm 使用引用計數管理每個線程中的 Realm 實例。多次針對同一個 RealmConfiguration 在同一線程中調用會返回同一個 Realm 實例。Realm 實現了 Closeable 接口,這意味這每一次的 getInstance() 調用都應該對應一個 close() 調用以釋放相應的資源。
如果 getInstance() 是第一次在當前線程調用,那么它會在當前最新的數據版本之上打開一個新的 Realm 實例。
對于一個擁有安卓 Looper 的線程,Realm 通過安卓的 Handler 系統來通知各個線程中的 Realm 實例有寫入操作發生。舉例來說,假設線程1是安卓 UI 線程,當線程2中對 Realm 進行了寫入操作后,線程1的 Realm 會在下一次 Looper 事件中更新到線程2寫入后的數據版本。
對于一個非 Looper 線程來說,Realm 的數據版本更新依賴于 Realm.waitForChange() 調用。該調用會阻塞當前線程直到其他線程有寫入操作完成。
Realm 對象代理和字節碼替換
Realm 通過使用注解處理和字節碼變換來聯系 RealmObject 和 Realm 數據存儲。我們通過下面這個簡單的例子來了解一下這個過程。例如我們有如下類定義:

當工程編譯完成后,Realm 的注解處理器會生成如下 DogRealmProxy.java:

請注意這里的 realmGet$xxx() 和 realmSet$xxx() 函數。RealmObject 正是通過這些函數來與 Realm 數據庫打交道的。當然這還不是 Realm 全部的秘密,如果反編譯 build/intermediates/transforms/xxx/xxx/Dog.class 文件,你會發現它與你之前定義的 Dog.java 并不完全一樣:

首先,我們注意到了有四個與 DogRealmProxy 類一一對應新的方法(realmGet$xxx/realmSet$xxx)被插入到了 Dog 類中,這四個新方法只是簡單的 setter 和 getter;其次,在函數 getAge()、setAget() 以及 printName 中所有對 Dog 屬性的直接訪問都被替換成了相應生成的方法 realmGet$xxx 和 realmSet$xxx。
這就是全部的秘密所在了。在從 Realm 實例中獲取任何 Realm 對象的時候(比如調用 Realm.createObject() 或者 RealmQuery.findFirst()),你實際上是獲取了這個對象相應的 Realm 代理對象。對其屬性的訪問實際上都是通過相應生成的方法來訪問底層的 Realm 數據庫來實現的。
同時這也解釋了我們之前提到的 Realm 的懶加載特性。在查詢返回一個或者多個 Realm 對象的時候,這些對象的屬性并沒有被拷貝到 Java 堆中,這使得 Realm 的查詢非常得快。這些屬性只在需要被訪問的時候,才經由生成的 getter 方法加載。
Realm Java 常見問題
在了解了 Realm 的這些關鍵實現之后,如下這些常見問題也就不難解釋了。
跨線程 Realm 訪問
Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
這是一個初次使用 Realm 時常見的異常。請注意,RealmObject、RealmResults等相關對象都是與其線程中的 Realm 實例綁定的。因為兩個線程中的 Realm 實例可能鎖定了不同的 Realm 版本,這些對象也可能處于不同的數據版本,跨線程訪問會引起數據的不一致性。所以,在另一個線程中訪問同一個對象的時候,請在該線程中進行查詢以獲得這個對象綁定該線程 Realm 的實例。或者使用 Realm 提供的相應的異步查詢接口,具體請參考相關文檔。
托管 Realm 對象與非托管 Realm 對象
在 Realm 文檔里這兩個概念(managed Realm object/unmanaged Realm Object)嘗嘗被提及。通過以上的介紹,我們不難想象這里的 托管 Realm 對象(managed Realm object)指的是 Realm 的代理對象實例,例如 DogRealmProxy的實例;而非托管 Realm 對象指的是 (unmanaged Relam object)原始對象的實例,例如 Dog 的實例。
我們也不難想象,對于非托管 Realm 對象來說,他可以經由類似 new Dog() 的方式創建,而且對它本身屬性的訪問并不會引起任何對 Realm 數據庫的訪問。
當然,非托管 Realm 對象仍然可以被保存到 Realm 數據庫中并且相應地返回一個托管 Realm 對象,例如:

如上代碼中的 Realm.copyToRealm() 會將傳入的非托管對象保存到 Realm 中并且返回一個托管 Realm 對象。
另外,顯而易見,非托管 Realm 對象不具備 Realm 托管對象的一切高級特性,比如自動更新特性。
重復主鍵異常
在調用 Realm.createObject(Class<E> clazz) 或類似函數時,下列異常有可能被拋出:
Primary key constraint broken. Value already exists: 0
這是因為 Realm.createObject(Class<E> clazz) 實際上隱式調用了原始對象的默認無參數構造器,然后通過 Realm.copyToRealm() 方法將其存入 Realm 中。隱式構造器會給其主鍵屬性賦一個默認值,而當第二次調用時,主鍵仍會是這個默認值。這就導致了 Realm 存儲的對象出現了重復主鍵,從而異常被拋出。解決方法有很多種,譬如調用 Realm.createObject(Class<E> clazz, Object primaryKeyValue) 方法在對象創建時指定一個不重復的主鍵。
Realm 數據庫文件不斷增大
讓我們來看看如下代碼:

這里聲明的 AsyncTask 會在每次執行的時候打開一個 Realm 實例,但是并沒有在使用結束后關閉。通過我們對 Realm 線程相關的介紹,不難想象這會導致某一 Realm 數據版本被該線程中的 Realm 實例鎖定,因為 Realm 實例沒有被正確關閉,Realm 無法得知其對應的數據已經不需要再被訪問。假設這個 AsyncTask 在后臺被反復執行,同時又有另一個線程在不斷更新著 Realm 的數據,那么每一個 AyncTask 都會鎖定一個不同的 Realm 數據版本,從而導致 Realm 文件的體積不斷變化。所以,請在后臺線程結束時關閉相應的 Realm 實例。
Realm 庫與 apk 大小
Realm 幾乎發布了針對所有 ABI 的 so 文件。如果你的應用在 google play 市場發布,那么你可以很方便的通過 google 官方提供的 apk Split 將各種 ABI 分開打包。但假設你的應用是在國內市場發布,ABI Split 可能無法正常工作,你可以考慮只包含部分 so 文件(例如 arm64 設備兼容 armeabi 和 armeabi-v7a,而只支持 armeabi 的設備幾乎沒有人使用了)。具體信息可以查看 Realm 的文檔。
來自:http://www.infoq.com/cn/articles/introduce-and-common-problems-of-java-realm-principle