使用Realm和RxJava打出组合拳的正确姿势

题外话,这周一直在读一本计算机读物,《浪潮之巅》,还未读完。

这本书讲述的是计算机科学的发展史,其中,也掺杂了作者的很多个人观点。作者语言朴实,文笔恢弘,当读到那些顶级公司的崛起时,我也会感到莫名的振奋,催人向前,产生无数动力;而读到那些鼎鼎大名的科技公司的衰落甚至消亡时,也会感到丝丝的哀痛;科技世界,是一个变化的世界,“以不变应万变”的法则,在这里是绝对行不通的。身处在科技浪潮之中我们也应该明白,下一个科技革命在不远的将来就会来临,我们自当立志站在浪潮之巅,这样才不会被巨大的浪潮湮灭。

正文开始

realm_combine_rxjava.png

先简单介绍下:
Realm:一款用来代替sqlite和 core data的移动端数据库;有了它,移动应用中的数据存储变得轻而易举。
想了解Realm如何使用可以查看官网或者中文站(翻译的非常棒)
RxJava:一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库。更多可以阅读扔物线的力作(真的学到了好多东西,应该是好多人的入门教程吧)

我自己在项目中是先用的RxJava,不得不说,实在太好用了,用了它之后可以避免回调噩梦还能够自由的切换工作线程。在项目中用到的地方很多,比如结合Retrofit进行网络接口调用,进行复杂业务逻辑的处理等等。

后来,由于客户端需要在本地存储业务数据,就考虑着要不要用Realm,同时考虑的还有GreenDao。GreenDao以前就用过,现在再次使用当然没有问题,但是作为技术人,有新的更好的技术而不去尝试,而墨守成规,是会落后的。况且,GreenDao确实也有些不方便的地方,比如需要使用Generator进行实体对象和Dao类的生成。

在真正使用Realm作为移动端数据库之前,还是做了很多考察的,第一步就是通读了Realm的官网文档,这样才能了解Realm的全貌,判断适合它使用的应用场景。

总结下来,
Realm有两个优点:

  • 对象就是表,属性就是字段,不需要额外的设置(相比GreenDao更简单)。
  • 相比其他移动端数据库框架,性能更加卓越,存取速度很快。

Realm也有两个缺点:

  • 不支持继承父类对象,所有Realm类都必须继承RealmObject(或者实现RealmModel接口)
  • realm对象只能在创建的线程使用,不允许跨线程访问realm对象。

关于第一点,无法解决,Realm设计之初,就要求所有Realm类都需要继承RealmObject,个人分析,毕竟Realm类还是一个数据表而不是纯粹的Java类,无法像类一样进行继承关系。目前只能每个表中都需要定义自己所有的字段,而无法把共有字段抽象到父类,暂时只能麻烦些。

关于第二点,也无法解决,Realm机制上就是如此,我们也没有办法。而我们又需要进行自由的切换线程,最终找到官方是有对应解决方案的,嗯,对,官网增加了对RxJava support,有了RxJava的强大助力,我们可以自由的玩耍了,但是先不要高兴的太早,这个方案其实有两个缺点。一个是这个方案只支持查询操作,如果我们需要存储操作也享受RxJava带来的种种便利呢?这个方案暂时没有解决方法。这个方案的另一个缺点时,realm的关闭问题,如果查询操作是异步的,我们如果写出这样的代码,

Realm realm = Realm.getDefaultInstance();
realm.where(Person.class)
    .findAllAsync()
    .asObservable()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<List<Person>>() {
        @Override
        public void call(List<Person> persons) {
        Logger.d("query complete");
        mPersons.clear();
        for (Person person : persons) {
            mPersons.add(person);
        }
        mPersonListAdapter.notifyDataSetChanged();
        }
    });
realm.close();

就会报错

Caused by: rx.exceptions.OnErrorNotImplementedException: This Realm instance has already been closed, making it unusable.
at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:374)
at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:371)

官方建议呢,将Realm的创建和关闭放在生命周期函数里面,这样做当然就无需考虑Realm的释放问题了,但是我们就需要在使用Realm的地方传入Realm的引用了,这也令人很不爽,毕竟Realm方法是数据库操作,按照架构层次来讲是不应该放View层的,如果可以随时使用随时关闭并且不会报错是最好的了。

为了这个问题,我和我们团队的小伙伴也是苦思冥想,各种尝试,经过不懈的努力,终于在GitHub((__) 嘻嘻……)上发现有位国外大哥fix了一个问题,连Realm官网都刊登了这篇文章,说明Realm官方也是认可这个解决方案的,放心了吧。不说废话了,上链接 Realm Post国外大哥的GitHub。

不过呢,上面的解决方案比较早,由于Realm也一直在更新,上面那个解决方案调用的API有些已经过时了,我自己稍微修改了下,使用起来非常完美,这次和RxJava结合毫无违和感。
代码如下:(也可以点击这里可以通过我的GitHub查看源码。)

package com.example.testingexample.view.rxjava;

/**
 * Created by ZhangYige on 2016/12/8.
 * 解决Realm不能使用RxJava 切换线程的问题
 * from(https://github.com/kboyarshinov/realm-rxjava-example)
 */

import android.support.annotation.NonNull;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.exceptions.RealmException;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;

public abstract class OnSubscribeRealm<T> implements Observable.OnSubscribe<T> {
    private final String fileName;

    private final List<Subscriber<? super T>> subscribers = new ArrayList<>();
    private final AtomicBoolean canceled = new AtomicBoolean();
    private final Object lock = new Object();

    public OnSubscribeRealm() {
        this(null);
    }

    public OnSubscribeRealm(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void call(final Subscriber<? super T> subscriber) {
        synchronized (lock) {
            boolean canceled = this.canceled.get();
            if (!canceled && !subscribers.isEmpty()) {
                subscriber.add(newUnsubscribeAction(subscriber));
                subscribers.add(subscriber);
                return;
            } else if (canceled) {
                return;
            }
        }
        subscriber.add(newUnsubscribeAction(subscriber));
        subscribers.add(subscriber);

        RealmConfiguration.Builder builder = new RealmConfiguration.Builder();
        builder.deleteRealmIfMigrationNeeded();
        if (fileName != null) {
            builder.name(fileName);
        }
        Realm realm = Realm.getInstance(builder.build());
        boolean withError = false;

        T object = null;
        try {
            if (!this.canceled.get()) {
                realm.beginTransaction();
                object = get(realm);
                if (!this.canceled.get()) {
                    realm.commitTransaction();
                } else {
                    realm.cancelTransaction();
                }
            }
        } catch (RuntimeException e) {
            realm.cancelTransaction();
            sendOnError(new RealmException("Error during transaction.", e));
            withError = true;
        } catch (Error e) {
            realm.cancelTransaction();
            sendOnError(e);
            withError = true;
        }
        if (!this.canceled.get() && !withError) {
            sendOnNext(object);
        }

        try {
            realm.close();
        } catch (RealmException ex) {
            sendOnError(ex);
            withError = true;
        }
        if (!withError) {
            sendOnCompleted();
        }
        this.canceled.set(false);
    }

    private void sendOnNext(T object) {
        for (int i = 0; i < subscribers.size(); i++) {
            Subscriber<? super T> subscriber = subscribers.get(i);
            subscriber.onNext(object);
        }
    }

    private void sendOnError(Throwable e) {
        for (int i = 0; i < subscribers.size(); i++) {
            Subscriber<? super T> subscriber = subscribers.get(i);
            subscriber.onError(e);
        }
    }

    private void sendOnCompleted() {
        for (int i = 0; i < subscribers.size(); i++) {
            Subscriber<? super T> subscriber = subscribers.get(i);
            subscriber.onCompleted();
        }
    }

    @NonNull
    private Subscription newUnsubscribeAction(final Subscriber<? super T> subscriber) {
        return Subscriptions.create(new Action0() {
            @Override
            public void call() {
                synchronized (lock) {
                    subscribers.remove(subscriber);
                    if (subscribers.isEmpty()) {
                        canceled.set(true);
                    }
                }
            }
        });
    }

    public abstract T get(Realm realm);
}

package com.example.testingexample.view.rxjava;

/**
 * Created by ZhangYige on 2016/12/8.
 * 解决Realm不能使用RxJava 切换线程的问题
 * from(https://github.com/kboyarshinov/realm-rxjava-example)
 */

import java.util.List;

import io.realm.Realm;
import io.realm.RealmList;
import io.realm.RealmObject;
import io.realm.RealmResults;
import rx.Observable;
import rx.functions.Func1;

public final class RealmObservable {
    private RealmObservable() {
    }

    public static <T extends Object> Observable<T> createObservable(final Func1<Realm, T> function) {
        return Observable.create(new OnSubscribeRealm<T>() {
            @Override
            public T get(Realm realm) {
                T t = function.call(realm);
                if(t!=null){
                    if (t instanceof RealmObject) {
                        return (T) realm.copyFromRealm((RealmObject)t);
                    } else if (t instanceof RealmList) {
                        return (T) realm.copyFromRealm((List<RealmObject>) t);
                    } else if(t instanceof RealmResults){
                        return (T) realm.copyFromRealm((List<RealmObject>) t);
                    }
                    return t;
                }
                return t;
            }
        });
    }
}

使用示例

RealmObservable
        .createObservable(new Func1<Realm, Void>() {
            @Override
            public Void call(Realm realm) {
                Logger.d("realm add start");
                Person person = realm.createObject(Person.class);
                person.setName("zhangsan" + System.currentTimeMillis());
                person.setSex("男");
                person.setAge(24);
                return null;
            }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Void>() {
            @Override
            public void call(Void aVoid) {
                Logger.d("realm add finish");
                //查询人员信息
                queryPersons();
            }
        });

文章都来自个人工作总结和个人感悟,如有不正确的地方,还请不吝赐教,先在此谢过。

推荐阅读更多精彩内容