Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Όλοι οι χρήστες θεωρούν δεδομένη τη γρήγορη εκκίνηση και την απόκριση διεπαφής χρήστη σε εφαρμογές για κινητά. Εάν η εφαρμογή αργεί να ξεκινήσει, ο χρήστης αρχίζει να νιώθει θλίψη και θυμό. Μπορείτε εύκολα να χαλάσετε την εμπειρία του πελάτη ή να χάσετε εντελώς τον χρήστη ακόμα και πριν αρχίσει να χρησιμοποιεί την εφαρμογή.

Κάποτε ανακαλύψαμε ότι η εφαρμογή Dodo Pizza χρειάζεται 3 δευτερόλεπτα για να ξεκινήσει κατά μέσο όρο και για ορισμένους «τυχερούς» χρειάζονται 15-20 δευτερόλεπτα.

Κάτω από την περικοπή υπάρχει μια ιστορία με αίσιο τέλος: για την ανάπτυξη της βάσης δεδομένων του Realm, μια διαρροή μνήμης, πώς συσσωρεύσαμε ένθετα αντικείμενα και στη συνέχεια συγκεντρωθήκαμε και διορθώσαμε τα πάντα.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm
Συντάκτης άρθρου: Μαξίμ Κατσίνκιν — Προγραμματιστής Android στο Dodo Pizza.

Τρία δευτερόλεπτα από το κλικ στο εικονίδιο της εφαρμογής έως το onResume() της πρώτης δραστηριότητας είναι άπειρα. Και για ορισμένους χρήστες, ο χρόνος εκκίνησης έφτασε τα 15-20 δευτερόλεπτα. Πώς είναι ακόμη δυνατό αυτό;

Μια πολύ σύντομη περίληψη για όσους δεν έχουν χρόνο να διαβάσουν
Η βάση δεδομένων μας Realm μεγάλωσε ασταμάτητα. Ορισμένα ένθετα αντικείμενα δεν διαγράφηκαν, αλλά συσσωρεύονταν συνεχώς. Ο χρόνος εκκίνησης της εφαρμογής αυξήθηκε σταδιακά. Στη συνέχεια, το διορθώσαμε και ο χρόνος εκκίνησης έφτασε στον στόχο - έγινε λιγότερο από 1 δευτερόλεπτο και δεν αυξήθηκε πλέον. Το άρθρο περιέχει μια ανάλυση της κατάστασης και δύο λύσεις - μια γρήγορη και μια κανονική.

Αναζήτηση και ανάλυση του προβλήματος

Σήμερα, οποιαδήποτε εφαρμογή για κινητά πρέπει να ξεκινήσει γρήγορα και να ανταποκρίνεται. Αλλά δεν αφορά μόνο την εφαρμογή για κινητά. Η εμπειρία χρήστη από την αλληλεπίδραση με μια υπηρεσία και μια εταιρεία είναι ένα περίπλοκο πράγμα. Για παράδειγμα, στην περίπτωσή μας, η ταχύτητα παράδοσης είναι ένας από τους βασικούς δείκτες για την εξυπηρέτηση πίτσας. Εάν η παράδοση είναι γρήγορη, η πίτσα θα είναι ζεστή και ο πελάτης που θέλει να φάει τώρα δεν θα χρειαστεί να περιμένει πολύ. Για την εφαρμογή, με τη σειρά της, είναι σημαντικό να δημιουργηθεί η αίσθηση της γρήγορης εξυπηρέτησης, γιατί αν η εφαρμογή διαρκεί μόνο 20 δευτερόλεπτα για να ξεκινήσει, τότε πόσο καιρό θα πρέπει να περιμένετε για την πίτσα;

Στην αρχή, ήμασταν αντιμέτωποι με το γεγονός ότι μερικές φορές η εφαρμογή χρειαζόταν μερικά δευτερόλεπτα για να ξεκινήσει και στη συνέχεια αρχίσαμε να ακούμε παράπονα από άλλους συναδέλφους σχετικά με το χρόνο που χρειαζόταν. Αλλά δεν μπορέσαμε να επαναλάβουμε με συνέπεια αυτή την κατάσταση.

Πόσο καιρό είναι; Σύμφωνα με τεκμηρίωση Google, εάν μια κρύα εκκίνηση μιας εφαρμογής διαρκεί λιγότερο από 5 δευτερόλεπτα, τότε αυτό θεωρείται «σαν φυσιολογικό». Κυκλοφόρησε η εφαρμογή Android Dodo Pizza (σύμφωνα με τις μετρήσεις του Firebase _app_start) στο κρύο ξεκίνημα κατά μέσο όρο σε 3 δευτερόλεπτα - "Όχι υπέροχο, όχι τρομερό", όπως λένε.

Στη συνέχεια, όμως, άρχισαν να εμφανίζονται παράπονα ότι η εφαρμογή χρειάστηκε πολύ, πολύ, πολύ χρόνο για να ξεκινήσει! Αρχικά, αποφασίσαμε να μετρήσουμε τι είναι το "πολύ, πολύ, πολύ μεγάλο". Και χρησιμοποιήσαμε το Firebase trace για αυτό Ίχνος έναρξης εφαρμογής.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Αυτό το τυπικό ίχνος μετρά το χρόνο μεταξύ της στιγμής που ο χρήστης ανοίγει την εφαρμογή και της στιγμής που εκτελείται η onResume() της πρώτης δραστηριότητας. Στο Firebase Console αυτή η μέτρηση ονομάζεται _app_start. Αποδείχθηκε ότι:

  • Οι χρόνοι εκκίνησης για χρήστες που υπερβαίνουν το 95ο εκατοστημόριο είναι σχεδόν 20 δευτερόλεπτα (μερικοί ακόμη μεγαλύτεροι), παρά το γεγονός ότι ο διάμεσος χρόνος κρύας εκκίνησης είναι μικρότερος από 5 δευτερόλεπτα.
  • Ο χρόνος εκκίνησης δεν είναι σταθερή τιμή, αλλά αυξάνεται με την πάροδο του χρόνου. Αλλά μερικές φορές υπάρχουν σταγόνες. Βρήκαμε αυτό το μοτίβο όταν αυξήσαμε την κλίμακα ανάλυσης σε 90 ημέρες.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Δύο σκέψεις μου ήρθαν στο μυαλό:

  1. Κάτι διαρρέει.
  2. Αυτό το "κάτι" επαναφέρεται μετά την απελευθέρωση και στη συνέχεια διαρρέει ξανά.

«Μάλλον κάτι με τη βάση δεδομένων», σκεφτήκαμε και είχαμε δίκιο. Πρώτον, χρησιμοποιούμε τη βάση δεδομένων ως κρυφή μνήμη· κατά τη μετεγκατάσταση τη διαγράφουμε. Δεύτερον, η βάση δεδομένων φορτώνεται όταν ξεκινά η εφαρμογή. Όλα ταιριάζουν μεταξύ τους.

Τι συμβαίνει με τη βάση δεδομένων Realm

Αρχίσαμε να ελέγχουμε πώς αλλάζουν τα περιεχόμενα της βάσης δεδομένων κατά τη διάρκεια ζωής της εφαρμογής, από την πρώτη εγκατάσταση και περαιτέρω κατά την ενεργό χρήση. Μπορείτε να δείτε τα περιεχόμενα της βάσης δεδομένων Realm μέσω Στέθο ή πιο αναλυτικά και σαφώς ανοίγοντας το αρχείο μέσω Realm Studio. Για να προβάλετε τα περιεχόμενα της βάσης δεδομένων μέσω 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 MB σε 15 MB. Συνολικά, αποδεικνύεται ότι με την πάροδο του χρόνου (με την αύξηση των ψυχρών εκκινήσεων), τόσο ο χρόνος εκκίνησης της εφαρμογής όσο και το μέγεθος της βάσης δεδομένων αυξήθηκαν. Έχουμε μια υπόθεση στα χέρια μας. Τώρα το μόνο που έμενε ήταν να επιβεβαιωθεί η εξάρτηση. Ως εκ τούτου, αποφασίσαμε να αφαιρέσουμε τις «διαρροές» και να δούμε αν αυτό θα επιτάχυνε την εκτόξευση.

Λόγοι για ατελείωτη ανάπτυξη βάσεων δεδομένων

Πριν αφαιρέσετε τις "διαρροές", αξίζει να καταλάβετε γιατί εμφανίστηκαν στην πρώτη θέση. Για να το κάνουμε αυτό, ας θυμηθούμε τι είναι το Realm.

Το 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()) με το ίδιο id, τότε αυτό το αντικείμενο θα αντικατασταθεί πλήρως. Αλλά όλα τα εσωτερικά αντικείμενα (εικόνα, προσαρμογήΕνότητα και 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

Η πράσινη γραμμή δείχνει την εξάρτηση του χρόνου εκκίνησης της εφαρμογής από τον αριθμό των ψυχρών εκκινήσεων κατά τη διάρκεια της αυτόματης καταρράκτης διαγραφής ένθετων αντικειμένων.

Αποτελέσματα και συμπεράσματα

Η συνεχώς αναπτυσσόμενη βάση δεδομένων Realm έκανε την εφαρμογή να ξεκινήσει πολύ αργά. Κυκλοφόρησε μια ενημέρωση με τη δική μας "διαδοχική διαγραφή" ένθετων αντικειμένων. Και τώρα παρακολουθούμε και αξιολογούμε πώς η απόφασή μας επηρέασε τον χρόνο εκκίνησης της εφαρμογής μέσω της μέτρησης _app_start.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Για ανάλυση, λαμβάνουμε μια χρονική περίοδο 90 ημερών και βλέπουμε: ο χρόνος εκκίνησης της εφαρμογής, τόσο ο διάμεσος όσο και αυτός που πέφτει στο 95ο εκατοστημόριο των χρηστών, άρχισε να μειώνεται και δεν αυξάνεται πλέον.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του Realm

Αν κοιτάξετε το γράφημα επτά ημερών, η μέτρηση _app_start φαίνεται απολύτως επαρκής και είναι λιγότερο από 1 δευτερόλεπτο.

Αξίζει επίσης να προσθέσουμε ότι από προεπιλογή, το Firebase στέλνει ειδοποιήσεις εάν η διάμεση τιμή του _app_start υπερβαίνει τα 5 δευτερόλεπτα. Ωστόσο, όπως μπορούμε να δούμε, δεν πρέπει να βασιστείτε σε αυτό, αλλά μάλλον να μπείτε και να το ελέγξετε ρητά.

Το ιδιαίτερο με τη βάση δεδομένων Realm είναι ότι είναι μια μη σχεσιακή βάση δεδομένων. Παρά την ευκολία χρήσης, την ομοιότητα με τις λύσεις ORM και τη σύνδεση αντικειμένων, δεν έχει διαγραφή καταρράκτη.

Εάν αυτό δεν ληφθεί υπόψη, τότε τα ένθετα αντικείμενα θα συσσωρευτούν και θα «διαρρεύσουν». Η βάση δεδομένων θα μεγαλώνει συνεχώς, κάτι που με τη σειρά του θα επηρεάσει την επιβράδυνση ή την εκκίνηση της εφαρμογής.

Μοιράστηκα την εμπειρία μας σχετικά με το πώς να κάνουμε γρήγορα μια καταρράκτη διαγραφή αντικειμένων στο Realm, για το οποίο δεν έχει βγει ακόμα, αλλά έχει συζητηθεί εδώ και πολύ καιρό λένε и λένε. Στην περίπτωσή μας, αυτό επιτάχυνε πολύ τον χρόνο εκκίνησης της εφαρμογής.

Παρά τη συζήτηση για την επικείμενη εμφάνιση αυτού του χαρακτηριστικού, η απουσία διαγραφής καταρράκτη στο Realm γίνεται από το σχεδιασμό. Εάν σχεδιάζετε μια νέα εφαρμογή, τότε λάβετε αυτό υπόψη. Και αν χρησιμοποιείτε ήδη το Realm, ελέγξτε αν έχετε τέτοια προβλήματα.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο