Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Опыт использования Realm в Android: все за и против

Погос Азарян
Android-разработчик

Realm Mobile Database — это ORM (Object-Relational Mapping) база данных для мобильных платформ на базе iOS и Android. Она отличается от других решений высокой скоростью работы, простотой в использовании, а так же «живыми» объектами, то есть, при изменении данных в базе, обновятся и объекты в коде, которые ссылаются на эти данные. Realm действительно весьма удобен и позволяет использовать его сразу «из коробки», но во всяком продукте есть свои особенности и недостатки, с которыми может столкнуться каждый разработчик. В этой статье я хотел бы рассказать о них, чтобы читатель мог взвесить все «за» и «против» перед тем, как использовать эту библиотеку в своём проекте.

При подключении Realm будьте готовы к тому, что вес вашего apk файла увеличится примерно на 5 МБ. Дело в том, что Realm написан на C++, и для поддержки различных архитектур процессоров используются нативные библиотеки, которые все вместе весят не мало. Конечно, при установке система сама определяет, какую библиотеку стоит оставить, а какие удалить, и в развернутом виде ненужных файлов не будет, но всё равно размер самого apk в Google play будет выше на эти пресловутые 5 МБ. Для решения этой проблемы можно воспользоваться возможностью gradle: Android Build Tool ABI Split.

android {
    splits {
        abi {
            enable true
            reset()
            include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
        }
    }
}

С помощью этих строк кода можно собрать несколько apk файлов для каждой из библиотек. Если же вы не хотите возиться с несколькими apk, то можно просто исключить ненужные библиотеки с помощью abiFilters.

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
        }
    }
}

Закрытие Realm или Realm.close()

Для того чтобы работать с Realm, необходимо для начала создать его инстанс. Можно использовать для этого обычный Realm.getDefaultInstance(), который можно заранее сконфигурировать, например, в вашем Application классе на onCreate. После работы необходимо закрыть инстанс, чтобы предотвратить утечки памяти и рост размера файла Realm на устройстве пользователя. Дело в том, что при действиях с данными, Realm выделяет дополнительную память в файле. После закрытия инстанса закрывается и файл. Были случаи, когда не закрытый инстанс Realm, порождал файл размером с 5 ГБ на устройстве. В документациях и примерах советуют подвязать закрытие Realm с onDestroy Activity или Fargment. Но если вы разрабатываете программу на основе MVP, то, вероятно, хотели бы сделать так, чтобы жизненный цикл View никак не касался объектов Realm. Об этом я расскажу ниже.

Работа с многопоточностью

Для обеспечения безопасности при работе с многопоточностью, Realm позволяет создавать лишь один инстанс на поток, и данные, которые отдаст этот инстанс, можно будет использовать только на этом потоке, иначе вы получите IllegalStateException. То есть, чтобы передать RealmObject между потоками, нужно будет передавать какой-нибудь идентификатор, и на другом потоке открывать новый инстанс, чтобы найти по переданному ключу нужный объект. Данный факт заставляет вас более внимательно следить за доступом из других потоков к вашим Realm объектам. Если вы используете в разработке RxJava, то должны четко строить цепочку операторов так, чтобы всё, что связано с Realm, обрабатывалось на том же потоке, где был создан инстанс. В принципе, если вы разрабатываете приложение с использованием стандартного подхода «Всё в Activity», то особой проблемы у вас возникнуть не должно. Создание инстанса Realm можно подвязать к жизненному циклу Activity, и тогда просто нужно следить, чтобы все действия с данными происходили на основном потоке. Благо, Realm позволяет выполнять асинхронные транзакции, что помогает сильно не нагружать main thread. Но данный способ программирования не рекомендуется, так как с ростом проекта у вас получится неплохой такой god object в виде Activity, с которым будет невозможно работать в будущем. Так что

Realm в паттернах MVP, MVC

Данный вопрос стоял довольно остро на одном из проектов, в котором я принимал участие. Задача была следующая: выделить всю работу с Realm в отдельный класс, например, в UserDbSource, в общем, организовать настоящую clean architecture, чтобы слои выше «не знали», что под этим классом скрывается Realm. Первая итерация данного класса получилась такая:

public class UserDbSource {
    
    private Realm mRealm;

    
  
    public UserDbSource() {
        
        mRealm = Realm.getDefaultInstance();
    
    }

    
        
    public Single<List<User>> getUsers() {
        
        return mRealm.where(User.class)
                
            .findAll()
                
            .asObservable()
                
            .map(users -> (List<User>) users)
                
            .toSingle();
    
    }

    
        
    public Single<User> getUser(long id) {
        
        return mRealm.where(User.class)
                
            .equalTo("id", id)
                
            .findFirst()
                
            .asObservable()
                
            .cast(User.class)
                
            .toSingle();
    
    }   

    
        
    public Completable updateUsers(List<User> users) {
        
        return Completable.fromAction(() -> {
            mRealm.beginTransaction();
            
            mRealm.insertOrUpdate(users);
            
            mRealm.commitTransaction();
        
        });
    
    }
    
    

    public Completable updateUser(User user) {
        
        return Completable.fromAction(() -> {
            
            mRealm.beginTransaction();
           
            mRealm.insertOrUpdate(user);
            
            mRealm.commitTransaction();
        
        });
    
    }

}

Вы наверняка уже нашли первую ошибку в этом классе. Да, нигде не закрывается инстанс реалма. Другая состояла как раз-таки в многопоточности. Сначала мы хотели оставить возможность «живых» объектов, чтобы просто ловить onChange от реалма и обновлять вьюшки. Для этого нужно было сделать так, чтобы класс создался на основном потоке, чтобы мы могли использовать объекты в активити. Думаю, не стоит говорить, что это создавало множество костылей и кучу плохого кода. И из-за этого нарушалась концепция чистой архитектуры, так как вьюшки у нас знали о том, что они используют RealmObject и RealmList. Ещё одна проблема была в том, что мы не могли изменять объект вне mRealm.executeTransaction(). Из-за этого класс наполнялся множеством методов, типа changeUserName(String name, User user), либо приходилось создавать новый объект User, который не являлся RealmObject, потом присваивать его полям все нужные нам параметры, хотя мы поменяли лишь имя у User. Немного поразмыслив, мы решили пожертвовать возможностью автообновляемых объектов и отдавать обычные объекты вместо RealmObject. В итоге класс получился следующего вида:

public class UserDbSource {

    private RealmConfiguration mRealmConfiguration;


        
    public UserDbSource() {

        mRealmConfiguration = App.getUserConfiguration();

    }


        
    public Single<List<User>> getUsers() {

        Realm realm = Realm.getInstance(mRealmConfiguration);

        List<User> users = realm.copyFromRealm(realm.where(User.class).findAll());

        realm.close();

        return Single.just(users);

    }



    public Single<User> getUser(long id) {

        Realm realm = Realm.getInstance(mRealmConfiguration);

        User user = null;

        User realmUser = realm.where(User.class).equalTo("id", id).findFirst();

        if (realmUser != null) {

            user = realm.copyFromRealm(realmUser);

        }
        
        realm.close();

        return Single.just(user);

    }



    public Completable updateUsers(List<User> users) {

        return Completable.fromAction(() -> {

            Realm realm = Realm.getInstance(mRealmConfiguration);

            realm.insertOrUpdate(users);

            realm.close();

            Realm.compactRealm(mRealmConfiguration);

        });

    }



    public Completable updateUser(User user) {
 
        return Completable.fromAction(() -> {

            Realm realm = Realm.getInstance(mRealmConfiguration);

            realm.insertOrUpdate(user);

            realm.close();

            Realm.compactRealm(mRealmConfiguration);

        });

    }

}

В данном случае с помощью copyFromRealm мы достаем обычные Java объекты, которые спокойно можно передавать между потоками, изменять и оборачивать в модель, предназначенную для View. Теперь у нас не будет ошибки, когда инстанс создался в одном потоке, а затем мы пытаемся использовать его же в другом. Так же realm закрывается сразу после выполнения метода. И ещё добавилась строчка compactRealm, которая слегка уменьшает размер файла Realm на устройстве после выполнения действий, связанных с записью. Вполне вероятно, что данная реализация ещё далека от идеала, что можно ещё попробовать как-нибудь воспользоваться фишкой Realm с «живыми объектами», но на данный момент мы пришли к этому варианту, и пока что он весьма неплохо справляется со своей задачей.

Нативные библиотеки

Это нельзя назвать косяком Realm или кого-либо ещё, скорее это просто особенность нативных библиотек и использование их в системе. Был у меня один проект, к которому подключался отдельным модулем проект, созданный на Unity. Сделав всё по документации на сайте Unity, я подключил проект, всё настроил, и всё запустилось с первого раза. Затем я отдал билд на тест клиенту и получил в ответ, что Unity не запускается, и всё плохо. Взяв другой телефон, я увидел следующую ошибку: “Это приложение не поддерживается на вашем устройстве” (точную формулировку, к сожалению, не помню). Спустя несколько часов гугления, я нашел небольшой issue на GitHub реалма о такой же проблеме, как и у меня. Оказывается, и в Realm и в Unity используются нативные библиотеки, но в Unity использовались лишь armeabi-v7a и x86, в то время как в Realm был полный набор. Так вот, когда приложение ставилось на 32-битной Arm процессор, то всё работало нормально, но когда ставили на 64-битный, то Unity отказывалась работать. Происходило то, что я описывал выше, при установке apk остаются подходящие библиотеки для устройства, ну и, следовательно, на 64-битный ставился arm64-v8a, которого не было для Unity. Решилась проблема исключением библиотек, которые мешали запуску Unity, в итоге всё нормально заработало.

Падение с кодом C++

Иногда бывает, но не так уж и часто. Удалось спровоцировать неоднократное падение с нативным кодом, лишь когда пытались написать подобие join, так как в самом Realm такого оператора нет, но пересмотрев немного код, удалось побороть этот недуг.

Заключение

Realm действительно очень удобная ORM, которая позволяет не задумываться о создании таблицы, поддержки её в актуальном состоянии, билдить каждый раз проект, чтобы получить сгенерированные классы (в Realm тоже есть сгенерированный код, но он в процессе написания основного кода не нужен). Но также стоит учитывать особенности работы с ним. Надеюсь, данная статья помогла вам узнать и другую сторону этой ORM и принять решение по внедрению её в проект. Лично я считаю, что потраченное время на все эти грабли стоило того, и в итоге мы получили хороший, быстрый инструмент. Всем спасибо за внимание.

Читать и комментировать