داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

همه کاربران راه اندازی سریع و UI پاسخگو در برنامه های تلفن همراه را مسلم می دانند. اگر برنامه زمان زیادی طول بکشد تا راه اندازی شود، کاربر شروع به احساس غم و عصبانیت می کند. شما به راحتی می توانید تجربه مشتری را خراب کنید یا کاربر را حتی قبل از شروع استفاده از برنامه کاملاً از دست بدهید.

زمانی متوجه شدیم که برنامه Dodo Pizza به طور متوسط ​​3 ثانیه طول می کشد تا راه اندازی شود و برای برخی از "خوش شانس ها" 15 تا 20 ثانیه طول می کشد.

در زیر برش داستانی با پایانی خوش آمده است: در مورد رشد پایگاه داده Realm، نشت حافظه، نحوه جمع آوری اشیاء تو در تو، و سپس جمع کردن خود و رفع همه چیز.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد
نویسنده مقاله: ماکسیم کاچینکین - توسعه دهنده اندروید در Dodo Pizza.

سه ثانیه از کلیک بر روی نماد برنامه تا onResume() اولین فعالیت بی نهایت است. و برای برخی از کاربران، زمان راه اندازی به 15-20 ثانیه رسید. اصلا این چطور ممکن است؟

خلاصه ای بسیار کوتاه برای کسانی که وقت خواندن ندارند
پایگاه داده Realm ما بی نهایت رشد کرد. برخی از اشیاء تودرتو حذف نشدند، اما دائماً انباشته شدند. زمان راه اندازی برنامه به تدریج افزایش یافت. سپس آن را برطرف کردیم و زمان راه اندازی به هدف رسید - کمتر از 1 ثانیه شد و دیگر افزایش پیدا نکرد. این مقاله شامل تجزیه و تحلیل وضعیت و دو راه حل است - یک راه حل سریع و یک راه حل معمولی.

جستجو و تجزیه و تحلیل مسئله

امروزه هر اپلیکیشن موبایلی باید به سرعت راه اندازی شود و پاسخگو باشد. اما این فقط مربوط به اپلیکیشن موبایل نیست. تجربه کاربر از تعامل با یک سرویس و یک شرکت چیز پیچیده ای است. به عنوان مثال، در مورد ما، سرعت تحویل یکی از شاخص های کلیدی برای سرویس پیتزا است. اگر تحویل سریع باشد، پیتزا داغ می شود و مشتری که الان می خواهد غذا بخورد، نیازی به انتظار طولانی نخواهد داشت. برای برنامه، به نوبه خود، ایجاد احساس سرویس سریع مهم است، زیرا اگر برنامه تنها 20 ثانیه طول بکشد تا راه اندازی شود، پس چه مدت باید برای پیتزا صبر کنید؟

در ابتدا، ما خودمان با این واقعیت روبرو بودیم که گاهی اوقات برنامه چند ثانیه طول می کشد تا راه اندازی شود و سپس شروع به شنیدن شکایت از سایر همکاران در مورد مدت زمان آن کردیم. اما ما نتوانستیم این وضعیت را به طور مداوم تکرار کنیم.

چه مدت است؟ مطابق با اسناد گوگل، اگر شروع سرد یک برنامه کمتر از 5 ثانیه طول بکشد، آنگاه به عنوان "عادی" در نظر گرفته می شود. برنامه اندروید Dodo Pizza راه اندازی شد (طبق معیارهای Firebase _app_start) در شروع سرد به طور متوسط ​​در 3 ثانیه - همانطور که می گویند "عالی نیست، وحشتناک نیست".

اما پس از آن شکایت هایی ظاهر شد که برنامه راه اندازی بسیار بسیار بسیار طولانی طول کشید! برای شروع، ما تصمیم گرفتیم اندازه "بسیار، بسیار، بسیار طولانی" را اندازه گیری کنیم. و برای این کار از Firebase Trace استفاده کردیم ردیابی شروع برنامه.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

این ردیابی استاندارد زمان بین لحظه ای که کاربر برنامه را باز می کند و لحظه ای که onResume() اولین فعالیت اجرا می شود را اندازه گیری می کند. در کنسول Firebase این معیار _app_start نامیده می شود. معلوم شد که:

  • زمان راه اندازی برای کاربران بالای صدک 95 نزدیک به 20 ثانیه است (بعضی از آنها حتی طولانی تر)، با وجود اینکه میانگین زمان راه اندازی سرد کمتر از 5 ثانیه است.
  • زمان راه اندازی یک مقدار ثابت نیست، اما در طول زمان رشد می کند. اما گاهی اوقات قطرات وجود دارد. ما این الگو را زمانی پیدا کردیم که مقیاس تجزیه و تحلیل را به 90 روز افزایش دادیم.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

دو فکر به ذهنم خطور کرد:

  1. چیزی نشت می کند.
  2. این "چیزی" پس از انتشار مجدد تنظیم می شود و سپس دوباره نشت می کند.

ما فکر کردیم "احتمالاً چیزی در پایگاه داده وجود دارد" و حق با ما بود. ابتدا از پایگاه داده به عنوان کش استفاده می کنیم و در حین مهاجرت آن را پاک می کنیم. در مرحله دوم، پایگاه داده با شروع برنامه بارگذاری می شود. همه چیز با هم جور در می آید.

چه مشکلی در پایگاه داده Realm وجود دارد

ما شروع به بررسی چگونگی تغییر محتویات پایگاه داده در طول عمر برنامه، از اولین نصب و سپس در طول استفاده فعال کردیم. شما می توانید محتویات پایگاه داده Realm را از طریق مشاهده کنید استتو و یا با جزئیات بیشتر و واضح تر با باز کردن فایل از طریق Realm Studio. برای مشاهده محتویات پایگاه داده از طریق ADB، فایل پایگاه داده Realm را کپی کنید:

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

با نگاهی به محتویات پایگاه داده در زمان های مختلف، متوجه شدیم که تعداد اشیاء یک نوع خاص به طور مداوم در حال افزایش است.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد
تصویر بخشی از Realm Studio را برای دو فایل نشان می دهد: در سمت چپ - پایگاه برنامه مدتی پس از نصب، در سمت راست - پس از استفاده فعال. دیده می شود که تعداد اشیاء ImageEntity и MoneyType به طور قابل توجهی رشد کرده است (تصویر صفحه تعداد اشیاء هر نوع را نشان می دهد).

رابطه بین رشد پایگاه داده و زمان راه اندازی

رشد کنترل نشده پایگاه داده بسیار بد است. اما این موضوع چگونه بر زمان راه اندازی اپلیکیشن تاثیر می گذارد؟ اندازه گیری این مورد از طریق ActivityManager بسیار آسان است. از اندروید 4.4، logcat گزارش را با رشته نمایش داده شده و زمان نمایش می دهد. این زمان برابر است با فاصله زمانی از راه اندازی برنامه تا پایان رندر فعالیت. در این مدت حوادث زیر رخ می دهد:

  • فرآیند را شروع کنید.
  • مقداردهی اولیه اشیا
  • ایجاد و مقداردهی اولیه فعالیت ها.
  • ایجاد یک طرح.
  • رندر اپلیکیشن.

مناسب ماست اگر ADB را با پرچم های -S و -W اجرا می کنید، می توانید با زمان راه اندازی خروجی طولانی دریافت کنید:

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

اگه از اونجا بگیری grep -i WaitTime زمان، می توانید جمع آوری این متریک را خودکار کنید و به صورت بصری به نتایج نگاه کنید. نمودار زیر وابستگی زمان راه اندازی برنامه را به تعداد شروع سرد برنامه نشان می دهد.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

در همان زمان، ماهیت یکسانی از رابطه بین اندازه و رشد پایگاه داده وجود داشت که از 4 مگابایت به 15 مگابایت افزایش یافت. در مجموع، معلوم می شود که با گذشت زمان (با رشد شروع سرد)، هم زمان راه اندازی برنامه و هم اندازه پایگاه داده افزایش می یابد. ما یک فرضیه در دست داریم. اکنون تنها چیزی که باقی مانده بود تأیید وابستگی بود. بنابراین، تصمیم گرفتیم «نشت‌ها» را حذف کنیم و ببینیم که آیا این کار پرتاب را سرعت می‌بخشد.

دلایل رشد بی پایان پایگاه داده

قبل از از بین بردن "نشت"، ارزش درک اینکه چرا آنها در وهله اول ظاهر شدند. برای انجام این کار، بیایید به یاد بیاوریم که Realm چیست.

Realm یک پایگاه داده غیر رابطه ای است. این به شما امکان می دهد روابط بین اشیاء را به روشی مشابه با تعداد پایگاه داده های رابطه ای ORM در Android توصیف کنید. در عین حال، Realm اشیاء را مستقیماً با کمترین تغییر و نگاشت در حافظه ذخیره می کند. این به شما امکان می‌دهد داده‌ها را از روی دیسک بسیار سریع بخوانید، که این نقطه قوت Realm است و دلیل محبوبیت آن است.

(برای اهداف این مقاله، این توضیحات برای ما کافی خواهد بود. شما می توانید در مورد Realm بیشتر بخوانید مستندات یا در آنها آکادمی).

بسیاری از توسعه دهندگان به کار بیشتر با پایگاه داده های رابطه ای (به عنوان مثال، پایگاه های داده ORM با SQL در زیر هود) عادت دارند. و چیزهایی مانند حذف آبشاری داده ها اغلب به نظر می رسد. اما نه در قلمرو.

به هر حال، ویژگی حذف آبشار برای مدت طولانی درخواست شده است. این تجدید نظر и یکی دیگر، مرتبط با آن، به طور فعال مورد بحث قرار گرفت. این احساس وجود داشت که به زودی انجام می شود. اما پس از آن همه چیز به معرفی لینک های قوی و ضعیف تبدیل شد که به طور خودکار این مشکل را حل می کند. در این کار کاملاً پر جنب و جوش و فعال بود درخواست کشش، که فعلاً به دلیل مشکلات داخلی متوقف شده است.

نشت داده ها بدون حذف آبشاری

اگر به یک حذف آبشاری غیرموجود تکیه کنید، دقیقاً چگونه داده نشت می کند؟ اگر اشیاء Realm تودرتو دارید، باید حذف شوند.
بیایید به یک مثال (تقریبا) واقعی نگاه کنیم. ما یک شی داریم 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()

کالای موجود در سبد دارای فیلدهای مختلفی از جمله تصویر می باشد ImageEntity، مواد سفارشی CustomizationEntity. همچنین، محصول موجود در سبد خرید می تواند ترکیبی با مجموعه محصولات خاص خود باشد RealmList (CartProductEntity). همه فیلدهای فهرست شده اشیاء Realm هستند. اگر یک شی جدید (copyToRealm() / copyToRealmOrUpdate()) با همان شناسه وارد کنیم، آنگاه این شی به طور کامل رونویسی می شود. اما تمام اشیاء داخلی (تصویر، customizationEntity و cartComboProducts) ارتباط خود را با والد قطع می‌کنند و در پایگاه داده باقی می‌مانند.

از آنجایی که ارتباط با آنها از بین رفته است، دیگر آنها را نمی خوانیم یا حذف نمی کنیم (مگر اینکه صریحاً به آنها دسترسی داشته باشیم یا کل "جدول" را پاک کنیم). ما این را "نشت حافظه" نامیدیم.

هنگامی که با Realm کار می کنیم، باید به صراحت تمام عناصر را مرور کنیم و قبل از انجام چنین عملیاتی همه چیز را به صراحت حذف کنیم. این را می توان به عنوان مثال به صورت زیر انجام داد:

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()
}
// и потом уже сохраняем

اگر این کار را انجام دهید، همه چیز همانطور که باید کار می کند. در این مثال، ما فرض می‌کنیم که در داخل image، customizationEntity و cartComboProducts هیچ شیء تودرتوی دیگری وجود ندارد، بنابراین حلقه‌ها و حذف‌های تودرتو دیگری وجود ندارد.

راه حل "سریع".

اولین کاری که تصمیم گرفتیم انجام دهیم این بود که اشیایی که سریع‌ترین رشد را دارند تمیز کنیم و نتایج را بررسی کنیم تا ببینیم آیا این کار مشکل اصلی ما را حل می‌کند یا خیر. ابتدا ساده ترین و شهودی ترین راه حل ساخته شد، یعنی: هر شی باید مسئول حذف فرزندان خود باشد. برای انجام این کار، ما یک رابط معرفی کردیم که لیستی از اشیاء تودرتوی Realm خود را برمی گرداند:

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

و ما آن را در اشیاء Realm خود پیاده سازی کردیم:

@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 ما همه کودکان را به عنوان یک لیست ثابت برمی گردانیم. و هر شی فرزند همچنین می‌تواند رابط NestedEntityAware را پیاده‌سازی کند، که نشان می‌دهد برای مثال، اشیاء Realm داخلی برای حذف دارد. 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
   )
 }
}

و به همین ترتیب، لانه سازی اشیاء را می توان تکرار کرد.

سپس متدی می نویسیم که به صورت بازگشتی همه اشیاء تودرتو را حذف می کند. روش (ساخته شده به عنوان پسوند) deleteAllNestedEntities تمام اشیاء و روش های سطح بالا را دریافت می کند deleteNestedRecursively با استفاده از رابط NestedEntityAware همه اشیاء تودرتو را به صورت بازگشتی حذف می کند:

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()
   }
 }
}

ما این کار را با اشیایی که سریع‌ترین رشد را داشتند انجام دادیم و بررسی کردیم که چه اتفاقی افتاده است.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

در نتیجه، اجسامی که با این محلول پوشانده بودیم رشد نکردند. و رشد کلی پایه کند شد، اما متوقف نشد.

راه حل "عادی".

اگرچه پایه شروع به رشد کندتر کرد، اما همچنان رشد کرد. بنابراین شروع به جستجوی بیشتر کردیم. پروژه ما بسیار فعال از کش داده ها در Realm استفاده می کند. بنابراین، نوشتن همه اشیاء تو در تو برای هر شی کار فشرده است، به علاوه خطر خطا افزایش می یابد، زیرا می توانید هنگام تغییر کد، تعیین اشیاء را فراموش کنید.

می‌خواستم مطمئن شوم که از رابط‌ها استفاده نمی‌کنم، اما همه چیز خود به خود کار می‌کند.

وقتی می خواهیم چیزی به تنهایی کار کند، باید از بازتاب استفاده کنیم. برای انجام این کار، می‌توانیم هر فیلد کلاس را مرور کنیم و بررسی کنیم که آیا یک شی Realm است یا لیستی از اشیاء:

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

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

اگر فیلد RealmModel یا RealmList است، شیء این فیلد را به لیستی از اشیاء تودرتو اضافه کنید. همه چیز دقیقاً همان است که در بالا انجام دادیم، فقط در اینجا به خودی خود انجام می شود. خود روش حذف آبشاری بسیار ساده است و به شکل زیر است:

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()
         }
       }
 }
}

افزونه filterRealmObject فقط اشیاء Realm را فیلتر کرده و عبور می دهد. روش getNestedRealmObjects از طریق انعکاس، تمام اشیاء تودرتو در قلمرو را پیدا کرده و آنها را در یک لیست خطی قرار می دهد. سپس همین کار را به صورت بازگشتی انجام می دهیم. هنگام حذف، باید اعتبار شی را بررسی کنید isValid، زیرا ممکن است که اشیاء والد مختلف می توانند موارد مشابه تو در تو داشته باشند. بهتر است از این کار اجتناب کنید و به سادگی از تولید خودکار id هنگام ایجاد اشیاء جدید استفاده کنید.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

پیاده سازی کامل متد getNestedRealmObjects

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)
}

در نتیجه، در کد مشتری خود از "حذف آبشاری" برای هر عملیات تغییر داده استفاده می کنیم. به عنوان مثال، برای عملیات درج به این شکل است:

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 }
 ))
}

روش اول getManagedEntities تمام اشیاء اضافه شده و سپس متد را دریافت می کند cascadeDelete به صورت بازگشتی همه اشیاء جمع آوری شده را قبل از نوشتن موارد جدید حذف می کند. ما در نهایت از این روش در سراسر برنامه استفاده می کنیم. نشت حافظه در Realm کاملاً از بین رفته است. با انجام همان اندازه گیری وابستگی زمان راه اندازی به تعداد شروع سرد برنامه، نتیجه را مشاهده می کنیم.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

خط سبز وابستگی زمان راه اندازی برنامه را به تعداد شروع سرد در حین حذف خودکار آبشاری اشیاء تودرتو نشان می دهد.

نتایج و نتیجه گیری

پایگاه داده در حال رشد Realm باعث می شد که برنامه بسیار کند راه اندازی شود. ما یک به‌روزرسانی با «حذف آبشاری» اشیاء تودرتو منتشر کردیم. و اکنون ما نظارت و ارزیابی می کنیم که چگونه تصمیم ما بر زمان راه اندازی برنامه از طریق متریک _app_start تاثیر گذاشته است.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

برای تجزیه و تحلیل، ما یک دوره زمانی 90 روزه را در نظر می گیریم و می بینیم: زمان راه اندازی برنامه، هم میانگین و هم زمانی که روی صدک 95 کاربران قرار می گیرد، شروع به کاهش کرد و دیگر افزایش نمی یابد.

داستان چگونگی حذف آبشار در قلمرو در یک راه‌اندازی طولانی پیروز شد

اگر به نمودار هفت روزه نگاه کنید، متریک _app_start کاملاً کافی به نظر می رسد و کمتر از 1 ثانیه است.

همچنین شایان ذکر است که اگر میانگین مقدار _app_start از 5 ثانیه بیشتر شود، به‌طور پیش‌فرض، Firebase اعلان‌هایی را ارسال می‌کند. با این حال، همانطور که می بینیم، شما نباید به این تکیه کنید، بلکه باید وارد شوید و آن را به صراحت بررسی کنید.

نکته خاص در مورد پایگاه داده Realm این است که یک پایگاه داده غیر رابطه ای است. علیرغم سهولت استفاده، شباهت به راه حل های ORM و پیوند شیء، حذف آبشاری ندارد.

اگر این مورد در نظر گرفته نشود، اشیاء تو در تو جمع می شوند و "نشت می کنند". پایگاه داده به طور مداوم رشد می کند که به نوبه خود بر کاهش سرعت یا راه اندازی برنامه تأثیر می گذارد.

من تجربه خود را در مورد نحوه انجام سریع حذف آبشاری اشیاء در Realm به اشتراک گذاشتم، چیزی که هنوز از جعبه خارج نشده است، اما مدتهاست در مورد آن صحبت شده است. آنها می گویند и آنها می گویند. در مورد ما، این امر زمان راه‌اندازی اپلیکیشن را بسیار افزایش داد.

با وجود بحث در مورد ظهور قریب الوقوع این ویژگی، عدم حذف آبشار در Realm با طراحی انجام می شود. اگر در حال طراحی یک برنامه جدید هستید، پس این را در نظر بگیرید. و اگر قبلاً از Realm استفاده می کنید، بررسی کنید که آیا چنین مشکلاتی دارید.

منبع: www.habr.com

اضافه کردن نظر