Realm Java 原理介绍以及常见问题

JulianProva 5年前
   <h2><strong>Realm 简介</strong></h2>    <h3><strong>Realm 与 MVCC</strong></h3>    <p>Realm 是一个 MVCC 数据库 ,底层用 C++ 编写。MVCC 指的是多版本并发控制。</p>    <p>MVCC 解决了一个重要的并发问题:在所有的数据库中都有这样的时候,当有人正在写数据库的时候有人又想读取数据库了(例如,不同的线程可以同时读取或者写入同一个数据库)。这会导致数据的不一致性 - 可能当你读取记录的时候一个写操作才部分结束。如果数据库允许这种事情发生,你就会得到和最终数据库里的数据不一致的数据。</p>    <p>有很多的办法可以解决读、写并发的问题,最常见的就是给数据库加锁。在之前的情况下,我们在写数据的时候就会加上一个锁。在写操作完成之前,所有的读操作都会被阻塞。这就是众所周知的读-写锁。这常常都会很慢。</p>    <p>类似 Realm 的 MVCC 的数据库采用了另外的一个方法:每一个连接的线程都会有数据在一个特定时刻的快照。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/2138ba17c0846b9256786d7223e762b5.jpg"></p>    <p>如上图所示:假设线程1正在读取 Realm 数据库的 V1 版本,与此同时,线程2需要写入数据库,创建一个新的 R1 节点以修改 V1 版本中的 R 节点;R1 节点的右子树仍然指向原 B 节点,左子树指向新建的 A1 节点;A1 节点的右子树仍然指向原 D 节点,左子树指向新创建的 C1 节点。</p>    <p>在线程2写入的过程中,线程1的读取操作并不会被阻塞,其仍然能够正常访问数据库版本 V1 的所有节点。</p>    <p>请看上图中的第三部分,当线程2写入完成,线程1之前的读取操作也完成,于是线程1决定刷新以得到最新的数据库更改。这时线程1也同步到了数据库的 V2 版本,所有在第二部中线程2对数据库的更改都对线程1可见。R 和其他相应的节点都替换成了线程2写入的新信息,同时原节点 C、A 和 R 不再被任何线程需要,变成了垃圾节点,将会在之后的写操作中被回收。</p>    <h3><strong>Realm 的懒加载</strong></h3>    <p>大部分的时候,你都把数据存在磁盘上的数据库文件中。开发者发起一个从持久化机制(比如 ORM 或者 Core Data)中获取数据的请求,数据格式会是和本地平台密切相关的(比如安卓或者苹果)。这个时候,持久化机制会把请求转换成一系列的 SQL 语句,创建一个数据库连接(如果没有创建的话),发送到磁盘上,执行查询,读取命中查询的每一行的数据,然后存到内存里(这里有内存消耗)。之后你需要把数据序列化成可在内存里面存储的格式,这意味着比特对齐,这样 CPU 才能处理它们。</p>    <p>最后,数据需要转换成语言层面的类型,然后它会以对象的形式返回,这样平台才能用(POJO, NSManagedObject 等等)来处理它。如果你在你的持续化机制中有子引用或者列表引用的话,这个过程会更复杂。这个过程会一遍一遍的执行(取决于你的持续化机制和配置)。如果你使用自产自销的机制,情况也大致相同。</p>    <p>Realm 的方法不一样。这就是我们零拷贝架构起作用的地方。</p>    <p>Realm 跳过了整个拷贝过程,因为数据库文件是 memory-mapped。Realm 在访问文件偏移的时候就好像文件已经在内存中一样,实际上不是,而是虚拟内存。这是个 Realm 核心文件格式的重要设计决定。它允许文件能在没有做任何反序列化的情况下可以在内存中读取。</p>    <p>Realm 跳过了所有这些开销很大的步骤,而这些步骤在传统的持久化机制中必须执行。Realm 只需要简单地计算偏移来找到文件中的数据,然后从原始访问点返回数据结构(POJO/NSManagedObject/等等)的值 。这更有效而且更快。</p>    <h2><strong>Realm Java 介绍</strong></h2>    <p>上文中所提到的 Realm 与 MVCC 相关的概念在所有的 Realm 产品中都适用,接下来我们介绍一下在 Realm Java 中这些概念是怎么与 Java 语言和 安卓框架相结合并实现的。</p>    <h3><strong>线程</strong></h3>    <p>在 Realm Java 中你可以使用 Realm.getInstance()(或者Realm.getDefaultInstance())来在当前线程中获得一个 Realm 实例。Realm 使用引用计数管理每个线程中的 Realm 实例。多次针对同一个 RealmConfiguration 在同一线程中调用会返回同一个 Realm 实例。Realm 实现了 Closeable 接口,这意味这每一次的 getInstance() 调用都应该对应一个 close() 调用以释放相应的资源。</p>    <p>如果 getInstance() 是第一次在当前线程调用,那么它会在当前最新的数据版本之上打开一个新的 Realm 实例。</p>    <p>对于一个拥有安卓 Looper 的线程,Realm 通过安卓的 Handler 系统来通知各个线程中的 Realm 实例有写入操作发生。举例来说,假设线程1是安卓 UI 线程,当线程2中对 Realm 进行了写入操作后,线程1的 Realm 会在下一次 Looper 事件中更新到线程2写入后的数据版本。</p>    <p>对于一个非 Looper 线程来说,Realm 的数据版本更新依赖于 Realm.waitForChange() 调用。该调用会阻塞当前线程直到其他线程有写入操作完成。</p>    <h3><strong>Realm 对象代理和字节码替换</strong></h3>    <p>Realm 通过使用注解处理和字节码变换来联系 RealmObject 和 Realm 数据存储。我们通过下面这个简单的例子来了解一下这个过程。例如我们有如下类定义:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/f9ced14d9715fa1fd9364d89fab4a8f4.jpg"></p>    <p>当工程编译完成后,Realm 的注解处理器会生成如下 DogRealmProxy.java:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/57e1d215e04217b797c9a27f5fc7e7e2.jpg"></p>    <p>请注意这里的 realmGet$xxx() 和 realmSet$xxx() 函数。RealmObject 正是通过这些函数来与 Realm 数据库打交道的。当然这还不是 Realm 全部的秘密,如果反编译 build/intermediates/transforms/xxx/xxx/Dog.class 文件,你会发现它与你之前定义的 Dog.java 并不完全一样:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6c935b573ec5d145f13f0436d419739d.jpg"></p>    <p>首先,我们注意到了有四个与 DogRealmProxy 类一一对应新的方法(realmGet$xxx/realmSet$xxx)被插入到了 Dog 类中,这四个新方法只是简单的 setter 和 getter;其次,在函数 getAge()、setAget() 以及 printName 中所有对 Dog 属性的直接访问都被替换成了相应生成的方法 realmGet$xxx 和 realmSet$xxx。</p>    <p>这就是全部的秘密所在了。在从 Realm 实例中获取任何 Realm 对象的时候(比如调用 Realm.createObject() 或者 RealmQuery.findFirst()),你实际上是获取了这个对象相应的 Realm 代理对象。对其属性的访问实际上都是通过相应生成的方法来访问底层的 Realm 数据库来实现的。</p>    <p>同时这也解释了我们之前提到的 Realm 的懒加载特性。在查询返回一个或者多个 Realm 对象的时候,这些对象的属性并没有被拷贝到 Java 堆中,这使得 Realm 的查询非常得快。这些属性只在需要被访问的时候,才经由生成的 getter 方法加载。</p>    <h2><strong>Realm Java 常见问题</strong></h2>    <p>在了解了 Realm 的这些关键实现之后,如下这些常见问题也就不难解释了。</p>    <h3><strong>跨线程 Realm 访问</strong></h3>    <p>Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.</p>    <p>这是一个初次使用 Realm 时常见的异常。请注意,RealmObject、RealmResults等相关对象都是与其线程中的 Realm 实例绑定的。因为两个线程中的 Realm 实例可能锁定了不同的 Realm 版本,这些对象也可能处于不同的数据版本,跨线程访问会引起数据的不一致性。所以,在另一个线程中访问同一个对象的时候,请在该线程中进行查询以获得这个对象绑定该线程 Realm 的实例。或者使用 Realm 提供的相应的异步查询接口,具体请参考相关文档。</p>    <h3><strong>托管 Realm 对象与非托管 Realm 对象</strong></h3>    <p>在 Realm 文档里这两个概念(managed Realm object/unmanaged Realm Object)尝尝被提及。通过以上的介绍,我们不难想象这里的 托管 Realm 对象(managed Realm object)指的是 Realm 的代理对象实例,例如 DogRealmProxy的实例;而非托管 Realm 对象指的是 (unmanaged Relam object)原始对象的实例,例如 Dog 的实例。</p>    <p>我们也不难想象,对于非托管 Realm 对象来说,他可以经由类似 new Dog() 的方式创建,而且对它本身属性的访问并不会引起任何对 Realm 数据库的访问。</p>    <p>当然,非托管 Realm 对象仍然可以被保存到 Realm 数据库中并且相应地返回一个托管 Realm 对象,例如:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/01db0ee19b459145472d5fd565235525.jpg"></p>    <p>如上代码中的 Realm.copyToRealm() 会将传入的非托管对象保存到 Realm 中并且返回一个托管 Realm 对象。</p>    <p>另外,显而易见,非托管 Realm 对象不具备 Realm 托管对象的一切高级特性,比如自动更新特性。</p>    <h3><strong>重复主键异常</strong></h3>    <p>在调用 Realm.createObject(Class<E> clazz) 或类似函数时,下列异常有可能被抛出:</p>    <p>Primary key constraint broken. Value already exists: 0</p>    <p>这是因为 Realm.createObject(Class<E> clazz) 实际上隐式调用了原始对象的默认无参数构造器,然后通过 Realm.copyToRealm() 方法将其存入 Realm 中。隐式构造器会给其主键属性赋一个默认值,而当第二次调用时,主键仍会是这个默认值。这就导致了 Realm 存储的对象出现了重复主键,从而异常被抛出。解决方法有很多种,譬如调用 Realm.createObject(Class<E> clazz, Object primaryKeyValue) 方法在对象创建时指定一个不重复的主键。</p>    <h3><strong>Realm 数据库文件不断增大</strong></h3>    <p>让我们来看看如下代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3177bd17928d968b56f2ce6ab9063c31.jpg"></p>    <p>这里声明的 AsyncTask 会在每次执行的时候打开一个 Realm 实例,但是并没有在使用结束后关闭。通过我们对 Realm 线程相关的介绍,不难想象这会导致某一 Realm 数据版本被该线程中的 Realm 实例锁定,因为 Realm 实例没有被正确关闭,Realm 无法得知其对应的数据已经不需要再被访问。假设这个 AsyncTask 在后台被反复执行,同时又有另一个线程在不断更新着 Realm 的数据,那么每一个 AyncTask 都会锁定一个不同的 Realm 数据版本,从而导致 Realm 文件的体积不断变化。所以,请在后台线程结束时关闭相应的 Realm 实例。</p>    <h3><strong>Realm 库与 apk 大小</strong></h3>    <p>Realm 几乎发布了针对所有 ABI 的 so 文件。如果你的应用在 google play 市场发布,那么你可以很方便的通过 google 官方提供的 apk Split 将各种 ABI 分开打包。但假设你的应用是在国内市场发布,ABI Split 可能无法正常工作,你可以考虑只包含部分 so 文件(例如 arm64 设备兼容 armeabi 和 armeabi-v7a,而只支持 armeabi 的设备几乎没有人使用了)。具体信息可以查看 Realm 的文档。</p>    <p> </p>    <p>来自:http://www.infoq.com/cn/articles/introduce-and-common-problems-of-java-realm-principle</p>    <p> </p>