Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Semua pengguna mengambil pelancaran pantas dan UI responsif dalam aplikasi mudah alih begitu sahaja. Jika aplikasi mengambil masa yang lama untuk dilancarkan, pengguna mula berasa sedih dan marah. Anda boleh dengan mudah merosakkan pengalaman pelanggan atau kehilangan pengguna sepenuhnya walaupun sebelum dia mula menggunakan aplikasi.

Kami pernah mendapati bahawa aplikasi Dodo Pizza mengambil masa 3 saat untuk dilancarkan secara purata, dan untuk sesetengah "yang bertuah" ia mengambil masa 15-20 saat.

Di bawah potongan adalah cerita dengan pengakhiran yang menggembirakan: tentang pertumbuhan pangkalan data Realm, kebocoran memori, cara kami mengumpul objek bersarang, dan kemudian menarik diri kami bersama-sama dan membetulkan segala-galanya.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang
Penulis artikel: Maxim Kachinkin β€” Pembangun Android di Dodo Pizza.

Tiga saat dari mengklik pada ikon aplikasi ke onResume() aktiviti pertama adalah infiniti. Dan bagi sesetengah pengguna, masa permulaan mencapai 15-20 saat. Bagaimana ini boleh berlaku?

Ringkasan yang sangat singkat untuk mereka yang tidak mempunyai masa untuk membaca
Pangkalan data Realm kami berkembang tanpa henti. Beberapa objek bersarang tidak dipadamkan, tetapi sentiasa terkumpul. Masa permulaan aplikasi meningkat secara beransur-ansur. Kemudian kami membetulkannya, dan masa permulaan tiba kepada sasaran - ia menjadi kurang daripada 1 saat dan tidak lagi meningkat. Artikel itu mengandungi analisis situasi dan dua penyelesaian - yang cepat dan yang biasa.

Carian dan analisis masalah

Hari ini, mana-mana aplikasi mudah alih mesti dilancarkan dengan cepat dan responsif. Tetapi ia bukan hanya mengenai aplikasi mudah alih. Pengalaman pengguna berinteraksi dengan perkhidmatan dan syarikat adalah perkara yang kompleks. Sebagai contoh, dalam kes kami, kelajuan penghantaran adalah salah satu petunjuk utama untuk perkhidmatan pizza. Jika penghantaran cepat, pizza akan menjadi panas, dan pelanggan yang ingin makan sekarang tidak perlu menunggu lama. Untuk aplikasi pula, adalah penting untuk mewujudkan perasaan perkhidmatan yang pantas, kerana jika aplikasi hanya mengambil masa 20 saat untuk dilancarkan, maka berapa lama anda perlu menunggu piza?

Pada mulanya, kami sendiri berhadapan dengan hakikat bahawa kadangkala aplikasi mengambil masa beberapa saat untuk dilancarkan, dan kemudian kami mula mendengar aduan daripada rakan sekerja lain tentang tempoh masa yang diambil. Tetapi kami tidak dapat mengulangi situasi ini secara konsisten.

Berapa lama? mengikut dokumentasi Google, jika permulaan sejuk permohonan mengambil masa kurang daripada 5 saat, maka ini dianggap "seolah-olah biasa". Apl Android Dodo Pizza dilancarkan (mengikut metrik Firebase _app_start) pada permulaan yang sejuk secara purata dalam 3 saat - "Tidak hebat, tidak mengerikan," seperti yang mereka katakan.

Tetapi kemudian aduan mula muncul bahawa aplikasi mengambil masa yang sangat, sangat, sangat lama untuk dilancarkan! Sebagai permulaan, kami memutuskan untuk mengukur apa itu "sangat, sangat, sangat panjang". Dan kami menggunakan jejak Firebase untuk ini Jejak permulaan apl.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Surih standard ini mengukur masa antara saat pengguna membuka aplikasi dan saat onResume() aktiviti pertama dilaksanakan. Dalam Firebase Console metrik ini dipanggil _app_start. Ternyata:

  • Masa permulaan untuk pengguna di atas persentil ke-95 adalah hampir 20 saat (ada yang lebih lama), walaupun masa permulaan sejuk median kurang daripada 5 saat.
  • Masa permulaan bukanlah nilai tetap, tetapi berkembang dari semasa ke semasa. Tetapi kadang-kadang terdapat titisan. Kami menemui corak ini apabila kami meningkatkan skala analisis kepada 90 hari.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Dua pemikiran terlintas di fikiran:

  1. Ada yang bocor.
  2. "Sesuatu" ini ditetapkan semula selepas dikeluarkan dan kemudian bocor lagi.

"Mungkin sesuatu dengan pangkalan data," kami fikir, dan kami betul. Pertama, kami menggunakan pangkalan data sebagai cache; semasa penghijrahan kami mengosongkannya. Kedua, pangkalan data dimuatkan apabila aplikasi dimulakan. Semuanya sesuai.

Apa yang salah dengan pangkalan data Realm

Kami mula menyemak bagaimana kandungan pangkalan data berubah sepanjang hayat aplikasi, dari pemasangan pertama dan seterusnya semasa penggunaan aktif. Anda boleh melihat kandungan pangkalan data Realm melalui stetho atau dengan lebih terperinci dan jelas dengan membuka fail melalui Studio Alam. Untuk melihat kandungan pangkalan data melalui ADB, salin fail pangkalan data Realm:

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

Setelah melihat kandungan pangkalan data pada masa yang berbeza, kami mendapati bahawa bilangan objek jenis tertentu sentiasa meningkat.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang
Gambar menunjukkan serpihan Realm Studio untuk dua fail: di sebelah kiri - pangkalan aplikasi beberapa lama selepas pemasangan, di sebelah kanan - selepas penggunaan aktif. Ia boleh dilihat bahawa bilangan objek ImageEntity ΠΈ MoneyType telah berkembang dengan ketara (tangkapan skrin menunjukkan bilangan objek bagi setiap jenis).

Hubungan antara pertumbuhan pangkalan data dan masa permulaan

Pertumbuhan pangkalan data yang tidak terkawal adalah sangat buruk. Tetapi bagaimanakah ini mempengaruhi masa permulaan aplikasi? Agak mudah untuk mengukur ini melalui ActivityManager. Sejak Android 4.4, logcat memaparkan log dengan rentetan Dipaparkan dan masa. Masa ini adalah sama dengan selang dari saat aplikasi dilancarkan sehingga tamat pemaparan aktiviti. Pada masa ini, peristiwa berikut berlaku:

  • Mulakan proses.
  • Inisialisasi objek.
  • Penciptaan dan permulaan aktiviti.
  • Membuat susun atur.
  • Penyampaian aplikasi.

Sesuai dengan kita. Jika anda menjalankan ADB dengan bendera -S dan -W, anda boleh mendapatkan output lanjutan dengan masa permulaan:

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

Jika anda ambil dari sana grep -i WaitTime masa, anda boleh mengautomasikan pengumpulan metrik ini dan melihat hasilnya secara visual. Graf di bawah menunjukkan pergantungan masa permulaan aplikasi pada bilangan permulaan sejuk aplikasi.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Pada masa yang sama, terdapat sifat hubungan yang sama antara saiz dan pertumbuhan pangkalan data, yang meningkat daripada 4 MB kepada 15 MB. Secara keseluruhan, ternyata dari masa ke masa (dengan pertumbuhan permulaan sejuk), kedua-dua masa pelancaran aplikasi dan saiz pangkalan data meningkat. Kami mempunyai hipotesis di tangan kami. Sekarang yang tinggal hanyalah untuk mengesahkan pergantungan. Oleh itu, kami memutuskan untuk mengalih keluar "kebocoran" dan melihat sama ada ini akan mempercepatkan pelancaran.

Sebab pertumbuhan pangkalan data yang tidak berkesudahan

Sebelum mengalih keluar "kebocoran", adalah wajar memahami mengapa ia muncul di tempat pertama. Untuk melakukan ini, mari kita ingat apa itu Realm.

Realm ialah pangkalan data bukan perhubungan. Ia membolehkan anda menerangkan perhubungan antara objek dengan cara yang serupa dengan bilangan pangkalan data hubungan ORM pada Android yang diterangkan. Pada masa yang sama, Realm menyimpan objek terus dalam ingatan dengan jumlah transformasi dan pemetaan paling sedikit. Ini membolehkan anda membaca data dari cakera dengan cepat, yang merupakan kekuatan Realm dan sebab ia disukai.

(Untuk tujuan artikel ini, penerangan ini akan mencukupi untuk kami. Anda boleh membaca lebih lanjut tentang Realm dalam keadaan sejuk dokumentasi atau dalam mereka Akademi).

Ramai pembangun terbiasa bekerja lebih dengan pangkalan data hubungan (contohnya, pangkalan data ORM dengan SQL di bawah hud). Dan perkara seperti pemadaman data berlatarkan sering kelihatan seperti diberikan. Tetapi tidak di Alam.

Ngomong-ngomong, ciri pemadaman lata telah diminta untuk masa yang lama. ini ulang kaji ΠΈ yang lain, yang berkaitan dengannya, telah dibincangkan secara aktif. Terdapat perasaan bahawa ia akan selesai tidak lama lagi. Tetapi kemudian semuanya diterjemahkan ke dalam pengenalan pautan yang kuat dan lemah, yang juga akan menyelesaikan masalah ini secara automatik. Agak meriah dan aktif dalam tugasan ini permintaan tarik, yang telah dijeda buat masa ini kerana masalah dalaman.

Kebocoran data tanpa pemadaman bertingkat

Bagaimanakah sebenarnya data bocor jika anda bergantung pada pemadaman lata yang tidak wujud? Jika anda mempunyai objek Realm bersarang, maka objek tersebut mesti dipadamkan.
Mari kita lihat contoh (hampir) nyata. Kami mempunyai objek 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()

Produk dalam troli mempunyai medan yang berbeza, termasuk gambar ImageEntity, bahan tersuai CustomizationEntity. Selain itu, produk dalam troli boleh menjadi kombo dengan set produknya sendiri RealmList (CartProductEntity). Semua medan yang disenaraikan ialah objek Realm. Jika kita memasukkan objek baharu (copyToRealm() / copyToRealmOrUpdate()) dengan id yang sama, maka objek ini akan ditimpa sepenuhnya. Tetapi semua objek dalaman (imej, customizationEntity dan cartComboProducts) akan kehilangan sambungan dengan induk dan kekal dalam pangkalan data.

Memandangkan sambungan dengan mereka terputus, kami tidak lagi membaca atau memadamnya (melainkan kami mengaksesnya secara eksplisit atau mengosongkan keseluruhan "jadual"). Kami memanggil ini "kebocoran memori".

Apabila kita bekerja dengan Realm, kita mesti melalui semua elemen secara eksplisit dan secara eksplisit memadamkan semuanya sebelum operasi sedemikian. Ini boleh dilakukan, sebagai contoh, seperti ini:

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()
}
// ΠΈ ΠΏΠΎΡ‚ΠΎΠΌ ΡƒΠΆΠ΅ сохраняСм

Jika anda melakukan ini, maka semuanya akan berfungsi sebagaimana mestinya. Dalam contoh ini, kami menganggap bahawa tiada objek Realm bersarang lain di dalam imej, customizationEntity dan cartComboProducts, jadi tiada gelung dan pemadaman bersarang lain.

Penyelesaian "pantas".

Perkara pertama yang kami putuskan untuk lakukan ialah membersihkan objek yang paling cepat berkembang dan semak keputusan untuk melihat sama ada ini akan menyelesaikan masalah asal kami. Pertama, penyelesaian yang paling mudah dan paling intuitif telah dibuat, iaitu: setiap objek harus bertanggungjawab untuk mengeluarkan anak-anaknya. Untuk melakukan ini, kami memperkenalkan antara muka yang mengembalikan senarai objek Realm bersarangnya:

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

Dan kami melaksanakannya dalam objek Realm kami:

@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 kami mengembalikan semua kanak-kanak sebagai senarai rata. Dan setiap objek anak juga boleh melaksanakan antara muka NestedEntityAware, menunjukkan bahawa ia mempunyai objek Realm dalaman untuk dipadamkan, contohnya 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
   )
 }
}

Dan seterusnya, sarang objek boleh diulang.

Kemudian kami menulis kaedah yang memadamkan semua objek bersarang secara rekursif. Kaedah (dibuat sebagai lanjutan) deleteAllNestedEntities mendapat semua objek dan kaedah peringkat atas deleteNestedRecursively Secara rekursif mengalih keluar semua objek bersarang menggunakan antara muka 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()
   }
 }
}

Kami melakukan ini dengan objek yang paling cepat berkembang dan menyemak apa yang berlaku.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Akibatnya, objek yang kami tutup dengan penyelesaian ini berhenti berkembang. Dan pertumbuhan keseluruhan asas perlahan, tetapi tidak berhenti.

Penyelesaian "biasa".

Walaupun pangkal mula berkembang dengan lebih perlahan, ia masih berkembang. Jadi kami mula mencari lebih jauh. Projek kami menggunakan caching data yang sangat aktif dalam Realm. Oleh itu, menulis semua objek bersarang untuk setiap objek adalah intensif buruh, ditambah dengan risiko ralat meningkat, kerana anda boleh lupa untuk menentukan objek apabila menukar kod.

Saya ingin memastikan bahawa saya tidak menggunakan antara muka, tetapi semuanya berfungsi dengan sendirinya.

Apabila kita mahu sesuatu berfungsi dengan sendirinya, kita perlu menggunakan refleksi. Untuk melakukan ini, kita boleh melalui setiap medan kelas dan menyemak sama ada ia adalah objek Realm atau senarai objek:

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

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

Jika medan itu ialah RealmModel atau RealmList, kemudian tambahkan objek medan ini pada senarai objek bersarang. Semuanya betul-betul sama seperti yang kami lakukan di atas, hanya di sini ia akan dilakukan dengan sendirinya. Kaedah pemadaman lata itu sendiri sangat mudah dan kelihatan seperti ini:

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

Sambungan filterRealmObject menapis keluar dan menghantar objek Realm sahaja. Kaedah getNestedRealmObjects melalui refleksi, ia mencari semua objek Realm bersarang dan meletakkannya ke dalam senarai linear. Kemudian kita melakukan perkara yang sama secara rekursif. Apabila memadam, anda perlu menyemak objek untuk kesahihan isValid, kerana mungkin objek induk yang berbeza boleh mempunyai objek yang serupa bersarang. Adalah lebih baik untuk mengelakkan ini dan hanya menggunakan penjanaan automatik id apabila mencipta objek baharu.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Pelaksanaan penuh kaedah 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)
}

Akibatnya, dalam kod pelanggan kami, kami menggunakan "pemadaman bertingkat" untuk setiap operasi pengubahsuaian data. Sebagai contoh, untuk operasi sisipan ia kelihatan seperti ini:

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

Kaedah dahulu getManagedEntities menerima semua objek tambahan, dan kemudian kaedah cascadeDelete Secara rekursif memadam semua objek yang dikumpul sebelum menulis yang baharu. Kami akhirnya menggunakan pendekatan ini sepanjang aplikasi. Kebocoran memori dalam Alam telah hilang sepenuhnya. Setelah melakukan pengukuran yang sama kebergantungan masa permulaan pada bilangan permulaan sejuk aplikasi, kami melihat hasilnya.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Garis hijau menunjukkan pergantungan masa permulaan aplikasi pada bilangan permulaan sejuk semasa pemadaman lata automatik objek bersarang.

Keputusan dan kesimpulan

Pangkalan data Realm yang sentiasa berkembang menyebabkan aplikasi dilancarkan dengan sangat perlahan. Kami mengeluarkan kemas kini dengan "pemadaman bertingkat" kami sendiri bagi objek bersarang. Dan kini kami memantau dan menilai cara keputusan kami mempengaruhi masa permulaan aplikasi melalui metrik _app_start.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Untuk analisis, kami mengambil tempoh masa selama 90 hari dan melihat: masa pelancaran aplikasi, kedua-dua median dan yang jatuh pada persentil ke-95 pengguna, mula berkurangan dan tidak lagi meningkat.

Kisah bagaimana pemadaman lata dalam Realm memenangi pelancaran yang panjang

Jika anda melihat carta tujuh hari, metrik _app_start kelihatan mencukupi sepenuhnya dan kurang daripada 1 saat.

Ia juga patut ditambah bahawa secara lalai, Firebase menghantar pemberitahuan jika nilai median _app_start melebihi 5 saat. Walau bagaimanapun, seperti yang kita dapat lihat, anda tidak seharusnya bergantung pada ini, tetapi pergi ke dalam dan menyemaknya dengan jelas.

Perkara istimewa tentang pangkalan data Realm ialah ia adalah pangkalan data bukan perhubungan. Walaupun kemudahan penggunaannya, persamaan dengan penyelesaian ORM dan pemautan objek, ia tidak mempunyai pemadaman lata.

Jika ini tidak diambil kira, objek bersarang akan terkumpul dan "bocor". Pangkalan data akan berkembang secara berterusan, yang seterusnya akan menjejaskan kelembapan atau permulaan aplikasi.

Saya berkongsi pengalaman kami tentang cara cepat melakukan pemadaman lata objek dalam Alam, yang masih belum keluar dari kotak, tetapi telah diperkatakan sejak sekian lama katakan ΠΈ katakan. Dalam kes kami, ini sangat mempercepatkan masa permulaan aplikasi.

Walaupun terdapat perbincangan tentang kemunculan yang akan berlaku bagi ciri ini, ketiadaan pemadaman lata dalam Realm dilakukan secara reka bentuk. Jika anda mereka bentuk aplikasi baharu, maka ambil kira perkara ini. Dan jika anda sudah menggunakan Realm, semak sama ada anda menghadapi masalah sedemikian.

Sumber: www.habr.com

Tambah komen