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

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

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

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

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

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

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

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

Εύρεση και ανάλυση του προβλήματος

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

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

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

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

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

Αυτή η τυπική ιχνηλάτηση μετρά τον χρόνο μεταξύ της στιγμής που ο χρήστης ανοίγει την εφαρμογή και της στιγμής που εκτελείται η συνάρτηση onResume() της πρώτης δραστηριότητας. Στην κονσόλα Firebase, αυτή η μέτρηση ονομάζεται _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 εμφανίζει ένα αρχείο καταγραφής με τη γραμμή Displayed και την ώρα. Αυτός ο χρόνος είναι ίσος με το διάστημα από τη στιγμή που ξεκινά η εφαρμογή μέχρι το τέλος της απόδοσης της δραστηριότητας. Κατά τη διάρκεια αυτού του χρόνου, συμβαίνουν τα ακόλουθα συμβάντα:

  • Έναρξη της διαδικασίας.
  • Αρχικοποίηση αντικειμένων.
  • Δημιουργία και αρχικοποίηση μιας δραστηριότητας.
  • Δημιουργία διάταξης.
  • Απόδοση εφαρμογής.

Μας βολεύει. Αν εκτελέσετε το 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 στο δροσερό τεκμηρίωση ή στο δικό τους την ακαδημία).

Πολλοί προγραμματιστές έχουν συνηθίσει να εργάζονται περισσότερο με σχεσιακές βάσεις δεδομένων (όπως βάσεις δεδομένων ORM με SQL στο παρασκήνιο). Και πράγματα όπως οι διαδοχικές διαγραφές συχνά φαίνονται δεδομένα. Αλλά όχι στο Realm.

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

Διαρροή δεδομένων χωρίς διαδοχική διαγραφή

Πώς ακριβώς διαρρέει δεδομένα εάν βασιζόμαστε σε μια ανύπαρκτη διαγραφή μέσω αλληλουχίας; Εάν έχετε ένθετα αντικείμενα 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, αυτό το αντικείμενο θα αντικατασταθεί πλήρως. Αλλά όλα τα εσωτερικά αντικείμενα (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 μέσα στα image, customizationEntity και cartComboProducts, επομένως δεν υπάρχουν άλλα ένθετα loops και διαγραφές.

Μια γρήγορη λύση

Το πρώτο πράγμα που κάναμε ήταν να καθαρίσουμε τα αντικείμενα που αναπτύσσονταν ταχύτερα και να ελέγξουμε τα αποτελέσματα - αν αυτό θα έλυνε το αρχικό μας πρόβλημα. Αρχικά, δόθηκε η απλούστερη και πιο διαισθητική λύση, δηλαδή: κάθε αντικείμενο θα πρέπει να είναι υπεύθυνο για τη διαγραφή των θυγατρικών του αντικειμένων. Για να το κάνουμε αυτό, εισαγάγαμε μια διεπαφή που επέστρεψε μια λίστα με τα ένθετα αντικείμενα 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. Επομένως, η εγγραφή όλων των ένθετων αντικειμένων για κάθε αντικείμενο είναι επίπονη, ενώ αυξάνεται και ο κίνδυνος σφάλματος, επειδή μπορεί να ξεχάσετε να καθορίσετε αντικείμενα κατά την αλλαγή του κώδικα.

Ήθελα να το κάνω έτσι ώστε να μην χρησιμοποιώ διεπαφές, αλλά όλα να λειτουργούν από μόνα τους.

Όταν θέλουμε κάτι να λειτουργήσει μόνο του, πρέπει να χρησιμοποιήσουμε την αναστοχασμό. Για να το κάνουμε αυτό, μπορούμε να εξετάσουμε κάθε πεδίο της κλάσης και να ελέγξουμε αν πρόκειται για αντικείμενο 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 μέσω αντανάκλασης βρίσκει όλα τα ένθετα αντικείμενα Realm και τα τοποθετεί σε μια γραμμική λίστα. Στη συνέχεια, κάνει το ίδιο αναδρομικά. Κατά τη διαγραφή, πρέπει να ελέγξετε την εγκυρότητα του αντικειμένου. isValid, επειδή είναι πιθανό διαφορετικά γονικά αντικείμενα να έχουν ενσωματωμένα πανομοιότυπα. Είναι καλύτερο να μην επιτρέπεται αυτό και να χρησιμοποιείται η αυτόματη δημιουργία id κατά τη δημιουργία νέων αντικειμένων.

Η ιστορία για το πώς κέρδισε η διαδοχική διαγραφή στη μακρά κυκλοφορία του 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