Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Bütün istifadəçilər mobil tətbiqlərdə sürətli işə salma və cavab verən UI-dən istifadə edirlər. Tətbiqin işə salınması uzun müddət çəkirsə, istifadəçi kədərlənməyə və qəzəblənməyə başlayır. Siz müştəri təcrübəsini asanlıqla poza və ya istifadəçini proqramdan istifadə etməyə başlamazdan əvvəl tamamilə itirə bilərsiniz.

Biz bir dəfə kəşf etdik ki, Dodo Pizza tətbiqinin işə salınması orta hesabla 3 saniyə çəkir, bəzi “şanslılar” üçün isə 15-20 saniyə çəkir.

Kesimin altında xoşbəxt sonluqla bitən bir hekayə var: Realm verilənlər bazasının böyüməsi, yaddaş sızması, iç-içə obyektləri necə yığdığımız və sonra özümüzü bir yerə yığıb hər şeyi düzəltdiyimiz haqqında.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl
Məqalə müəllifi: Maksim Kaçinkin — Dodo Pizza-da Android developeri.

Tətbiq ikonasına klikləməklə ilk fəaliyyətin onResume() funksiyasına qədər olan üç saniyə sonsuzdur. Bəzi istifadəçilər üçün başlanğıc vaxtı 15-20 saniyəyə çatdı. Bu necə mümkündür?

Oxumağa vaxtı olmayanlar üçün çox qısa xülasə
Realm verilənlər bazamız sonsuz şəkildə böyüdü. Bəzi yuvalanmış obyektlər silinmədi, lakin daim yığıldı. Tətbiqin başlama vaxtı tədricən artdı. Sonra onu düzəltdik və başlanğıc vaxtı hədəfə çatdı - 1 saniyədən az oldu və artıq artmadı. Məqalədə vəziyyətin təhlili və iki həll yolu var - sürətli və normal.

Problemin axtarışı və təhlili

Bu gün istənilən mobil proqram tez işə düşməli və həssas olmalıdır. Amma bu, təkcə mobil proqrama aid deyil. İstifadəçinin xidmət və şirkətlə qarşılıqlı əlaqə təcrübəsi mürəkkəb bir şeydir. Məsələn, bizim vəziyyətimizdə çatdırılma sürəti pizza xidmətinin əsas göstəricilərindən biridir. Çatdırılma sürətli olarsa, pizza isti olacaq və indi yemək istəyən müştəri çox gözləməli olmayacaq. Tətbiq üçün öz növbəsində sürətli xidmət hissi yaratmaq vacibdir, çünki tətbiqin işə salınması cəmi 20 saniyə çəkirsə, o zaman pizzanı nə qədər gözləməli olacaqsınız?

Əvvəlcə biz özümüz də bəzən tətbiqin işə salınması bir neçə saniyə çəkdiyi faktı ilə qarşılaşdıq və sonra digər həmkarlarımızdan bunun nə qədər vaxt çəkdiyi ilə bağlı şikayətlər eşitməyə başladıq. Amma biz bu vəziyyəti ardıcıl olaraq təkrarlaya bilmədik.

Uzunluğu nə qədərdir? görə Google sənədləri, əgər proqramın soyuq başlaması 5 saniyədən az çəkirsə, bu, “normal kimi” hesab olunur. Dodo Pizza Android proqramı işə salındı ​​(Firebase göstəricilərinə görə _app_start) saat soyuq başlanğıc orta hesabla 3 saniyədə - necə deyərlər, "Əla deyil, dəhşətli deyil".

Ancaq sonra şikayətlər görünməyə başladı ki, tətbiqin işə salınması çox, çox, çox uzun müddət çəkdi! Başlamaq üçün "çox, çox, çox uzun" nə olduğunu ölçmək qərarına gəldik. Və bunun üçün Firebase izindən istifadə etdik Proqram başlanğıc izi.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Bu standart iz istifadəçinin tətbiqi açdığı andan ilk fəaliyyətin onResume() yerinə yetirildiyi an arasındakı vaxtı ölçür. Firebase Konsolunda bu göstərici _app_start adlanır. Məlum oldu ki:

  • 95-ci faizdən yuxarı olan istifadəçilər üçün başlanğıc vaxtları, orta soyuq başlanğıc vaxtının 20 saniyədən az olmasına baxmayaraq, təxminən 5 saniyədir (bəziləri daha uzundur).
  • Başlanğıc vaxtı sabit bir dəyər deyil, zaman keçdikcə artır. Ancaq bəzən damcılar olur. Təhlil miqyasını 90 günə qədər artırdıqda bu nümunəni tapdıq.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Ağlıma iki fikir gəldi:

  1. Nəsə sızır.
  2. Bu "bir şey" buraxıldıqdan sonra yenidən qurulur və sonra yenidən sızır.

“Yəqin ki, verilənlər bazası ilə bağlı bir şeydir” deyə düşündük və haqlı idik. Birincisi, verilənlər bazasını keş kimi istifadə edirik, köçürmə zamanı onu təmizləyirik. İkincisi, proqram başlayanda verilənlər bazası yüklənir. Hər şey bir-birinə uyğun gəlir.

Realm verilənlər bazasında nə səhvdir

Biz ilk quraşdırmadan başlayaraq aktiv istifadə zamanı verilənlər bazasının məzmununun tətbiqin ömrü boyu necə dəyişdiyini yoxlamağa başladıq. Realm verilənlər bazasının məzmununa vasitəsilə baxa bilərsiniz steto vasitəsilə faylı açaraq daha ətraflı və aydın şəkildə Realm Studio. AİB vasitəsilə verilənlər bazasının məzmununa baxmaq üçün Realm verilənlər bazası faylını kopyalayın:

adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}

Müxtəlif vaxtlarda verilənlər bazasının məzmununa nəzər salaraq müəyyən tipli obyektlərin sayının durmadan artdığını bildik.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl
Şəkildə Realm Studio-nun iki fayl üçün fraqmenti göstərilir: solda - quraşdırmadan bir müddət sonra proqram bazası, sağda - aktiv istifadədən sonra. Obyektlərin sayını görmək olar ImageEntity и MoneyType əhəmiyyətli dərəcədə artmışdır (ekran görüntüsü hər bir növ obyektlərin sayını göstərir).

Verilənlər bazası artımı və başlanğıc vaxtı arasında əlaqə

Nəzarətsiz verilənlər bazası artımı çox pisdir. Bəs bu, tətbiqin başlama vaxtına necə təsir edir? Bunu ActivityManager vasitəsilə ölçmək olduqca asandır. Android 4.4-dən bəri logcat qeydi Göstərilən sətir və vaxtla göstərir. Bu vaxt proqramın işə salındığı andan fəaliyyət göstərilməsinin sonuna qədər olan intervala bərabərdir. Bu müddət ərzində aşağıdakı hadisələr baş verir:

  • Prosesə başlayın.
  • Obyektlərin işə salınması.
  • Fəaliyyətlərin yaradılması və işə salınması.
  • Layout yaradılması.
  • Tətbiqin göstərilməsi.

Bizə yaraşır. ADB-ni -S və -W bayraqları ilə işlədirsinizsə, başlanğıc vaxtı ilə uzadılmış çıxış əldə edə bilərsiniz:

adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

Ordan tutsan grep -i WaitTime vaxt, siz bu metrikanın toplanmasını avtomatlaşdıra və nəticələrə vizual olaraq baxa bilərsiniz. Aşağıdakı qrafik proqramın işə düşmə vaxtının tətbiqin soyuq başlanğıclarının sayından asılılığını göstərir.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Eyni zamanda, 4 MB-dan 15 MB-a qədər böyüyən verilənlər bazasının ölçüsü və böyüməsi arasında əlaqənin eyni xarakteri var idi. Ümumilikdə məlum olur ki, zaman keçdikcə (soyuq başlanğıcların artması ilə) həm tətbiqin işə salınma vaxtı, həm də verilənlər bazasının ölçüsü artıb. Bizim əlimizdə bir fərziyyə var. İndi yalnız asılılığı təsdiqləmək qaldı. Buna görə də, "sızmaları" aradan qaldırmaq və bunun işə salınmanı sürətləndirib-tezləşdirməyini görmək qərarına gəldik.

Sonsuz verilənlər bazası artımının səbəbləri

"Sızıntıları" aradan qaldırmazdan əvvəl onların niyə ilk növbədə göründüyünü başa düşməyə dəyər. Bunu etmək üçün gəlin Realm nə olduğunu xatırlayaq.

Realm əlaqəsiz verilənlər bazasıdır. O, Android-də neçə ORM relational verilənlər bazası təsvir edildiyi kimi obyektlər arasındakı münasibətləri təsvir etməyə imkan verir. Eyni zamanda, Realm obyektləri birbaşa yaddaşda ən az transformasiya və xəritələşdirmə ilə saxlayır. Bu, diskdən məlumatları çox tez oxumağa imkan verir ki, bu da Realmın gücüdür və nə üçün sevilir.

(Bu məqalənin məqsədləri üçün bu təsvir bizim üçün kifayət edəcəkdir. Realm haqqında daha çox oxuya bilərsiniz sənədləşdirmə ya da onların içində akademiya).

Bir çox tərtibatçılar daha çox əlaqəli verilənlər bazaları ilə işləməyə öyrəşiblər (məsələn, SQL ilə ORM verilənlər bazaları). Və məlumatların kaskad silinməsi kimi şeylər çox vaxt verilmiş kimi görünür. Amma səltənətdə deyil.

Yeri gəlmişkən, kaskad silmə xüsusiyyəti uzun müddətdir tələb olunur. Bu təftiş и başqa, onunla bağlı fəal müzakirələr aparılmışdır. Bunun tezliklə həyata keçəcəyi hissi var idi. Ancaq sonra hər şey güclü və zəif bağlantıların tətbiqinə çevrildi, bu da avtomatik olaraq bu problemi həll edəcəkdir. Bu işdə kifayət qədər canlı və aktiv idi çəkmə tələbi, daxili çətinliklərə görə hələlik fasilə verilmişdir.

Kaskad silmə olmadan məlumat sızması

Mövcud olmayan kaskadlı silməyə etibar etsəniz, məlumat tam olaraq necə sızır? Əgər sizdə daxili Realm obyektləri varsa, onlar silinməlidir.
Gəlin (demək olar ki,) real nümunəyə baxaq. Bizim obyektimiz var CartItemEntity:

@RealmClass
class CartItemEntity(
 @PrimaryKey
 override var id: String? = null,
 ...
 var name: String = "",
 var description: String = "",
 var image: ImageEntity? = null,
 var category: String = MENU_CATEGORY_UNKNOWN_ID,
 var customizationEntity: CustomizationEntity? = null,
 var cartComboProducts: RealmList<CartProductEntity> = RealmList(),
 ...
) : RealmObject()

Səbətdəki məhsulun müxtəlif sahələri, o cümlədən şəkil var ImageEntity, xüsusi inqrediyentlər CustomizationEntity. Həmçinin səbətdəki məhsul öz məhsul dəsti ilə kombin ola bilər RealmList (CartProductEntity). Bütün sadalanan sahələr Realm obyektləridir. Eyni id ilə yeni obyekt (copyToRealm() / copyToRealmOrUpdate()) daxil etsək, bu obyekt tamamilə üzərinə yazılacaq. Lakin bütün daxili obyektlər (şəkil, customizationEntity və cartComboProducts) valideynlə əlaqəni itirəcək və verilənlər bazasında qalacaq.

Onlarla əlaqə kəsildiyi üçün biz artıq onları oxumuruq və ya silmirik (açıq şəkildə onlara daxil olmadıqda və ya bütün “cədvəl”i silməyincə). Biz bunu “yaddaş sızması” adlandırdıq.

Realm ilə işləyərkən biz açıq şəkildə bütün elementləri nəzərdən keçirməli və bu cür əməliyyatlardan əvvəl hər şeyi açıq şəkildə silməliyik. Bu, məsələn, belə edilə bilər:

val entity = realm.where(CartItemEntity::class.java).equalTo("id", id).findFirst()
if (first != null) {
 deleteFromRealm(first.image)
 deleteFromRealm(first.customizationEntity)
 for(cartProductEntity in first.cartComboProducts) {
   deleteFromRealm(cartProductEntity)
 }
 first.deleteFromRealm()
}
// и потом уже сохраняем

Bunu etsəniz, hər şey lazım olduğu kimi işləyəcək. Bu misalda biz güman edirik ki, image, customizationEntity və cartComboProducts daxilində başqa iç içə salınmış Realm obyektləri yoxdur, ona görə də başqa iç-içə döngələr və silmələr yoxdur.

"Tez" həll

Etməyə qərar verdiyimiz ilk şey, ən sürətlə böyüyən obyektləri təmizləmək və bunun orijinal problemimizi həll edib-etməyəcəyini görmək üçün nəticələri yoxlamaq oldu. Birincisi, ən sadə və ən intuitiv həll edildi, yəni: hər bir obyekt öz uşaqlarının çıxarılmasına cavabdeh olmalıdır. Bunu etmək üçün biz daxili Realm obyektlərinin siyahısını qaytaran bir interfeys təqdim etdik:

interface NestedEntityAware {
 fun getNestedEntities(): Collection<RealmObject?>
}

Və biz onu Realm obyektlərimizdə həyata keçirdik:

@RealmClass
class DataPizzeriaEntity(
 @PrimaryKey
 var id: String? = null,
 var name: String? = null,
 var coordinates: CoordinatesEntity? = null,
 var deliverySchedule: ScheduleEntity? = null,
 var restaurantSchedule: ScheduleEntity? = null,
 ...
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       coordinates,
       deliverySchedule,
       restaurantSchedule
   )
 }
}

В getNestedEntities biz bütün uşaqları düz siyahı kimi qaytarırıq. Və hər bir uşaq obyekt də NestedEntityAware interfeysini həyata keçirə bilər ki, bu da onun silmək üçün daxili Realm obyektlərinin olduğunu göstərir, məsələn ScheduleEntity:

@RealmClass
class ScheduleEntity(
 var monday: DayOfWeekEntity? = null,
 var tuesday: DayOfWeekEntity? = null,
 var wednesday: DayOfWeekEntity? = null,
 var thursday: DayOfWeekEntity? = null,
 var friday: DayOfWeekEntity? = null,
 var saturday: DayOfWeekEntity? = null,
 var sunday: DayOfWeekEntity? = null
) : RealmObject(), NestedEntityAware {

 override fun getNestedEntities(): Collection<RealmObject?> {
   return listOf(
       monday, tuesday, wednesday, thursday, friday, saturday, sunday
   )
 }
}

Və s., obyektlərin yuvalanması təkrarlana bilər.

Sonra bütün iç-içə obyektləri rekursiv silən metod yazırıq. Metod (uzatma şəklində hazırlanmışdır) deleteAllNestedEntities bütün yüksək səviyyəli obyektləri və metodu əldə edir deleteNestedRecursively NestedEntityAware interfeysindən istifadə edərək bütün daxili obyektləri rekursiv şəkildə silir:

fun <T> Realm.deleteAllNestedEntities(entities: Collection<T>,
 entityClass: Class<out RealmObject>,
 idMapper: (T) -> String,
 idFieldName : String = "id"
 ) {

 val existedObjects = where(entityClass)
     .`in`(idFieldName, entities.map(idMapper).toTypedArray())
     .findAll()

 deleteNestedRecursively(existedObjects)
}

private fun Realm.deleteNestedRecursively(entities: Collection<RealmObject?>) {
 for(entity in entities) {
   entity?.let { realmObject ->
     if (realmObject is NestedEntityAware) {
       deleteNestedRecursively((realmObject as NestedEntityAware).getNestedEntities())
     }
     realmObject.deleteFromRealm()
   }
 }
}

Biz bunu ən sürətlə böyüyən obyektlərlə etdik və baş verənləri yoxladıq.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Nəticədə bu məhlulla örtdüyümüz obyektlər böyüməyi dayandırdı. Və bazanın ümumi artımı yavaşladı, amma dayanmadı.

"Normal" həll

Baza daha yavaş böyüməyə başlasa da, yenə də böyüdü. Beləliklə, daha çox axtarmağa başladıq. Layihəmiz Realm-də məlumatların keşləşdirilməsindən çox fəal istifadə edir. Buna görə də, hər bir obyekt üçün bütün yuvalanmış obyektlərin yazılması çox əmək tələb edir, üstəlik səhv riski artır, çünki kodu dəyişdirərkən obyektləri göstərməyi unuda bilərsiniz.

İnterfeyslərdən istifadə etmədiyimə, lakin hər şeyin öz-özünə işlədiyinə əmin olmaq istədim.

Bir şeyin öz-özünə işləməsini istədikdə, əks etdirmə üsulundan istifadə etməliyik. Bunu etmək üçün hər bir sinif sahəsinə keçib onun Realm obyekti və ya obyektlərin siyahısı olub olmadığını yoxlaya bilərik:

RealmModel::class.java.isAssignableFrom(field.type)

RealmList::class.java.isAssignableFrom(field.type)

Sahə RealmModel və ya RealmListdirsə, bu sahənin obyektini iç-içə obyektlərin siyahısına əlavə edin. Hər şey yuxarıda etdiyimiz kimidir, yalnız burada özü tərəfindən ediləcək. Kaskad silmə üsulunun özü çox sadədir və belə görünür:

fun <T : Any> Realm.cascadeDelete(entities: Collection<T?>) {
 if(entities.isEmpty()) {
   return
 }

 entities.filterNotNull().let { notNullEntities ->
   notNullEntities
       .filterRealmObject()
       .flatMap { realmObject -> getNestedRealmObjects(realmObject) }
       .also { realmObjects -> cascadeDelete(realmObjects) }

   notNullEntities
       .forEach { entity ->
         if((entity is RealmObject) && entity.isValid) {
           entity.deleteFromRealm()
         }
       }
 }
}

Uzatma filterRealmObject yalnız Realm obyektlərini süzür və keçir. Metod getNestedRealmObjects əks etdirərək, o, bütün daxili səltənət obyektlərini tapır və onları xətti siyahıya qoyur. Sonra eyni şeyi rekursiv edirik. Silərkən, obyektin etibarlılığını yoxlamaq lazımdır isValid, çünki ola bilsin ki, müxtəlif ana obyektlər eyni obyektlərə daxil ola bilər. Bunun qarşısını almaq və yeni obyektlər yaratarkən sadəcə olaraq id-nin avtomatik yaradılmasından istifadə etmək daha yaxşıdır.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

getNestedRealmObjects metodunun tam tətbiqi

private fun getNestedRealmObjects(realmObject: RealmObject) : List<RealmObject> {
 val nestedObjects = mutableListOf<RealmObject>()
 val fields = realmObject.javaClass.superclass.declaredFields

// Проверяем каждое поле, не является ли оно RealmModel или списком RealmList
 fields.forEach { field ->
   when {
     RealmModel::class.java.isAssignableFrom(field.type) -> {
       try {
         val child = getChildObjectByField(realmObject, field)
         child?.let {
           if (isInstanceOfRealmObject(it)) {
             nestedObjects.add(child as RealmObject)
           }
         }
       } catch (e: Exception) { ... }
     }

     RealmList::class.java.isAssignableFrom(field.type) -> {
       try {
         val childList = getChildObjectByField(realmObject, field)
         childList?.let { list ->
           (list as RealmList<*>).forEach {
             if (isInstanceOfRealmObject(it)) {
               nestedObjects.add(it as RealmObject)
             }
           }
         }
       } catch (e: Exception) { ... }
     }
   }
 }

 return nestedObjects
}

private fun getChildObjectByField(realmObject: RealmObject, field: Field): Any? {
 val methodName = "get${field.name.capitalize()}"
 val method = realmObject.javaClass.getMethod(methodName)
 return method.invoke(realmObject)
}

Nəticədə, müştəri kodumuzda hər bir məlumatın dəyişdirilməsi əməliyyatı üçün “cascading sil” istifadə edirik. Məsələn, daxiletmə əməliyyatı üçün bu belə görünür:

override fun <T : Entity> insert(
 entityInformation: EntityInformation,
 entities: Collection<T>): Collection<T> = entities.apply {
 realmInstance.cascadeDelete(getManagedEntities(entityInformation, this))
 realmInstance.copyFromRealm(
     realmInstance
         .copyToRealmOrUpdate(this.map { entity -> entity as RealmModel }
 ))
}

Əvvəlcə üsul getManagedEntities bütün əlavə edilmiş obyektləri, sonra isə metodu qəbul edir cascadeDelete Yenilərini yazmazdan əvvəl bütün toplanmış obyektləri rekursiv şəkildə silir. Tətbiq boyu bu yanaşmadan istifadə edirik. Realm-də yaddaş sızması tamamilə yox oldu. Başlanğıc vaxtının tətbiqin soyuq başlanğıclarının sayından asılılığının eyni ölçülməsini həyata keçirərək nəticəni görürük.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Yaşıl xətt, tətbiqin işə salınma vaxtının daxili obyektlərin avtomatik kaskad silinməsi zamanı soyuq başlanğıcların sayından asılılığını göstərir.

Nəticələr və nəticələr

Daim böyüyən Realm verilənlər bazası tətbiqin çox yavaş işə salınmasına səbəb olurdu. İç içə daxil edilmiş obyektlərin öz "kaskad silmə"mizlə yeniləməsini buraxdıq. İndi biz _app_start metrikası vasitəsilə qərarımızın tətbiqin başlama vaxtına necə təsir etdiyini izləyirik və qiymətləndiririk.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Təhlil üçün biz 90 gün vaxt ayırırıq və görürük: tətbiqin işə salınma vaxtı həm median, həm də istifadəçilərin 95-ci faizinə düşən vaxt azalmağa başladı və artıq yüksəlmir.

Realm-ın uzun müddət işə salınmasında ardıcıl silinmənin necə qalib gəldiyinə dair nağıl

Yeddi günlük qrafikə baxsanız, _app_start metrikası tamamilə adekvat görünür və 1 saniyədən azdır.

Əlavə etmək lazımdır ki, standart olaraq, _app_start-ın median dəyəri 5 saniyədən çox olarsa, Firebase bildirişlər göndərir. Lakin, gördüyümüz kimi, buna güvənməməli, əksinə, içəri girib açıq şəkildə yoxlamalısınız.

Realm verilənlər bazası ilə bağlı xüsusi cəhət ondan ibarətdir ki, o, əlaqəli olmayan verilənlər bazasıdır. İstifadəsinin asanlığına, ORM həlləri ilə oxşarlığına və obyektin əlaqələndirilməsinə baxmayaraq, onun kaskad silinməsi yoxdur.

Bu nəzərə alınmazsa, o zaman yuvalanmış obyektlər yığılacaq və "sızacaq". Verilənlər bazası daim artacaq, bu da öz növbəsində tətbiqin yavaşlamasına və ya işə salınmasına təsir edəcək.

Hələ qutudan çıxmamış, lakin çoxdan danışılan Realm-də obyektlərin kaskad silinməsini necə tez bir zamanda etmək barədə təcrübəmizi bölüşdüm. govorят и govorят. Bizim vəziyyətimizdə bu, tətbiqin başlama vaxtını xeyli sürətləndirdi.

Bu xüsusiyyətin qaçılmaz görünüşü ilə bağlı müzakirələrə baxmayaraq, Realm-də kaskad silinməsinin olmaması dizaynla həyata keçirilir. Əgər siz yeni proqram tərtib edirsinizsə, bunu nəzərə alın. Əgər siz artıq Realm istifadə edirsinizsə, belə problemləriniz olub olmadığını yoxlayın.

Mənbə: www.habr.com

Добавить комментарий