Ir-rakkont ta' kif it-tħassir tal-kaskata f'Realm rebaħ fuq tnedija twila
L-utenti kollha jieħdu tnedija mgħaġġla u UI li tirreaġixxi fl-applikazzjonijiet mobbli bħala fatt. Jekk l-applikazzjoni tieħu żmien twil biex tniedi, l-utent jibda jħossu imdejjaq u rrabjat. Tista 'faċilment tħassar l-esperjenza tal-klijent jew titlef kompletament lill-utent anki qabel ma jibda juża l-applikazzjoni.
Darba skoprejna li l-app Dodo Pizza tieħu 3 sekondi biex titnieda bħala medja, u għal xi "xxurtjati" tieħu 15-20 sekonda.
Taħt il-qatgħa hemm storja bi tmiem kuntenti: dwar it-tkabbir tad-database Realm, tnixxija tal-memorja, kif akkumulajna oġġetti nested, u mbagħad ġibna lilna nfusna flimkien u ffissajna kollox.
Tliet sekondi minn meta tikklikkja fuq l-ikona tal-applikazzjoni għal onResume() tal-ewwel attività hija infinita. U għal xi utenti, il-ħin tal-istartjar laħaq 15-20 sekonda. Dan kif huwa anki possibbli?
Sommarju qasir ħafna għal dawk li m'għandhomx ħin biex jaqraw
Id-database tal-Isfera tagħna kibret bla tmiem. Xi oġġetti nested ma ġewx imħassra, iżda kienu kontinwament akkumulati. Il-ħin tal-istartjar tal-applikazzjoni żdied gradwalment. Imbagħad aħna rranġawha, u l-ħin tal-istartjar wasal għall-mira - sar inqas minn sekonda 1 u m'għadux żdied. L-artiklu fih analiżi tas-sitwazzjoni u żewġ soluzzjonijiet - waħda ta' malajr u waħda normali.
Tiftix u analiżi tal-problema
Illum, kull applikazzjoni mobbli trid titnieda malajr u tirreaġixxi. Imma mhux biss dwar l-app mobbli. L-esperjenza tal-utent ta 'interazzjoni ma' servizz u kumpanija hija ħaġa kumplessa. Pereżempju, fil-każ tagħna, il-veloċità tal-kunsinna hija waħda mill-indikaturi ewlenin għas-servizz tal-pizza. Jekk il-kunsinna tkun veloċi, il-pizza tkun sħuna, u l-klijent li jrid jiekol issa ma jkollux għalfejn jistenna ħafna. Għall-applikazzjoni, min-naħa tiegħu, huwa importanti li tinħoloq sensazzjoni ta 'servizz mgħaġġel, għaliex jekk l-applikazzjoni tieħu biss 20 sekonda biex titnieda, allura kemm se jkollok tistenna l-pizza?
Għall-ewwel, aħna stess konna ffaċċjati bil-fatt li kultant l-applikazzjoni kienet tieħu ftit sekondi biex titnieda, u mbagħad bdejna nisimgħu lmenti minn kollegi oħra dwar kemm damet. Imma ma stajniex nirrepetu din is-sitwazzjoni b’mod konsistenti.
Kemm hu twil? Skond Dokumentazzjoni Google, jekk bidu kiesaħ ta 'applikazzjoni jieħu inqas minn 5 sekondi, allura dan jitqies "bħallikieku normali". Tnediet l-app Dodo Pizza Android (skond il-metriċi ta' Firebase _app_start) fi bidu kiesaħ bħala medja fi 3 sekondi - "Mhux kbir, mhux terribbli," kif jgħidu.
Iżda mbagħad bdew jidhru lmenti li l-applikazzjoni damet ħafna, ħafna, ħafna żmien biex titnieda! Biex nibdew, iddeċidejna li nkejlu x'inhu "ħafna, ħafna, twil ħafna". U użajna Firebase traċċa għal dan App tibda traċċa.
Din it-traċċa standard tkejjel iż-żmien bejn il-mument li l-utent jiftaħ l-applikazzjoni u l-mument li jiġi esegwit l-onResume() tal-ewwel attività. Fil-Firebase Console din il-metrika tissejjaħ _app_start. Irriżulta li:
Il-ħinijiet tal-istartjar għall-utenti 'l fuq mill-95 perċentil huma kważi 20 sekonda (xi wħud saħansitra itwal), minkejja li l-ħin medjan tal-istartjar kiesaħ huwa inqas minn 5 sekondi.
Il-ħin tal-istartjar mhuwiex valur kostanti, iżda jikber maż-żmien. Imma kultant ikun hemm qtar. Sibna dan il-mudell meta żidna l-iskala tal-analiżi għal 90 jum.
Ġew f’moħħna żewġ ħsibijiet:
Xi ħaġa qed tnixxi.
Din "xi ħaġa" hija reset wara r-rilaxx u mbagħad tnixxi mill-ġdid.
"Probabbilment xi ħaġa mad-database," ħsibna, u kellna raġun. L-ewwelnett, nużaw id-database bħala cache; waqt il-migrazzjoni aħna nneħħiha. It-tieni nett, id-database titgħabba meta tibda l-applikazzjoni. Kollox jaqbel flimkien.
X'hemm ħażin fid-database Realm
Bdejna niċċekkjaw kif il-kontenut tad-database jinbidel matul il-ħajja tal-applikazzjoni, mill-ewwel installazzjoni u aktar waqt l-użu attiv. Tista 'tara l-kontenut tad-database Realm permezz stetho jew f'aktar dettall u b'mod ċar billi tiftaħ il-fajl permezz Realm Studio. Biex tara l-kontenut tad-database permezz tal-ADB, ikkopja l-fajl tad-database Realm:
Wara li ħares lejn il-kontenut tad-database fi żminijiet differenti, sibna li n-numru ta 'oġġetti ta' ċertu tip qiegħed dejjem jiżdied.
L-istampa turi framment ta 'Realm Studio għal żewġ fajls: fuq ix-xellug - il-bażi tal-applikazzjoni xi żmien wara l-installazzjoni, fuq il-lemin - wara użu attiv. Wieħed jista 'jara li n-numru ta' oġġetti ImageEntity и MoneyType kibret b'mod sinifikanti (l-screenshot turi n-numru ta 'oġġetti ta' kull tip).
Relazzjoni bejn it-tkabbir tad-database u l-ħin tal-istartjar
It-tkabbir mhux ikkontrollat tad-database huwa ħażin ħafna. Imma dan kif jaffettwa l-ħin tal-istartjar tal-applikazzjoni? Huwa pjuttost faċli li tkejjel dan permezz tal-ActivityManager. Minn Android 4.4, logcat juri r-reġistru bl-istring Displayed u l-ħin. Dan il-ħin huwa ugwali għall-intervall mill-mument li titnieda l-applikazzjoni sat-tmiem tar-rendi tal-attività. Matul dan iż-żmien iseħħu l-avvenimenti li ġejjin:
Ibda l-proċess.
Inizjalizzazzjoni ta' oġġetti.
Ħolqien u inizjalizzazzjoni ta 'attivitajiet.
Ħolqien ta 'tqassim.
Rendiment ta' applikazzjoni.
jaqbel lilna. Jekk tmexxi ADB bil-bnadar -S u -W, tista 'tikseb output estiż bil-ħin tal-istartjar:
adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN
Jekk taqbadha minn hemm grep -i WaitTime ħin, tista 'awtomatizza l-ġbir ta' din il-metrika u tħares viżwalment lejn ir-riżultati. Il-graff hawn taħt turi d-dipendenza tal-ħin tal-istartjar tal-applikazzjoni fuq in-numru ta 'startjar kiesaħ tal-applikazzjoni.
Fl-istess ħin, kien hemm l-istess natura tar-relazzjoni bejn id-daqs u t-tkabbir tad-database, li kibret minn 4 MB għal 15 MB. B'kollox, jirriżulta li maż-żmien (bit-tkabbir tal-bidu kiesaħ), żdiedu kemm il-ħin tat-tnedija tal-applikazzjoni kif ukoll id-daqs tad-database. Għandna ipoteżi fuq idejna. Issa baqa’ biss li tikkonferma d-dipendenza. Għalhekk, iddeċidejna li nneħħu l-"tnixxijiet" u naraw jekk dan iħaffef it-tnedija.
Raġunijiet għat-tkabbir tad-database bla tarf
Qabel ma tneħħi "tnixxijiet", ta 'min jifhem għaliex dehru fl-ewwel lok. Biex tagħmel dan, ejja niftakru x'inhu l-Isfera.
Realm hija database mhux relazzjonali. Jippermettilek tiddeskrivi relazzjonijiet bejn oġġetti b'mod simili għal kemm huma deskritti databases relazzjonali ORM fuq Android. Fl-istess ħin, Realm jaħżen oġġetti direttament fil-memorja bl-inqas ammont ta 'trasformazzjonijiet u mappings. Dan jippermettilek taqra data mid-diska malajr ħafna, li hija s-saħħa tal-Realm u għaliex hija maħbuba.
(Għall-finijiet ta 'dan l-artikolu, din id-deskrizzjoni se tkun biżżejjed għalina. Tista' taqra aktar dwar Realm fil-beraħ dokumentazzjoni jew fil tagħhom akkademja).
Ħafna żviluppaturi huma mdorrijin jaħdmu aktar ma 'databases relazzjonali (per eżempju, databases ORM b'SQL taħt il-barnuża). U affarijiet bħat-tħassir tad-data cascading spiss jidhru bħala mogħtija. Imma mhux fl-Isfera.
Mill-mod, il-karatteristika tat-tħassir tal-kaskata ilha mitluba għal żmien twil. Dan reviżjoni и ieħor, assoċjat magħha, ġie diskuss b'mod attiv. Kien hemm sensazzjoni li dalwaqt se jsir. Iżda mbagħad kollox tradott fl-introduzzjoni ta 'rabtiet b'saħħithom u dgħajfa, li wkoll issolvi awtomatikament din il-problema. Kien pjuttost ħaj u attiv f'dan il-kompitu talba tal-ġibda, li għalissa twaqqaf minħabba diffikultajiet interni.
Tnixxija tad-dejta mingħajr tħassir kaskata
Kif eżattament titnixxi d-dejta jekk tiddependi fuq tħassir ta 'kaskata ineżistenti? Jekk għandek oġġetti tal-Realm imnaqqsa, allura għandhom jitħassru.
Ejja nħarsu lejn eżempju (kważi) reali. Għandna oġġett 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()
Il-prodott fil-karrettun għandu oqsma differenti, inkluża stampa ImageEntity, ingredjenti personalizzati CustomizationEntity. Ukoll, il-prodott fil-karrettun jista 'jkun combo bis-sett ta' prodotti tiegħu stess RealmList (CartProductEntity). L-oqsma kollha elenkati huma oġġetti ta' Realm. Jekk indaħħlu oġġett ġdid (copyToRealm() / copyToRealmOrUpdate()) bl-istess id, allura dan l-oġġett jinkiteb kompletament. Iżda l-oġġetti interni kollha (immaġni, customizationEntity u cartComboProducts) jitilfu l-konnessjoni mal-ġenitur u jibqgħu fid-database.
Peress li l-konnessjoni magħhom tintilef, m'għadniex naqrawhom jew inħassruhom (sakemm ma naċċessawhomx b'mod espliċitu jew inneħħu t-"tabella") kollha. Aħna sejħu dan "tnixxijiet tal-memorja".
Meta naħdmu ma 'Realm, irridu ngħaddu b'mod espliċitu mill-elementi kollha u espliċitament inħassru kollox qabel tali operazzjonijiet. Dan jista 'jsir, pereżempju, bħal dan:
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()
}
// и потом уже сохраняем
Jekk tagħmel dan, allura kollox jaħdem kif suppost. F'dan l-eżempju, nassumu li m'hemm l-ebda oġġett ieħor tal-Realm ibejjed ġewwa immaġini, customizationEntity, u cartComboProducts, u għalhekk m'hemm l-ebda loops u tħassir nested oħra.
Soluzzjoni ta 'malajr
L-ewwel ħaġa li ddeċidejna li nagħmlu kienet inaddfu l-oġġetti li qed jikbru malajr u niċċekkjaw ir-riżultati biex naraw jekk dan isolvix il-problema oriġinali tagħna. L-ewwel, saret l-aktar soluzzjoni sempliċi u intuwittiva, jiġifieri: kull oġġett għandu jkun responsabbli għat-tneħħija tat-tfal tiegħu. Biex tagħmel dan, introduċejna interface li rritornat lista tal-oġġetti tal-Realm imnaqqxa tagħha:
interface NestedEntityAware {
fun getNestedEntities(): Collection<RealmObject?>
}
U implimentajnaha fl-oġġetti tal-Isfera tagħna:
@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 nirritornaw lit-tfal kollha bħala lista fissa. U kull oġġett tifel jista 'wkoll jimplimenta l-interface NestedEntityAware, li jindika li għandu oġġetti ta' Realm interni x'tħassar, pereżempju 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
)
}
}
U l-bqija, it-tbejjit ta 'oġġetti jista' jiġi ripetut.
Imbagħad niktbu metodu li b'mod rikorsiv iħassar l-oġġetti kollha nested. Metodu (magħmul bħala estensjoni) deleteAllNestedEntities iġib l-oġġetti u l-metodu tal-ogħla livell deleteNestedRecursively Tneħħi b'mod rikorsiv l-oġġetti kollha nested billi tuża l-interface 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()
}
}
}
Għamilna dan bl-oġġetti li qed jikbru malajr u ċċekkajna x’ġara.
Bħala riżultat, dawk l-oġġetti li koprejna b'din is-soluzzjoni waqfu jikbru. U t-tkabbir ġenerali tal-bażi naqas, iżda ma waqafx.
Is-soluzzjoni "normali".
Għalkemm il-bażi bdiet tikber aktar bil-mod, xorta kibret. Għalhekk bdejna nħarsu aktar. Il-proġett tagħna jagħmel użu attiv ħafna mill-caching tad-dejta f'Realm. Għalhekk, il-kitba ta 'l-oġġetti kollha nested għal kull oġġett hija intensiva fil-ħidma, flimkien ma' r-riskju ta 'żbalji jiżdied, għaliex tista' tinsa li tispeċifika l-oġġetti meta tbiddel il-kodiċi.
Ridt niżgura li ma użajtx interfaces, iżda li kollox ħadem waħdu.
Meta rridu li xi ħaġa taħdem waħedha, irridu nużaw ir-riflessjoni. Biex nagħmlu dan, nistgħu ngħaddu minn kull qasam tal-klassi u niċċekkjaw jekk huwiex oġġett ta' Realm jew lista ta' oġġetti:
Jekk il-qasam huwa RealmModel jew RealmList, imbagħad żid l-oġġett ta 'dan il-qasam ma' lista ta 'oġġetti mnaqqsa. Kollox huwa eżattament l-istess kif għamilna hawn fuq, biss hawn se jsir waħdu. Il-metodu tat-tħassir tal-kaskata innifsu huwa sempliċi ħafna u jidher bħal dan:
Estensjoni filterRealmObject jiffiltra u jgħaddi biss oġġetti Realm. Metodu getNestedRealmObjects permezz tar-riflessjoni, isib l-oġġetti kollha tal-Realm imnaqqxa u jpoġġihom f'lista lineari. Imbagħad nagħmlu l-istess ħaġa b'mod rikorsiv. Meta tħassar, trid tiċċekkja l-oġġett għall-validità isValid, minħabba li jista 'jkun li oġġetti ġenitur differenti jista' jkollhom dawk identiċi mnaqqsa. Huwa aħjar li tevita dan u sempliċement tuża awto-ġenerazzjoni ta 'id meta toħloq oġġetti ġodda.
Implimentazzjoni sħiħa tal-metodu 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)
}
Bħala riżultat, fil-kodiċi tal-klijent tagħna nużaw "cascading delete" għal kull operazzjoni ta 'modifika tad-dejta. Per eżempju, għal operazzjoni ta 'inserzjoni tidher bħal din:
Metodu l-ewwel getManagedEntities jirċievi l-oġġetti kollha miżjuda, u mbagħad il-metodu cascadeDelete Tħassar b'mod rikorsiv l-oġġetti kollha miġbura qabel ma tikteb oħrajn ġodda. Nispiċċaw nużaw dan l-approċċ matul l-applikazzjoni kollha. It-tnixxijiet tal-memorja f'Realm spiċċaw kompletament. Wara li wettaq l-istess kejl tad-dipendenza tal-ħin tal-istartjar fuq in-numru ta 'bidu kiesaħ tal-applikazzjoni, naraw ir-riżultat.
Il-linja ħadra turi d-dipendenza tal-ħin tal-istartjar tal-applikazzjoni fuq in-numru ta 'startjar kiesaħ waqt it-tħassir awtomatiku tal-kaskata ta' oġġetti nested.
Riżultati u konklużjonijiet
Id-database Realm li dejjem tikber kienet qed tikkawża li l-applikazzjoni titnieda bil-mod ħafna. Ħarġajna aġġornament b'"cascading delete" tagħna stess ta' oġġetti nested. U issa aħna nissorveljaw u nevalwaw kif id-deċiżjoni tagħna affettwat il-ħin tat-tnedija tal-applikazzjoni permezz tal-metrika _app_start.
Għall-analiżi, nieħdu perjodu ta 'żmien ta' 90 jum u naraw: il-ħin tat-tnedija tal-applikazzjoni, kemm il-medjan kif ukoll dak li jaqa 'fuq il-95 perċentil tal-utenti, beda jonqos u m'għadux jiżdied.
Jekk tħares lejn it-tabella ta' sebat ijiem, il-metrika _app_start tidher kompletament adegwata u hija inqas minn sekonda.
Ta’ min iżżid ukoll li b’mod awtomatiku, Firebase jibgħat notifiki jekk il-valur medjan ta’ _app_start jaqbeż il-5 sekondi. Madankollu, kif nistgħu naraw, m'għandekx tistrieħ fuq dan, iżda pjuttost tidħol u tiċċekkjaha b'mod espliċitu.
Il-ħaġa speċjali dwar id-database Realm hija li hija database mhux relazzjonali. Minkejja l-faċilità ta 'użu, ix-xebh ma' soluzzjonijiet ORM u konnessjoni ta 'oġġetti, m'għandux tħassir tal-kaskata.
Jekk dan ma jitqiesx, allura l-oġġetti nested jakkumulaw u "jnixxu". Id-database se tikber b'mod kostanti, li mbagħad taffettwa t-tnaqqis jew l-istartjar tal-applikazzjoni.
Qsamt l-esperjenza tagħna dwar kif malajr nagħmlu tħassir ta’ kaskata ta’ oġġetti f’Realm, li għadha mhix barra mill-kaxxa, iżda ilha titkellem dwarha jgħidu и jgħidu. Fil-każ tagħna, dan għaġġel ħafna l-ħin tal-istartjar tal-applikazzjoni.
Minkejja d-diskussjoni dwar id-dehra imminenti ta 'din il-karatteristika, in-nuqqas ta' tħassir ta 'kaskata f'Realm isir permezz tad-disinn. Jekk qed tfassal applikazzjoni ġdida, imbagħad ikkunsidra dan. U jekk diġà qed tuża Realm, iċċekkja jekk għandekx problemi bħal dawn.