Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Kabeh pangguna njupuk Bukak cepet lan UI responsif ing aplikasi seluler kanggo diwenehake. Yen aplikasi butuh wektu suwe kanggo diluncurake, pangguna wiwit sedhih lan nesu. Sampeyan bisa kanthi gampang ngrusak pengalaman pelanggan utawa ilang pangguna sadurunge nggunakake aplikasi kasebut.

Kita nate nemokake manawa aplikasi Dodo Pizza butuh 3 detik kanggo diluncurake rata-rata, lan kanggo sawetara "wong sing begja" butuh 15-20 detik.

Ing ngisor potongan kasebut ana crita kanthi akhir sing nyenengake: babagan pertumbuhan database Realm, bocor memori, kepiye kita nglumpukake obyek sing bersarang, banjur ngumpulake awake dhewe lan ndandani kabeh.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa
Penulis artikel: Maxim Kachinkin β€” Pangembang Android ing Dodo Pizza.

Telung detik saka ngeklik ing lambang aplikasi kanggo onResume () saka kegiatan pisanan iku tanpa wates. Lan kanggo sawetara pangguna, wektu wiwitan tekan 15-20 detik. Carane iki malah bisa?

Ringkesan sing cendhak banget kanggo sing ora duwe wektu kanggo maca
database Realm kita tansaya endlessly. Sawetara obyek nested ora dibusak, nanging terus-terusan akumulasi. Wektu wiwitan aplikasi saya mundhak. Banjur kita ndandani, lan wektu wiwitan tekan target - dadi kurang saka 1 detik lan ora tambah maneh. Artikel kasebut ngemot analisis kahanan lan rong solusi - sing cepet lan sing normal.

Panelusuran lan analisis masalah

Saiki, aplikasi seluler apa wae kudu diluncurake kanthi cepet lan responsif. Nanging ora mung babagan aplikasi seluler. Pengalaman pangguna interaksi karo layanan lan perusahaan minangka perkara sing rumit. Contone, ing kasus kita, kacepetan pangiriman minangka salah sawijining indikator utama kanggo layanan pizza. Yen pangiriman cepet, pizza bakal dadi panas, lan pelanggan sing pengin mangan saiki ora kudu ngenteni suwe. Kanggo aplikasi kasebut, penting kanggo nggawe perasaan layanan cepet, amarga yen aplikasi mung butuh 20 detik kanggo diluncurake, mula suwene sampeyan kudu ngenteni pizza?

Kaping pisanan, kita dhewe ngadhepi kasunyatan manawa aplikasi kasebut butuh sawetara detik kanggo diluncurake, banjur wiwit krungu keluhan saka kolega liyane babagan suwene wektu kasebut. Nanging kita ora bisa terus-terusan mbaleni kahanan iki.

Suwene iku? miturut Dokumentasi Google, yen wiwitan kadhemen saka aplikasi njupuk kurang saka 5 detik, banjur iki dianggep "kaya normal". Aplikasi Android Dodo Pizza diluncurake (miturut metrik Firebase _app_wiwitan) ing wiwitan kadhemen rata-rata ing 3 detik - "Ora gedhe, ora elek," kaya sing diucapake.

Nanging banjur keluhan wiwit katon manawa aplikasi kasebut butuh wektu sing suwe banget kanggo diluncurake! Kanggo miwiti, kita mutusake kanggo ngukur apa "banget, banget, dawa banget". Lan kita nggunakake jejak Firebase kanggo iki Tilak wiwitan app.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Tilak standar iki ngukur wektu antarane wayahe pangguna mbukak aplikasi lan wayahe onResume () saka kegiatan pisanan kaleksanan. Ing Firebase Console metrik iki diarani _app_start. Ternyata:

  • Wektu wiwitan kanggo pangguna ing ndhuwur persentil kaping 95 meh 20 detik (sawetara luwih suwe), sanajan wektu wiwitan kadhemen rata-rata kurang saka 5 detik.
  • Wektu wiwitan ora dadi nilai sing tetep, nanging saya suwe saya suwe. Nanging kadhangkala ana tetes. Kita nemokake pola iki nalika nambah skala analisis nganti 90 dina.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Loro pikirane teka ing pikiran:

  1. Ana sing bocor.
  2. Iki "soko" direset sawise release lan banjur bocor maneh.

"Mungkin soko karo database," kita panginten, lan kita padha bener. Kaping pisanan, kita nggunakake database minangka cache; sajrone migrasi kita mbusak. Kapindho, database dimuat nalika aplikasi diwiwiti. Kabeh mathuk bebarengan.

Apa sing salah karo database Realm

Kita wiwit mriksa carane isi database diganti liwat urip aplikasi, saka instalasi pisanan lan luwih sak nggunakake aktif. Sampeyan bisa ndeleng isi database Realm liwat stetho utawa luwih rinci lan cetha kanthi mbukak file liwat Realm Studio. Kanggo ndeleng isi database liwat ADB, nyalin file database Realm:

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

Sawise ndeleng isi database ing wektu sing beda-beda, kita nemokake manawa jumlah obyek saka jinis tartamtu terus saya tambah.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa
Gambar kasebut nuduhake fragmen Realm Studio kanggo rong file: ing sisih kiwa - basis aplikasi sawetara wektu sawise instalasi, ing sisih tengen - sawise nggunakake aktif. Bisa dideleng yen jumlah obyek ImageEntity ΠΈ MoneyType wis akeh banget (gambar nuduhake jumlah obyek saben jinis).

Hubungan antarane wutah database lan wektu wiwitan

Wutah database sing ora dikontrol banget ala. Nanging kepiye iki mengaruhi wektu wiwitan aplikasi? Iku cukup gampang kanggo ngukur iki liwat ActivityManager. Wiwit Android 4.4, logcat nampilake log kanthi string sing Ditampilake lan wektu. Wektu iki padha karo interval saka wayahe aplikasi diluncurake nganti pungkasan rendering kegiatan. Sajrone wektu kasebut kedadeyan ing ngisor iki:

  • Miwiti proses.
  • Initialization saka obyek.
  • Nggawe lan initialization saka aktivitas.
  • Nggawe tata letak.
  • Rendering aplikasi.

Cocok karo kita. Yen sampeyan mbukak ADB nganggo flag -S lan -W, sampeyan bisa entuk output lengkap kanthi wektu wiwitan:

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

Yen sampeyan njupuk saka kono grep -i WaitTime wektu, sampeyan bisa ngotomatisasi koleksi metrik iki lan visual katon ing asil. Grafik ing ngisor iki nuduhake katergantungan wektu wiwitan aplikasi ing jumlah wiwitan kadhemen aplikasi.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Ing wektu sing padha, ana hubungan sing padha karo ukuran lan pertumbuhan database, sing tuwuh saka 4 MB dadi 15 MB. Secara total, ternyata liwat wektu (kanthi wutah kadhemen wiwit), wektu peluncuran aplikasi lan ukuran database tambah. Kita duwe hipotesis ing tangan kita. Saiki sing isih ana yaiku konfirmasi ketergantungan. Mulane, kita mutusake kanggo mbusak "bocor" lan weruh yen iki bakal nyepetake peluncuran.

Alasan kanggo wutah database telas

Sadurunge njabut "bocor", iku worth ngerti kok padha muncul ing Panggonan pisanan. Kanggo nindakake iki, ayo elinga apa Realm.

Realm minangka basis data non-relasional. Ngidini sampeyan njlèntrèhaké hubungan ing antarane obyek kanthi cara sing padha karo jumlah database hubungan ORM ing Android sing diterangake. Ing wektu sing padha, Realm nyimpen obyek langsung ing memori kanthi jumlah transformasi lan pemetaan paling sithik. Iki ngidini sampeyan maca data saka disk kanthi cepet, yaiku kekuwatan Realm lan kenapa ditresnani.

(Kanggo tujuan artikel iki, katrangan iki bakal cukup kanggo kita. Sampeyan bisa maca liyane babagan Realm ing kelangan dokumentasi utawa ing akademi).

Akeh pangembang rakulino kanggo nggarap database relasional (contone, database ORM karo SQL ing hood). Lan bab kaya pambusakan data runtun asring katon kaya diwenehi. Nanging ora ing Realm.

Miturut cara, fitur pambusakan cascade wis dijaluk suwe. Iki revisi ΠΈ liyane, digandhengake karo, aktif rembugan. Ana rasa sing bakal enggal rampung. Nanging banjur kabeh diterjemahake menyang introduksi pranala sing kuwat lan lemah, sing uga bakal ngrampungake masalah iki kanthi otomatis. Cukup sregep lan aktif ing tugas iki panjaluk narik, sing saiki wis ngaso amarga masalah internal.

Data bocor tanpa mbusak runtun

Kepiye carane data bocor yen sampeyan ngandelake delete cascading sing ora ana? Yen sampeyan wis nested obyek Realm, banjur padha kudu dibusak.
Ayo katon ing conto (meh) nyata. Kita duwe obyek 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 ing kranjang duwe lapangan sing beda-beda, kalebu gambar ImageEntity, bahan khusus CustomizationEntity. Uga, produk ing kranjang bisa dadi kombo karo produk dhewe RealmList (CartProductEntity). Kabeh kolom sing kadhaptar minangka obyek Realm. Yen kita nglebokake obyek anyar (copyToRealm () / copyToRealmOrUpdate ()) karo id padha, obyek iki bakal rampung overwritten. Nanging kabeh obyek internal (gambar, customizationEntity lan cartComboProducts) bakal kelangan sambungan karo wong tuwa lan tetep ing database.

Wiwit sambungan karo wong-wong mau wis ilang, kita ora maca maneh utawa mbusak mau (kajaba kita tegas ngakses utawa mbusak kabeh "tabel"). We disebut iki "bocor memori".

Nalika kita nggarap Realm, kita kudu kanthi jelas ngliwati kabeh unsur lan kanthi jelas mbusak kabeh sadurunge operasi kasebut. Iki bisa ditindakake, contone, kaya iki:

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

Yen sampeyan nindakake iki, kabeh bakal bisa kaya sing dikarepake. Ing conto iki, kita nganggep yen ora ana obyek Realm sing bersarang liyane ing gambar, customizationEntity, lan cartComboProducts, saengga ora ana puteran lan penghapusan liyane.

Solusi "cepet".

Babagan pisanan sing kita mutusake yaiku ngresiki obyek sing paling cepet tuwuh lan mriksa asil kanggo ndeleng apa iki bakal ngrampungake masalah asli kita. Kaping pisanan, solusi sing paling gampang lan paling intuisi digawe, yaiku: saben obyek kudu tanggung jawab kanggo ngilangi anak-anake. Kanggo nindakake iki, kita ngenalake antarmuka sing ngasilake dhaptar obyek Realm sing bersarang:

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

Lan kita ngetrapake ing obyek 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 kita bali kabeh anak minangka dhaftar warata. Lan saben obyek anak uga bisa ngleksanakake antarmuka NestedEntityAware, nuduhake yen ana obyek Realm internal kanggo mbusak, contone. 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
   )
 }
}

Lan sateruse, nesting obyek bisa diulang.

Banjur kita nulis cara sing rekursif mbusak kabeh obyek nested. Metode (digawe minangka extension) deleteAllNestedEntities entuk kabeh obyek lan metode tingkat paling dhuwur deleteNestedRecursively Mbusak kabeh obyek bersarang kanthi rekursif nggunakake antarmuka 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()
   }
 }
}

Kita nindakake iki karo obyek sing paling cepet tuwuh lan mriksa apa sing kedadeyan.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Akibaté, obyek sing ditutupi karo solusi iki mandheg tuwuh. Lan wutah sakabèhé saka basa kalem, nanging ora mandheg.

Solusi "normal".

Sanajan dhasar wiwit tuwuh luwih alon, nanging isih tuwuh. Dadi kita miwiti looking luwih. Proyek kita nggunakake cache data kanthi aktif ing Realm. Mulane, nulis kabeh obyek nested kanggo saben obyek iku pegawe-intensif, plus risiko kasalahan mundhak, amarga sampeyan bisa lali kanggo nemtokake obyek nalika ngganti kode.

Aku wanted kanggo mesthekake yen aku ora nggunakake antarmuka, nanging kabeh bisa ing dhewe.

Nalika kita pengin soko bisa digunakake dhewe, kita kudu nggunakake refleksi. Kanggo nindakake iki, kita bisa ngliwati saben lapangan kelas lan mriksa apa iku obyek Realm utawa dhaptar obyek:

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

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

Yen lapangan kasebut minangka RealmModel utawa RealmList, banjur tambahake obyek saka lapangan iki menyang dhaptar obyek sing disarangke. Kabeh padha persis kaya ing ndhuwur, mung ing kene bakal ditindakake dhewe. Cara mbusak cascade dhewe gampang banget lan katon kaya iki:

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

Ekstensi filterRealmObject nyaring metu lan mung liwat obyek Realm. Metode getNestedRealmObjects liwat bayangan, nemokake kabeh obyek Realm nested lan sijine menyang dhaftar linear. Banjur kita nindakake perkara sing padha kanthi rekursif. Nalika mbusak, sampeyan kudu mriksa obyek kanggo validitas isValid, amarga bisa uga obyek induk sing beda-beda bisa duwe sarang sing padha. Iku luwih apik kanggo ngindhari iki lan mung nggunakake generasi otomatis id nalika nggawe obyek anyar.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Implementasi lengkap metode 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)
}

AkibatΓ©, ing kode klien kita nggunakake "cascading delete" kanggo saben operasi modifikasi data. Contone, kanggo operasi insert katon kaya iki:

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

Metode pisanan getManagedEntities nampa kabeh obyek ditambahakΓ©, lan banjur cara cascadeDelete Mbusak kabeh obyek sing diklumpukake kanthi rekursif sadurunge nulis sing anyar. Kita mungkasi nggunakake pendekatan iki ing saindhenging aplikasi. Bocor memori ing Realm wis ilang. Sawise nindakake pangukuran sing padha karo katergantungan wektu wiwitan ing jumlah wiwitan kadhemen aplikasi, kita ndeleng asil.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Garis ijo nuduhake katergantungan wektu wiwitan aplikasi ing jumlah wiwitan kadhemen sajrone pambusakan kaskade otomatis obyek bersarang.

Asil lan kesimpulan

Basis data Realm sing terus berkembang nyebabake aplikasi kasebut diluncurake alon-alon. Kita dirilis nganyari karo dhewe "cascading mbusak" obyek nested. Lan saiki kita ngawasi lan ngevaluasi kepiye keputusan kita mengaruhi wektu wiwitan aplikasi liwat metrik _app_start.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Kanggo analisis, kita njupuk wektu 90 dina lan ndeleng: wektu peluncuran aplikasi, loro median lan sing ana ing persentil 95th pangguna, wiwit suda lan ora munggah maneh.

Crita babagan carane mbusak cascade ing Realm menang liwat peluncuran sing dawa

Yen sampeyan ndeleng grafik pitung dina, metrik _app_start katon cukup lan kurang saka 1 detik.

Sampeyan uga kudu ditambahake yen kanthi gawan, Firebase ngirim kabar yen nilai median _app_start ngluwihi 5 detik. Nanging, kaya sing bisa dideleng, sampeyan ora kudu ngandelake iki, nanging mlebu lan mriksa kanthi jelas.

Babagan khusus babagan database Realm yaiku database non-relasional. Sanajan gampang digunakake, mirip karo solusi ORM lan panyambungan obyek, ora ana penghapusan kaskade.

Yen iki ora dianggep, obyek nested bakal nglumpukake lan "bocor adoh." Basis data bakal terus berkembang, sing bakal nyebabake kalem utawa wiwitan aplikasi.

Aku nuduhake pengalaman babagan carane cepet mbusak cascade obyek ing Realm, sing durung metu saka kothak, nanging wis suwe dirembug. padha ngomong ΠΈ padha ngomong. Ing kasus kita, iki nyepetake wektu wiwitan aplikasi.

Sanajan ana diskusi babagan tampilan fitur iki, ora ana penghapusan kaskade ing Realm ditindakake kanthi desain. Yen sampeyan ngrancang aplikasi anyar, mula kudu dipikirake. Lan yen sampeyan wis nggunakake Realm, priksa manawa sampeyan duwe masalah kasebut.

Source: www.habr.com

Add a comment