Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Sadaya pangguna nyandak peluncuran gancang sareng UI responsif dina aplikasi mobile. Upami aplikasina lami lami diluncurkeun, pangguna mimiti ngarasa sedih sareng ambek. Anjeun tiasa sacara gampil ngarusak pangalaman palanggan atanapi kaleungitan lengkep pangguna bahkan sateuacan anjeunna ngamimitian nganggo aplikasi.

Urang sakali manggihan yén aplikasi Dodo Pizza nyokot 3 detik pikeun ngajalankeun rata-rata, jeung sababaraha "nu untung" butuh 15-20 detik.

Di handap potongan aya carita kalayan tungtung senang: ngeunaan tumuwuhna database Realm, bocor memori, kumaha urang akumulasi objék nested, lajeng ditarik sorangan babarengan jeung ngalereskeun sagalana.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang
Panulis artikel: Maxim Kachinkin — pamekar Android di Dodo Pizza.

Tilu detik ti ngaklik dina ikon aplikasi pikeun onResume () tina aktivitas kahiji nyaeta takterhingga. Sareng pikeun sababaraha pangguna, waktos ngamimitian ngahontal 15-20 detik. Kumaha ieu malah mungkin?

Kasimpulan anu pondok pisan pikeun anu henteu gaduh waktos maca
Pangkalan data Realm kami tumbuh tanpa henti. Sababaraha objék nested teu dihapus, tapi terus akumulasi. Waktu ngamimitian aplikasi laun-laun ningkat. Teras we ngalereskeunana, sareng waktos ngamimitian dugi ka udagan - janten kirang ti 1 detik sareng henteu deui ningkat. Tulisan ngandung analisa kaayaan sareng dua solusi - anu gancang sareng anu normal.

Pilarian jeung analisis masalah

Kiwari, naon waé aplikasi sélulér kedah diluncurkeun gancang sareng responsif. Tapi éta sanés ngan ukur ngeunaan aplikasi sélulér. Pangalaman pangguna interaksi sareng jasa sareng perusahaan mangrupikeun hal anu rumit. Salaku conto, dina kasus urang, laju pangiriman mangrupikeun salah sahiji indikator konci pikeun layanan pizza. Upami pangiriman gancang, pizza bakal panas, sareng palanggan anu hoyong tuang ayeuna henteu kedah ngantosan lami. Pikeun aplikasi, kahareupna penting pikeun nyiptakeun rasa jasa gancang, sabab upami aplikasi ngan ukur 20 detik pikeun diluncurkeun, teras sabaraha lami anjeun kedah ngantosan pizza?

Mimitina, urang sorangan nyanghareupan kanyataan yén sakapeung aplikasi nyandak sababaraha detik pikeun diluncurkeun, teras urang mimiti nguping keluhan ti kolega anu sanés ngeunaan sabaraha lami waktosna. Tapi kami henteu tiasa konsistén ngulang kaayaan ieu.

Sabaraha lami? Numutkeun kana dokuméntasi Google, lamun mimiti tiis tina hiji aplikasi nyokot kirang ti 5 detik, lajeng ieu dianggap "saolah-olah normal". Aplikasi Android Dodo Pizza diluncurkeun (nurutkeun métrik Firebase _app_start) di mimiti tiis rata-rata dina 3 detik - "Teu hébat, teu dahsyat," sabab nyebutkeun.

Tapi teras keluhan mimiti muncul yén aplikasi nyandak waktos anu lami pisan pikeun diluncurkeun! Pikeun dimimitian ku, urang mutuskeun pikeun ngukur naon "pisan, pisan, lila pisan". Sareng kami nganggo jejak Firebase pikeun ieu Lacak ngamimitian aplikasi.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

ngambah standar ieu ngukur waktu antara momen pamaké muka aplikasi jeung momen onResume () tina aktivitas munggaran dieksekusi. Dina Firebase Console métrik ieu disebut _app_start. Tétéla éta:

  • Waktu ngamimitian pikeun pangguna di luhur persentil ka-95 ampir 20 detik (sababaraha malah langkung lami), sanaos waktos ngamimitian tiis median kirang ti 5 detik.
  • Waktu ngamimitian henteu nilai konstan, tapi tumuwuh kana waktu. Tapi sakapeung aya tetes. Kami mendakan pola ieu nalika urang ningkatkeun skala analisis ka 90 dinten.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Dua pikiran datang ka pikiran:

  1. Aya nu bocor.
  2. Ieu "hal" reset sanggeus release lajeng bocor deui.

"Panginten aya anu nganggo pangkalan data," saur kami, sareng kami leres. Anu mimiti, kami nganggo pangkalan data salaku cache; salami migrasi kami mupus éta. Kadua, pangkalan data dimuat nalika aplikasi dimimitian. Sagalana cocog babarengan.

Naon salahna database Realm

Urang mimitian mariksa kumaha eusi database robah leuwih hirup aplikasi, ti instalasi munggaran tur salajengna salila pamakéan aktip. Anjeun tiasa ningali eusi database Realm via stetho atawa leuwih jéntré tur jelas ku cara muka file via Realm Studio. Pikeun ningali eusi pangkalan data ngalangkungan ADB, salin file database Realm:

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

Saatos ningali eusi database dina waktos anu béda, urang mendakan yén jumlah objék tina jinis anu tangtu terus ningkat.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang
Gambar nunjukkeun sempalan Realm Studio pikeun dua file: di kénca - dasar aplikasi sababaraha waktos saatos instalasi, di katuhu - saatos dianggo aktip. Ieu bisa ditempo yén jumlah objék ImageEntity и MoneyType geus tumuwuh sacara signifikan (screenshot nembongkeun jumlah objék unggal jenis).

Hubungan antara pertumbuhan database sareng waktos ngamimitian

pertumbuhan database uncontrolled pisan goréng. Tapi kumaha ieu mangaruhan waktos ngamimitian aplikasi? Ieu rada gampang pikeun ngukur ieu ngaliwatan ActivityManager. Kusabab Android 4.4, logcat nampilkeun log kalayan senar Ditampilkeun sareng waktosna. Waktos ieu sami sareng interval ti mimiti aplikasi diluncurkeun dugi ka ahir kagiatan rendering. Salila ieu kajadian di handap ieu lumangsung:

  • Mimitian prosés.
  • Initialization objék.
  • Nyiptakeun sareng inisialisasi kagiatan.
  • Nyieun perenah.
  • Rendering aplikasi.

Cocog jeung urang. Upami anjeun ngajalankeun ADB nganggo bandéra -S sareng -W, anjeun tiasa kéngingkeun kaluaran anu diperpanjang sareng waktos ngamimitian:

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

Lamun grab ti dinya grep -i WaitTime waktos, anjeun tiasa ngajadikeun otomatis kumpulan métrik ieu sareng visually katingal dina hasil. Grafik di handap ieu nunjukkeun gumantungna waktos ngamimitian aplikasi dina jumlah mimiti tiis tina aplikasi.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Dina waktos anu sami, aya hubungan anu sami antara ukuran sareng kamekaran database, anu ningkat tina 4 MB dugi ka 15 MB. Dina total, tétéla yén kana waktu (kalayan tumuwuhna tiis mimiti), duanana waktu peluncuran aplikasi jeung ukuran database ngaronjat. Kami gaduh hipotésis dina panangan urang. Ayeuna sadayana anu tetep nyaéta pikeun ngonfirmasi gumantungna. Ku alatan éta, urang mutuskeun pikeun miceun "bocor" tur tingal lamun ieu bakal nyepetkeun peluncuran.

Alesan pikeun pertumbuhan database sajajalan

Sateuacan miceun "bocor", kedah ngartos naha aranjeunna mimiti muncul. Jang ngalampahkeun ieu, hayu urang apal naon Realm.

Realm mangrupikeun database non-relasional. Eta ngidinan Anjeun pikeun ngajelaskeun hubungan antara objék dina cara nu sarupa jeung sabaraha ORM database relational on Android digambarkeun. Dina waktos anu sami, Realm nyimpen objék langsung dina mémori kalayan pangsaeutikna transformasi sareng pemetaan. Ieu ngamungkinkeun anjeun gancang maca data tina disk, anu mangrupikeun kakuatan Realm sareng naha éta dipikacinta.

(Pikeun kaperluan artikel ieu, pedaran ieu bakal cukup keur urang. Anjeun bisa maca leuwih lengkep tentang Realm dina tiis dokuméntasi atawa di maranéhna akademi).

Seueur pamekar anu biasa damel langkung seueur sareng database relasional (contona, database ORM sareng SQL handapeun tiung). Jeung hal kawas cascading ngahapus data mindeng sigana kawas dibikeun. Tapi henteu di Realm.

Ku jalan kitu, fitur ngahapus cascade parantos lami ditaroskeun. Ieu révisi и nu sejen, pakait sareng eta, aktip dibahas. Aya rarasaan yen eta bakal pas rengse. Tapi lajeng sagalana ditarjamahkeun kana bubuka Tumbu kuat sarta lemah, nu ogé bakal otomatis ngajawab masalah ieu. Éta cukup lincah sareng aktip dina tugas ieu pamundut tarik, nu geus direureuhkeun pikeun ayeuna alatan kasusah internal.

Bocor data tanpa ngahapus cascading

Kumaha persisna data bocor upami anjeun ngandelkeun penghapusan cascading anu teu aya? Lamun anjeun geus nested objék Realm, mangka maranéhanana kudu dihapus.
Hayu urang nempo hiji (ampir) conto nyata. Urang boga obyék 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 dina karanjang gaduh widang anu béda, kalebet gambar ImageEntity, bahan ngaropéa CustomizationEntity. Ogé, produk dina karanjang tiasa janten combo sareng set produk sorangan RealmList (CartProductEntity). Sadaya widang anu didaptarkeun mangrupikeun objék Realm. Lamun urang nyelapkeun objék anyar (copyToRealm () / copyToRealmOrUpdate ()) jeung id sarua, objek ieu bakal ditumpes overwritten. Tapi sakabeh objék internal (gambar, customizationEntity na cartComboProducts) bakal leungit sambungan jeung indungna sarta tetep dina database.

Kusabab sambungan sareng aranjeunna leungit, kami henteu deui maca atanapi ngahapus aranjeunna (kecuali kami sacara eksplisit ngaksés aranjeunna atanapi mupus sadayana "tabel"). Urang disebut ieu "bocor memori".

Nalika urang damel sareng Realm, urang kedah sacara eksplisit ngalangkungan sadaya unsur sareng sacara eksplisit ngahapus sadayana sateuacan operasi sapertos kitu. Ieu tiasa dilakukeun, contona, sapertos kieu:

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

Upami anjeun ngalakukeun ieu, maka sadayana bakal jalan sakumaha sakuduna. Dina conto ieu, urang nganggap yén teu aya objék Realm nested séjén di jero gambar, customizationEntity, sarta cartComboProducts, jadi euweuh puteran nested sejen tur ngahapus.

"Gancang" solusi

Hal kahiji urang mutuskeun pikeun ngalakukeun éta ngabersihan up objék tumuwuh panggancangna tur pariksa hasil pikeun nempo naha ieu bakal ngajawab masalah aslina urang. Kahiji, leyuran pangbasajanna tur paling intuitif dijieun, nyaéta: unggal obyék kudu tanggung jawab nyoplokkeun barudak na. Jang ngalampahkeun ieu, kami ngenalkeun antarbeungeut anu ngabalikeun daptar objék Realm anu disarangkeun:

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

Sareng kami ngalaksanakeunana dina objék 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 urang balik kabeh barudak salaku daptar datar. Sareng unggal obyék anak ogé tiasa nerapkeun antarbeungeut NestedEntityAware, nunjukkeun yén éta ngagaduhan objék Realm internal pikeun dipupus, contona. 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
   )
 }
}

Jeung saterusna, nyarang objék bisa diulang.

Teras we nyerat metodeu anu sacara rekursif ngahapus sadaya objék anu disarangkeun. Métode (dijieun salaku extension) deleteAllNestedEntities nampi sadaya objék sareng metode tingkat luhur deleteNestedRecursively Recursively miceun kabeh objék nested maké panganteur 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()
   }
 }
}

Urang ngalakukeun ieu jeung objék tumuwuh panggancangna tur dipariksa naon anu lumangsung.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Hasilna, obyék anu kami tutup ku solusi ieu lirén ngembang. Jeung tumuwuhna sakabéh basa slowed, tapi teu eureun.

Solusi "normal".

Sanajan dasarna mimiti tumuwuh leuwih laun, eta tetep tumuwuh. Ku kituna urang mimitian néangan salajengna. Proyék kami ngagunakeun cache data anu aktip pisan dina Realm. Ku alatan éta, nulis sakabéh objék nested pikeun tiap obyék nyaéta kuli-intensif, ditambah résiko kasalahan naek, sabab bisa poho pikeun nangtukeun objék lamun ngarobah kodeu.

Kuring hayang mastikeun yén kuring teu make interfaces, tapi sagalana digawé sorangan.

Nalika urang hoyong hiji hal dianggo nyalira, urang kedah nganggo refleksi. Jang ngalampahkeun ieu, urang tiasa ngaliwat unggal widang kelas sareng pariksa naha éta mangrupikeun objék Realm atanapi daptar objék:

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

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

Upami lapangan mangrupikeun RealmModel atanapi RealmList, teras tambahkeun obyék lapangan ieu kana daptar objék anu disarangkeun. Sadayana sami sareng anu urang lakukeun di luhur, ngan di dieu bakal dilakukeun ku nyalira. Metodeu ngahapus cascade sorangan saderhana pisan sareng sapertos kieu:

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

Ékstensi filterRealmObject nyaring kaluar sarta ngalirkeun ukur objék Realm. Métode getNestedRealmObjects ngaliwatan cerminan, eta manggihan sakabeh objék Realm nested sarta nempatkeun kana daptar linier. Teras we ngalakukeun hal anu sami recursively. Nalika ngahapus, anjeun kedah pariksa obyék pikeun validitas isValid, sabab bisa jadi yén objék indungna béda bisa boga nested leuwih idéntik. Éta langkung saé pikeun ngahindarkeun ieu sareng ngan ukur nganggo generasi otomatis id nalika nyiptakeun objék énggal.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Palaksanaan pinuh ku metoda 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)
}

Hasilna, dina kode klien kami kami nganggo "cascading delete" pikeun tiap operasi modifikasi data. Salaku conto, pikeun operasi sisipan sigana kieu:

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

Métode munggaran getManagedEntities narima sagala objék ditambahkeun, lajeng metoda cascadeDelete Rekursif mupus sadaya objék anu dikumpulkeun sateuacan nyerat anu énggal. Urang mungkas ngagunakeun pendekatan ieu sapanjang aplikasi. Bocor memori di Realm parantos musna. Saatos ngalaksanakeun pangukuran anu sami sareng gumantungna waktos ngamimitian dina jumlah ngamimitian aplikasi, urang ningali hasilna.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Garis héjo nembongkeun gumantungna waktu ngamimitian aplikasi dina jumlah tiis dimimitian salila cascade otomatis ngahapus objék nested.

Hasil jeung conclusions

Pangkalan data Realm anu terus-terusan nyababkeun aplikasina diluncurkeun lambat pisan. Kami ngarilis pembaruan nganggo "cascading delete" tina objék anu disarangkeun. Sareng ayeuna urang ngawas sareng meunteun kumaha kaputusan urang mangaruhan waktos ngamimitian aplikasi ngalangkungan métrik _app_start.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Pikeun analisa, urang nyandak periode waktos 90 dinten sareng ningali: waktos peluncuran aplikasi, boh median sareng anu aya dina persentil ka-95 pangguna, mimiti ngirangan sareng henteu naék deui.

Dongéng kumaha ngahapus kaskade dina peluncuran panjang Realm meunang

Upami anjeun ningali bagan tujuh dinten, métrik _app_start katingalina cekap sareng kirang ti 1 detik.

Éta ogé patut nambihan yén sacara standar, Firebase ngirim béwara upami nilai median _app_start ngaleuwihan 5 detik. Nanging, sakumaha anu urang tingali, anjeun henteu kedah ngandelkeun ieu, tapi langkung lebet sareng pariksa sacara eksplisit.

Hal husus ngeunaan database Realm téh nya éta database non-relasional. Sanajan betah pamakéan na, kasaruaan jeung solusi ORM jeung objék linking, teu boga ngahapus cascade.

Upami ieu henteu diperhatoskeun, maka objék anu sarang bakal ngumpulkeun sareng "bocor jauh". Pangkalan data bakal terus-terusan tumbuh, anu bakal mangaruhan kalambatan atanapi ngamimitian aplikasi.

Kuring ngabagikeun pangalaman urang ngeunaan cara gancang ngahapus cascade objék di Realm, anu henteu acan kaluar tina kotak, tapi parantos lami diobrolkeun. nyarios и nyarios. Dina kasus urang, ieu pisan nyepetkeun waktos ngamimitian aplikasi.

Sanaos diskusi ngeunaan penampilan caket tina fitur ieu, henteuna ngahapus cascade di Realm dilakukeun ku desain. Upami anjeun ngarancang aplikasi énggal, teras perhatikeun ieu. Sareng upami anjeun parantos nganggo Realm, pariksa naha anjeun ngagaduhan masalah sapertos kitu.

sumber: www.habr.com

Tambahkeun komentar