Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Të gjithë përdoruesit e marrin si të mirëqenë nisjen e shpejtë dhe ndërfaqen e përgjegjshme në aplikacionet celulare. Nëse aplikacioni kërkon shumë kohë për t'u nisur, përdoruesi fillon të ndihet i trishtuar dhe i zemëruar. Ju lehtë mund të prishni përvojën e klientit ose ta humbni plotësisht përdoruesin edhe para se të fillojë të përdorë aplikacionin.

Dikur zbuluam se aplikacioni Dodo Pizza merr mesatarisht 3 sekonda për t'u nisur, dhe për disa "me fat" duhen 15-20 sekonda.

Poshtë prerjes është një histori me një fund të lumtur: në lidhje me rritjen e bazës së të dhënave të Realm, një rrjedhje memorie, se si grumbulluam objekte të vendosura dhe më pas u mblodhëm dhe rregulluam gjithçka.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë
Autori i artikullit: Maxim Kachinkin — Zhvilluesi i Android në Dodo Pizza.

Tre sekonda nga klikimi në ikonën e aplikacionit te onResume() i aktivitetit të parë janë pafundësi. Dhe për disa përdorues, koha e fillimit arriti në 15-20 sekonda. Si është madje e mundur kjo?

Një përmbledhje shumë e shkurtër për ata që nuk kanë kohë për të lexuar
Baza e të dhënave të Mbretërisë sonë u rrit pafundësisht. Disa objekte të mbivendosur nuk u fshinë, por u grumbulluan vazhdimisht. Koha e nisjes së aplikacionit u rrit gradualisht. Pastaj e rregulluam dhe koha e fillimit erdhi në objektiv - u bë më pak se 1 sekondë dhe nuk u rrit më. Artikulli përmban një analizë të situatës dhe dy zgjidhje - një të shpejtë dhe një normale.

Kërkimi dhe analiza e problemit

Sot, çdo aplikacion celular duhet të nisë shpejt dhe të jetë i përgjegjshëm. Por nuk ka të bëjë vetëm me aplikacionin celular. Përvoja e përdoruesit të ndërveprimit me një shërbim dhe një kompani është një gjë komplekse. Për shembull, në rastin tonë, shpejtësia e dorëzimit është një nga treguesit kryesorë për shërbimin e picës. Nëse dorëzimi është i shpejtë, pica do të jetë e nxehtë dhe klienti që dëshiron të hajë tani nuk do të duhet të presë gjatë. Për aplikacionin, nga ana tjetër, është e rëndësishme të krijoni një ndjenjë shërbimi të shpejtë, sepse nëse aplikacioni merr vetëm 20 sekonda për t'u nisur, atëherë sa kohë do t'ju duhet të prisni për picën?

Në fillim, ne vetë u përballëm me faktin se ndonjëherë aplikacioni merrte disa sekonda për t'u nisur, dhe më pas filluam të dëgjonim ankesa nga kolegë të tjerë për sa kohë zgjati. Por ne nuk ishim në gjendje ta përsërisnim vazhdimisht këtë situatë.

Sa e gjatë është? Sipas Dokumentacioni i Google, nëse një fillim i ftohtë i një aplikacioni zgjat më pak se 5 sekonda, atëherë kjo konsiderohet "sikur normale". U lançua aplikacioni Android Dodo Pizza (sipas matjeve të Firebase _app_start) në fillimi i ftohtë mesatarisht në 3 sekonda - "Jo e mrekullueshme, jo e tmerrshme", siç thonë ata.

Por më pas filluan të shfaqen ankesat se aplikacioni mori një kohë shumë, shumë, shumë të gjatë për të nisur! Për të filluar, ne vendosëm të masim se çfarë është "shumë, shumë, shumë e gjatë". Dhe ne përdorëm gjurmën Firebase për këtë Gjurma e fillimit të aplikacionit.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Kjo gjurmë standarde mat kohën ndërmjet momentit kur përdoruesi hap aplikacionin dhe momentit kur ekzekutohet onResume() e aktivitetit të parë. Në konsolën e Firebase kjo metrikë quhet _app_start. Doli se:

  • Kohët e fillimit për përdoruesit mbi përqindjen e 95-të janë gati 20 sekonda (disa edhe më të gjata), pavarësisht se koha mesatare e fillimit të ftohtë është më pak se 5 sekonda.
  • Koha e fillimit nuk është një vlerë konstante, por rritet me kalimin e kohës. Por ndonjëherë ka pika. Ne e gjetëm këtë model kur rritëm shkallën e analizës në 90 ditë.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Dy mendime më erdhën në mendje:

  1. Diçka po rrjedh.
  2. Kjo "diçka" rivendoset pas lëshimit dhe më pas rrjedh përsëri.

"Ndoshta diçka me bazën e të dhënave," menduam dhe kishim të drejtë. Së pari, ne përdorim bazën e të dhënave si një cache; gjatë migrimit ne e pastrojmë atë. Së dyti, baza e të dhënave ngarkohet kur fillon aplikacioni. Gjithçka përshtatet së bashku.

Çfarë nuk shkon me bazën e të dhënave Realm

Filluam të kontrollojmë se si ndryshon përmbajtja e bazës së të dhënave gjatë jetës së aplikacionit, që nga instalimi i parë dhe më tej gjatë përdorimit aktiv. Ju mund të shikoni përmbajtjen e bazës së të dhënave Realm nëpërmjet steto ose më hollësisht dhe qartë duke hapur skedarin nëpërmjet Realm Studio. Për të parë përmbajtjen e bazës së të dhënave nëpërmjet ADB, kopjoni skedarin e bazës së të dhënave Realm:

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

Duke parë përmbajtjen e bazës së të dhënave në periudha të ndryshme, zbuluam se numri i objekteve të një lloji të caktuar po rritet vazhdimisht.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë
Fotografia tregon një fragment të Realm Studio për dy skedarë: në të majtë - baza e aplikacionit disa kohë pas instalimit, në të djathtë - pas përdorimit aktiv. Mund të shihet se numri i objekteve ImageEntity и MoneyType është rritur ndjeshëm (pamja e ekranit tregon numrin e objekteve të secilit lloj).

Marrëdhënia midis rritjes së bazës së të dhënave dhe kohës së fillimit

Rritja e pakontrolluar e bazës së të dhënave është shumë e keqe. Por si ndikon kjo në kohën e fillimit të aplikacionit? Është mjaft e lehtë për ta matur këtë përmes ActivityManager. Që nga Android 4.4, logcat shfaq regjistrin me vargun e shfaqur dhe kohën. Kjo kohë është e barabartë me intervalin nga momenti i hapjes së aplikacionit deri në përfundimin e paraqitjes së aktivitetit. Gjatë kësaj kohe ndodhin ngjarjet e mëposhtme:

  • Filloni procesin.
  • Inicializimi i objekteve.
  • Krijimi dhe inicializimi i aktiviteteve.
  • Krijimi i një plan urbanistik.
  • Paraqitja e aplikacionit.

na përshtatet. Nëse përdorni ADB me flamujt -S dhe -W, mund të merrni rezultate të zgjatura me kohën e fillimit:

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

Nëse e kapni prej andej grep -i WaitTime kohë, ju mund të automatizoni mbledhjen e kësaj metrike dhe të shikoni vizualisht rezultatet. Grafiku më poshtë tregon varësinë e kohës së fillimit të aplikacionit nga numri i fillimeve të ftohta të aplikacionit.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Në të njëjtën kohë, ekzistonte e njëjta natyrë e marrëdhënies midis madhësisë dhe rritjes së bazës së të dhënave, e cila u rrit nga 4 MB në 15 MB. Në total, rezulton se me kalimin e kohës (me rritjen e fillimeve të ftohta), si koha e nisjes së aplikacionit ashtu edhe madhësia e bazës së të dhënave u rritën. Ne kemi një hipotezë në duart tona. Tani mbetej vetëm të konfirmohej varësia. Prandaj, vendosëm të heqim "rrjedhjet" dhe të shohim nëse kjo do të përshpejtonte nisjen.

Arsyet për rritjen e pafundme të bazës së të dhënave

Para se të hiqni "rrjedhjet", ia vlen të kuptoni pse u shfaqën në radhë të parë. Për ta bërë këtë, le të kujtojmë se çfarë është Realm.

Realm është një bazë të dhënash jo-relacionale. Kjo ju lejon të përshkruani marrëdhëniet midis objekteve në një mënyrë të ngjashme me atë se sa baza të të dhënave relacionale ORM përshkruhen në Android. Në të njëjtën kohë, Realm ruan objektet drejtpërdrejt në memorie me sasinë më të vogël të transformimeve dhe hartave. Kjo ju lejon të lexoni të dhënat nga disku shumë shpejt, gjë që është forca e Realm dhe pse ajo është e dashur.

(Për qëllimet e këtij artikulli, ky përshkrim do të jetë i mjaftueshëm për ne. Ju mund të lexoni më shumë rreth Realm në qetësi dokumentacionin ose në të tyre akademi).

Shumë zhvillues janë mësuar të punojnë më shumë me bazat e të dhënave relacionale (për shembull, bazat e të dhënave ORM me SQL nën kapuç). Dhe gjëra të tilla si fshirja e të dhënave në kaskadë shpesh duken si të dhëna. Por jo në mbretëri.

Nga rruga, funksioni i fshirjes së kaskadës është kërkuar për një kohë të gjatë. Kjo rishikim и një tjetër, lidhur me të, u diskutua në mënyrë aktive. Kishte një ndjenjë se së shpejti do të bëhej. Por më pas gjithçka u përkthye në futjen e lidhjeve të forta dhe të dobëta, të cilat gjithashtu do ta zgjidhnin automatikisht këtë problem. Ishte mjaft i gjallë dhe aktiv në këtë detyrë kërkesë për tërheqje, e cila tani për tani është ndërprerë për shkak të vështirësive të brendshme.

Rrjedhje e të dhënave pa fshirje në kaskadë

Si rrjedhin saktësisht të dhënat nëse mbështeteni në një fshirje kaskadë jo-ekzistente? Nëse keni të vendosur objekte të Mbretërisë, atëherë ato duhet të fshihen.
Le të shohim një shembull (pothuajse) real. Ne kemi një objekt 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()

Produkti në karrocë ka fusha të ndryshme, duke përfshirë një foto ImageEntity, përbërës të personalizuar CustomizationEntity. Gjithashtu, produkti në karrocë mund të jetë një kombinim me grupin e vet të produkteve RealmList (CartProductEntity). Të gjitha fushat e listuara janë objekte të Mbretërisë. Nëse fusim një objekt të ri (copyToRealm() / copyToRealmOrUpdate()) me të njëjtën ID, atëherë ky objekt do të mbishkruhet plotësisht. Por të gjitha objektet e brendshme (imazhi, personalizimiEntity dhe cartComboProducts) do të humbasin lidhjen me prindin dhe do të mbeten në bazën e të dhënave.

Meqenëse lidhja me ta ka humbur, ne nuk i lexojmë më ose nuk i fshijmë ato (përveç nëse i qasemi në mënyrë të qartë ose pastrojmë të gjithë "tabelën"). Ne e quajtëm këtë "rrjedhje memorie".

Kur punojmë me Realm, duhet të kalojmë në mënyrë eksplicite të gjithë elementët dhe të fshijmë në mënyrë eksplicite gjithçka përpara operacioneve të tilla. Kjo mund të bëhet, për shembull, si kjo:

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

Nëse e bëni këtë, atëherë gjithçka do të funksionojë siç duhet. Në këtë shembull, supozojmë se nuk ka objekte të tjera të mbivendosura të Mbretërisë brenda imazhit, Entitetit të personalizimit dhe cartComboProducts, kështu që nuk ka sythe dhe fshirje të tjera të mbivendosur.

Zgjidhje "e shpejtë".

Gjëja e parë që vendosëm të bënim ishte të pastronim objektet me rritje më të shpejtë dhe të kontrollonim rezultatet për të parë nëse kjo do të zgjidhte problemin tonë origjinal. Së pari, u bë zgjidhja më e thjeshtë dhe më intuitive, domethënë: çdo objekt duhet të jetë përgjegjës për heqjen e fëmijëve të tij. Për ta bërë këtë, ne prezantuam një ndërfaqe që kthente një listë të objekteve të saj të mbivendosur të Mbretërisë:

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

Dhe ne e zbatuam atë në objektet tona të Mbretërisë:

@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 ne i kthejmë të gjithë fëmijët si një listë të sheshtë. Dhe çdo objekt fëmijë mund të zbatojë gjithashtu ndërfaqen NestedEntityAware, duke treguar se ka objekte të brendshme të Mbretërisë për t'u fshirë, për shembull 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
   )
 }
}

Dhe kështu me radhë, foleja e objekteve mund të përsëritet.

Pastaj shkruajmë një metodë që fshin në mënyrë rekursive të gjitha objektet e mbivendosur. Metoda (e bërë si shtesë) deleteAllNestedEntities merr të gjitha objektet dhe metodën e nivelit të lartë deleteNestedRecursively Heq në mënyrë rekursive të gjitha objektet e mbivendosur duke përdorur ndërfaqen 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()
   }
 }
}

Ne e bëmë këtë me objektet me rritje më të shpejtë dhe kontrolluam se çfarë ndodhi.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Si rezultat, ato objekte që mbuluam me këtë zgjidhje ndaluan së rrituri. Dhe rritja e përgjithshme e bazës u ngadalësua, por nuk u ndal.

Zgjidhja "normale".

Megjithëse baza filloi të rritet më ngadalë, ajo përsëri u rrit. Kështu filluam të kërkonim më tej. Projekti ynë përdor shumë aktivisht ruajtjen e të dhënave në Realm. Prandaj, shkrimi i të gjitha objekteve të mbivendosur për çdo objekt kërkon punë intensive, plus rreziku i gabimeve rritet, sepse mund të harroni të specifikoni objektet kur ndryshoni kodin.

Doja të sigurohesha që të mos përdorja ndërfaqe, por që gjithçka të funksiononte më vete.

Kur duam që diçka të funksionojë më vete, duhet të përdorim reflektimin. Për ta bërë këtë, ne mund të kalojmë nëpër secilën fushë të klasës dhe të kontrollojmë nëse është një objekt Realm ose një listë objektesh:

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

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

Nëse fusha është një RealmModel ose RealmList, atëherë shtoni objektin e kësaj fushe në një listë të objekteve të mbivendosur. Gjithçka është saktësisht e njëjtë siç bëmë më lart, vetëm këtu do të bëhet vetë. Vetë metoda e fshirjes së kaskadës është shumë e thjeshtë dhe duket si kjo:

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

Zgjerim filterRealmObject filtron dhe kalon vetëm objektet e mbretërisë. Metoda getNestedRealmObjects përmes reflektimit, gjen të gjitha objektet e mbivendosura të Mbretërisë dhe i vendos ato në një listë lineare. Pastaj ne bëjmë të njëjtën gjë në mënyrë rekursive. Kur fshini, duhet të kontrolloni objektin për vlefshmëri isValid, sepse mund të ndodhë që objekte të ndryshme prind mund të kenë të mbivendosur identikë. Është më mirë ta shmangni këtë dhe thjesht të përdorni gjenerimin automatik të id kur krijoni objekte të reja.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Zbatimi i plotë i metodës 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)
}

Si rezultat, në kodin tonë të klientit ne përdorim "fshirje në kaskadë" për çdo operacion modifikimi të të dhënave. Për shembull, për një operacion insert duket kështu:

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

Metoda e parë getManagedEntities merr të gjitha objektet e shtuara dhe më pas metodën cascadeDelete Fshin në mënyrë rekursive të gjitha objektet e mbledhura përpara se të shkruani të reja. Ne përfundojmë duke përdorur këtë qasje gjatë gjithë aplikacionit. Rrjedhjet e kujtesës në Realm janë zhdukur plotësisht. Pasi kemi kryer të njëjtën matje të varësisë së kohës së fillimit nga numri i fillimeve të ftohta të aplikacionit, ne shohim rezultatin.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Vija e gjelbër tregon varësinë e kohës së fillimit të aplikacionit nga numri i fillimeve të ftohta gjatë fshirjes automatike të kaskadës së objekteve të mbivendosur.

Rezultatet dhe përfundimet

Baza e të dhënave gjithnjë në rritje e Realm po bënte që aplikacioni të niste shumë ngadalë. Ne lëshuam një përditësim me "fshirjen në kaskadë" tonë të objekteve të mbivendosur. Dhe tani ne monitorojmë dhe vlerësojmë se si vendimi ynë ndikoi në kohën e nisjes së aplikacionit përmes metrikës _app_start.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Për analizë, marrim një periudhë kohore prej 90 ditësh dhe shohim: koha e nisjes së aplikacionit, si mesatarja ashtu edhe ajo që bie në përqindjen e 95-të të përdoruesve, filloi të ulet dhe nuk rritet më.

Historia se si fshirja në kaskadë në Realm fitoi gjatë një nisje të gjatë

Nëse shikoni grafikun shtatëditor, metrika _app_start duket plotësisht e përshtatshme dhe është më pak se 1 sekondë.

Vlen gjithashtu të shtohet se si parazgjedhje, Firebase dërgon njoftime nëse vlera mesatare e _app_start kalon 5 sekonda. Sidoqoftë, siç mund ta shohim, nuk duhet të mbështeteni në këtë, por përkundrazi të hyni dhe ta kontrolloni atë në mënyrë eksplicite.

E veçanta e bazës së të dhënave Realm është se ajo është një bazë të dhënash jo relacionale. Megjithë lehtësinë e përdorimit, ngjashmërinë me zgjidhjet ORM dhe lidhjen e objekteve, ai nuk ka fshirje në kaskadë.

Nëse kjo nuk merret parasysh, atëherë objektet e mbivendosur do të grumbullohen dhe "rrjedhin". Baza e të dhënave do të rritet vazhdimisht, gjë që nga ana tjetër do të ndikojë në ngadalësimin ose fillimin e aplikacionit.

Unë ndava përvojën tonë se si të bëjmë shpejt një fshirje në kaskadë të objekteve në Realm, i cili nuk është ende jashtë kutisë, por është folur për një kohë të gjatë thonë ata и thonë ata. Në rastin tonë, kjo përshpejtoi shumë kohën e fillimit të aplikacionit.

Megjithë diskutimin rreth shfaqjes së afërt të kësaj veçorie, mungesa e fshirjes së kaskadës në Realm bëhet me dizajn. Nëse jeni duke projektuar një aplikacion të ri, atëherë merrni parasysh këtë. Dhe nëse tashmë po përdorni Realm, kontrolloni nëse keni probleme të tilla.

Burimi: www.habr.com

Shto një koment