حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

يأخذ جميع المستخدمين التشغيل السريع وواجهة المستخدم سريعة الاستجابة في تطبيقات الهاتف المحمول كأمر مسلم به. إذا بدأ التطبيق لفترة طويلة ، يبدأ المستخدم بالشعور بالحزن والغضب. من السهل إفساد تجربة العميل أو فقد المستخدم تمامًا حتى قبل أن يبدأ في استخدام التطبيق.

بمجرد أن وجدنا أن تطبيق Dodo Pizza يبدأ في المتوسط ​​3 ثوانٍ ، وبالنسبة لبعض "المحظوظين" يستغرق 15-20 ثانية.

تحت القص ، قصة بنهاية سعيدة: حول نمو قاعدة بيانات Realm ، تسرب الذاكرة ، كيف قمنا بتجميع كائنات متداخلة ، ثم جمعنا أنفسنا معًا وأصلح كل شيء.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل
كاتب المقال: مكسيم كاتشينكين - مطور Android في Dodo Pizza.

ثلاث ثوانٍ من النقر على أيقونة التطبيق إلى onResume () للنشاط الأول هي ما لا نهاية. وبالنسبة لبعض المستخدمين ، بلغ وقت بدء التشغيل 15-20 ثانية. كيف يتم ذلك حتى ممكن؟

ملخص موجز لمن ليس لديهم وقت للقراءة
نمت قاعدة بيانات عالمنا إلى ما لا نهاية. لم يتم حذف بعض الكائنات المتداخلة ، ولكن يتم تجميعها باستمرار. زيادة وقت بدء التطبيق تدريجيًا. ثم قمنا بإصلاحه ، وجاء وقت الإطلاق إلى الهدف - أصبح أقل من ثانية واحدة ولم يعد ينمو. في المقال تحليل للوضع وحلين - بطريقة سريعة وطريقة عادية.

بحث وتحليل المشكلة

اليوم ، يحتاج أي تطبيق جوال إلى التشغيل بسرعة والاستجابة. لكن الأمر لا يتعلق فقط بتطبيق الهاتف. تجربة المستخدم للتفاعل مع الخدمة والشركة أمر معقد. على سبيل المثال ، في حالتنا ، تعد سرعة التوصيل أحد المؤشرات الرئيسية لخدمة البيتزا. إذا كان التوصيل سريعًا ، فستكون البيتزا ساخنة ، ولن يضطر العميل الذي يريد أن يأكل الآن إلى الانتظار طويلاً. بالنسبة للتطبيق ، بدوره ، من المهم خلق شعور بالخدمة السريعة ، لأنه إذا بدأ التطبيق لمدة 20 ثانية فقط ، فإلى متى يجب أن تنتظر البيتزا؟

في البداية ، واجهنا أنفسنا حقيقة أنه في بعض الأحيان يبدأ التطبيق لبضع ثوان ، ثم بدأ الزملاء الآخرون في التواصل معنا بشكاوى من أن التطبيق كان "طويلاً". لكننا لم نتمكن من تكرار هذا الوضع بثبات.

ما هو طول ما هي المدة؟ وفق وثائق جوجل، إذا كانت البداية الباردة للتطبيق تستغرق أقل من 5 ثوانٍ ، فسيتم اعتبارها "كما لو كانت عادية". تم إطلاق تطبيق Dodo Pizza Android (وفقًا لمقياس Firebase _app_start) في بداية باردة بمعدل 3 ثوان - "ليس رائعًا ، ليس فظيعًا" ، كما يقولون.

ولكن بعد ذلك بدأت الشكاوى تظهر أن التطبيق يبدأ لفترة طويلة جدًا جدًا جدًا! بادئ ذي بدء ، قررنا قياس ما هو "طويل جدًا جدًا جدًا". واستخدمنا تتبع Firebase لهذا الغرض تتبع بدء التطبيق.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

يقيس هذا التتبع القياسي الوقت بين لحظة فتح المستخدم للتطبيق ولحظة تنفيذ onResume () للنشاط الأول. في Firebase Console ، يُطلق على هذا المقياس _app_start. اتضح أن:

  • أوقات بدء التشغيل للمستخدمين فوق الشريحة المئوية 95 هي تقريبًا 20 ثانية (البعض أكثر) ، على الرغم من متوسط ​​وقت بدء التشغيل البارد الذي يقل عن 5 ثوانٍ.
  • وقت الإطلاق ليس قيمة ثابتة ، ولكنه ينمو بمرور الوقت. لكن في بعض الأحيان هناك شلالات. وجدنا هذا النمط عندما قمنا بزيادة مقياس التحليل إلى 90 يومًا.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

خطرت فكرتان إلى الذهن:

  1. شيء ما يتسرب.
  2. يتم التخلص من هذا "الشيء" بعد التحرير ثم يتسرب مرة أخرى.

اعتقدنا ، "ربما شيء ما مع قاعدة البيانات" ، وكنا على حق. أولاً ، نستخدم قاعدة البيانات كذاكرة تخزين مؤقت ، نقوم بمسحها أثناء الترحيل. ثانيًا ، يتم تحميل قاعدة البيانات عند بدء تشغيل التطبيق. كل شيء يتقارب.

ما الخطأ في قاعدة بيانات Realm

بدأنا في التحقق من كيفية تغير محتوى قاعدة البيانات على مدار عمر التطبيق ، من التثبيت الأول وما بعده في عملية الاستخدام النشط. يمكنك عرض محتويات قاعدة بيانات Realm من خلال ستيثو أو بمزيد من التفصيل والوضوح من خلال فتح الملف عبر استوديو Realm. لعرض محتويات قاعدة البيانات عبر ADB ، انسخ ملف قاعدة بيانات Realm:

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

بالنظر إلى محتويات قاعدة البيانات في أوقات مختلفة ، اكتشفنا أن عدد الكائنات من نوع معين يتزايد باستمرار.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل
تُظهر الصورة جزءًا من Realm Studio لملفين: على اليسار - قاعدة التطبيق بعض الوقت بعد التثبيت ، على اليمين - بعد الاستخدام النشط. يمكن ملاحظة أن عدد الأشياء ImageEntity и MoneyType زيادة كبيرة (تُظهر لقطة الشاشة عدد الكائنات من كل نوع).

ربط نمو قاعدة البيانات بوقت بدء التشغيل

نمو قاعدة البيانات غير المنضبط سيء للغاية. ولكن كيف يؤثر ذلك على وقت بدء تشغيل التطبيق؟ إن قياس هذا الأمر بسيط للغاية من خلال ActivityManager. بدءًا من Android 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 الوقت ، يمكنك أتمتة جمع هذا المقياس وإلقاء نظرة على النتائج بصريًا. يوضح الرسم البياني أدناه اعتماد وقت تشغيل التطبيق على عدد عمليات الإطلاق الباردة للتطبيق.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

في الوقت نفسه ، كان الاعتماد على حجم ونمو قاعدة البيانات هو نفسه ، حيث نما من 4 ميجا بايت إلى 15 ميجا بايت. في المجموع ، اتضح أنه بمرور الوقت (مع نمو عمليات الإطلاق الباردة) ، نما كل من وقت إطلاق التطبيق وحجم قاعدة البيانات. لدينا فرضية. الآن كان من الضروري تأكيد الاعتماد. لذلك ، قررنا إزالة "التسريبات" ومعرفة ما إذا كان هذا يسرع عملية الإطلاق.

أسباب نمو قاعدة البيانات اللانهائي

قبل إزالة "التسريبات" ، من المفيد معرفة سبب ظهورها على الإطلاق. للقيام بذلك ، دعونا نتذكر ما هو عالم.

Realm هي قاعدة بيانات غير علائقية. يتيح لك وصف العلاقات بين الكائنات بطريقة مماثلة تصفها العديد من قواعد البيانات العلائقية ORM على Android. في نفس الوقت ، يقوم Realm بتخزين الكائنات مباشرة في الذاكرة بأقل عدد من التحويلات والتعيينات. يسمح لك هذا بقراءة البيانات من القرص بسرعة كبيرة ، وهو ما يمثل نقطة قوة Realm التي يحبها.

(لأغراض هذه المقالة ، سيكون هذا الوصف كافياً بالنسبة لنا. يمكنك قراءة المزيد عن Realm in the cool توثيق أو في الأكاديمية).

كثير من المطورين معتادون على العمل أكثر مع قواعد البيانات العلائقية (مثل قواعد بيانات 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 ()) بنفس المعرف ، فسيتم استبدال هذا الكائن بالكامل. ولكن ستفقد جميع الكائنات الداخلية (image ، و 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()
}
// и потом уже сохраняем

إذا قمت بذلك ، فسيعمل كل شيء كما ينبغي. في هذا المثال ، نفترض أنه لا توجد كائنات أخرى متداخلة من Realm داخل الصورة و customizationEntity و cartComboProducts ، لذلك لا توجد حلقات متداخلة وحذف أخرى.

حل "الإصلاح السريع"

بادئ ذي بدء ، قررنا تنظيف الأشياء الأسرع نموًا والتحقق من النتائج - ما إذا كان هذا يحل مشكلتنا الأصلية. أولاً ، تم وضع الحل الأكثر بساطة وبديهية ، وهو: يجب أن يكون كل كائن مسؤولاً عن حذف أبنائه. للقيام بذلك ، قدمنا ​​واجهة عرضت قائمة بكائنات Realm المتداخلة:

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

ونفذته في كائنات عالمنا:

@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 بشكل نشط للغاية. لذلك ، فإن كتابة جميع الكائنات المتداخلة لكل كائن أمر شاق ، بالإضافة إلى زيادة مخاطر الخطأ ، لأنه يمكنك نسيان تحديد الكائنات عند تغيير الكود.

أردت التأكد من أنني لا أستخدم الواجهات ، ولكن كل شيء يعمل من تلقاء نفسه.

عندما نريد أن يعمل شيء ما بمفرده ، علينا استخدام التفكير. للقيام بذلك ، يمكننا إجراء حلقة عبر كل حقل في الفصل والتحقق مما إذا كان كائنًا من 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، لأنه يمكن أن يكون للكائنات الرئيسية المختلفة كائنات متطابقة متداخلة. من الأفضل عدم السماح بذلك واستخدام التوليد التلقائي للمعرف عند إنشاء كائنات جديدة.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

التنفيذ الكامل لطريقة 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.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

للتحليل ، نأخذ فترة زمنية مدتها 90 يومًا ونرى: بدأ وقت تشغيل التطبيق ، سواء الوسيط أو الذي يقع على 95 بالمائة من المستخدمين ، في الانخفاض ولم يعد يرتفع.

حكاية كيف فاز الحذف المتتالي في إطلاق Realm الطويل

إذا نظرت إلى الرسم البياني ذي السبعة أيام ، فإن مقياس _app_start مناسب تمامًا وأقل من ثانية واحدة.

بشكل منفصل ، يجدر إضافة أنه ، افتراضيًا ، يرسل Firebase إشعارات إذا تجاوزت القيمة المتوسطة لـ _app_start 5 ثوانٍ. ومع ذلك ، كما نرى ، لا يجب الاعتماد على هذا ، ولكن من الأفضل الدخول والتحقق من ذلك بشكل صريح.

تعتبر قاعدة بيانات Realm خاصة لأنها قاعدة بيانات غير علائقية. على الرغم من استخدامه البسيط والتشابه مع حلول ORM وربط الكائنات ، إلا أنه لا يحتوي على حذف متتالي.

إذا لم يتم أخذ ذلك في الاعتبار ، فسوف تتراكم الكائنات المتداخلة ، "تسرب". ستنمو قاعدة البيانات باستمرار ، مما سيؤثر بدوره على التباطؤ أو بدء تشغيل التطبيق.

لقد شاركت تجربتنا حول كيفية القيام بسرعة بحذف متتالي للكائنات في المملكة ، والتي لم يتم إخراجها من الصندوق بعد ، ولكن حولها لفترة طويلة يقولون и يقولون. في حالتنا ، أدى هذا إلى تسريع وقت تشغيل التطبيق بشكل كبير.

على الرغم من مناقشة المظهر الوشيك لهذه الميزة ، إلا أن عدم وجود حذف متتالي في المملكة يرجع إلى التصميم. إذا كنت تصمم تطبيقًا جديدًا ، فضع ذلك في الاعتبار. وإذا كنت تستخدم Realm بالفعل ، فتحقق مما إذا كانت لديك مثل هذه المشكلات.

المصدر: www.habr.com

إضافة تعليق