Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Erabiltzaile guztiek abiarazte azkarra eta UI erantzuteko aukera ematen dute aplikazio mugikorretan. Aplikazioa abiarazteko denbora luzea hartzen badu, erabiltzailea triste eta haserre sentitzen hasten da. Bezeroaren esperientzia erraz honda dezakezu edo erabiltzailea erabat galdu dezakezu aplikazioa erabiltzen hasi aurretik ere.

Behin aurkitu genuen Dodo Pizza aplikazioak batez beste 3 segundo behar dituela abiarazteko, eta "zortedunak" batzuentzat 15-20 segundo behar dituela.

Ebakiaren azpian amaiera zoriontsua duen istorio bat dago: Erreinuaren datu-basearen hazkuntzari buruz, memoria-ihes bati buruz, nola metatutako objektuak metatu genituen, eta, ondoren, elkarrekin bildu eta dena konpondu genuen.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa
Artikuluaren egilea: Maxim Kachinkin β€” Dodo Pizzako Android garatzailea.

Aplikazioaren ikonoan klik egiten duzunetik hiru segundo lehen jardueraren onResume() arte infinitua da. Eta erabiltzaile batzuentzat, abiarazteko denbora 15-20 segundora iritsi zen. Nola da posible hau ere?

Laburpen oso laburra irakurtzeko astirik ez dutenentzat
Gure Erreinuko datu-basea etengabe hazi zen. Habiaraturiko objektu batzuk ez ziren ezabatu, baina etengabe metatzen ziren. Aplikazioa abiarazteko denbora pixkanaka handitu zen. Ondoren konpondu genuen, eta abiarazte-denbora iritsi zen helburura - segundo 1 baino gutxiago bihurtu zen eta ez zen handitu. Artikuluak egoeraren analisia eta bi irtenbide biltzen ditu: azkarra eta normala.

Arazoaren bilaketa eta azterketa

Gaur egun, mugikorretarako edozein aplikazio azkar abiarazi behar da eta erantzunkorra izan behar du. Baina ez da mugikorretarako aplikazioari buruz bakarrik. Erabiltzaileen esperientzia zerbitzu batekin eta enpresa batekin elkarreraginean gauza konplexua da. Adibidez, gure kasuan, entrega-abiadura pizza zerbitzuaren adierazle nagusietako bat da. Bidalketa azkarra bada, pizza beroa izango da, eta orain jan nahi duen bezeroak ez du luzaroan itxaron beharko. Aplikaziorako, berriz, garrantzitsua da zerbitzu azkarraren sentsazioa sortzea, izan ere, aplikazioak 20 segundo baino ez baditu abiarazteko, zenbat denbora itxaron beharko duzu pizzarako?

Hasieran, gu geu izan ginen batzuetan aplikazioak segundo pare bat behar zituela abiarazteko, eta gero beste lankideen kexak entzuten hasi ginen zenbat denbora behar zuen. Baina ezin izan dugu egoera hau etengabe errepikatu.

Zenbat denbora da? Ren arabera Google dokumentazioa, aplikazio baten hotz abiarazteak 5 segundo baino gutxiago behar baditu, "normala balitz bezala" hartzen da. Dodo Pizza Android aplikazioa abiarazi da (Firebase-ren neurketen arabera _aplikazioa_hasi) orduetan hasiera hotza batez beste 3 segundotan - "Ez handia, ez izugarria", esaten duten moduan.

Baina gero kexak agertzen hasi ziren aplikazioa abiarazteko oso, oso, oso denbora luzea izan zelako! Hasteko, β€œoso, oso, oso luzea” zer den neurtzea erabaki dugu. Eta Firebase traza erabili dugu horretarako Aplikazioaren hasierako traza.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Traza estandar honek erabiltzaileak aplikazioa irekitzen duen unetik lehen jardueraren onResume() exekutatzen den unera arteko denbora neurtzen du. Firebase kontsolan metrika honi _app_start deitzen zaio. Hori atera zen:

  • 95. pertzentiletik gorako erabiltzaileen abiarazte-denborak ia 20 segundokoak dira (batzuk are luzeagoak), abiarazte hotzaren mediana 5 segundo baino txikiagoa izan arren.
  • Abiatzeko denbora ez da balio konstante bat, denborarekin hazten da. Baina batzuetan tantoak daude. Eredu hau analisi-eskala 90 egunera handitu genuenean aurkitu genuen.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Bi pentsamendu etorri zitzaizkidan burura:

  1. Zerbait isurtzen ari da.
  2. "Zerbait" hau askatu ondoren berrezartzen da eta berriro filtratzen da.

Β«Seguruenik datu-basearekin zerbaitΒ», pentsatu genuen, eta arrazoi geneukan. Lehenik eta behin, datu-basea cache gisa erabiltzen dugu; migrazioan garbitzen dugu. Bigarrenik, datu-basea kargatzen da aplikazioa abiaraztean. Dena bat egiten du.

Zer gertatzen da Erreinuko datu-basearekin

Datu-basearen edukiak aplikazioaren bizitzan zehar nola aldatzen diren egiaztatzen hasi ginen, lehen instalaziotik eta erabilera aktiboan aurrerago. Erreinuko datu-basearen edukia ikus dezakezu honen bidez Estetoa edo zehatzago eta argi eta garbi fitxategia irekiz bidez Erreinuko estudioa. Datu-basearen edukia ADB bidez ikusteko, kopiatu Realm datu-basearen fitxategia:

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

Datu-basearen edukia une ezberdinetan begiratuta, mota jakin bateko objektu kopurua etengabe hazten ari dela jakin dugu.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa
Irudiak Realm Studio-ren zati bat erakusten du bi fitxategietarako: ezkerrean - aplikazioaren oinarria instalatu eta denbora gutxira, eskuinaldean - erabilera aktiboaren ondoren. Objektu kopurua dela ikus daiteke ImageEntity ΠΈ MoneyType nabarmen hazi da (pantaila-argazkiak mota bakoitzeko objektu kopurua erakusten du).

Datu-basearen hazkundearen eta abiarazte-denboraren arteko erlazioa

Kontrolik gabeko datu-basearen hazkundea oso txarra da. Baina nola eragiten dio horrek aplikazioa abiarazteko denboran? Nahiko erraza da hau neurtzea Activity Manager-en bidez. Android 4.4tik aurrera, logcat-ek erregistroa bistaratzen du Bistaratu katearekin eta orduarekin. Denbora hori aplikazioa abiarazten den unetik jardueraren errendaketaren amaierara arteko tartearen berdina da. Denbora horretan, gertaera hauek gertatzen dira:

  • Hasi prozesua.
  • Objektuen hasieratzea.
  • Jarduerak sortzea eta hasieratzea.
  • Diseinu bat sortzea.
  • Aplikazioa errendatzea.

Egokitzen zaigu. ADB -S eta -W markekin exekutatzen baduzu, irteera hedatua lor dezakezu abiarazteko denborarekin:

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

Handik hartzen baduzu grep -i WaitTime denbora, metrika honen bilketa automatiza dezakezu eta emaitzak bisualki begiratu. Beheko grafikoak aplikazioaren abiarazte-denborak aplikazioaren abiarazte hotz-kopuruarekiko duen menpekotasuna erakusten du.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Aldi berean, datu-basearen tamainaren eta hazkundearen arteko erlazioaren izaera bera zegoen, 4 MBtik 15 MBra hazi baitzen. Guztira, denboraren poderioz (hazi hotzaren hazkundearekin), bai aplikazioa abiarazteko denbora eta baita datu-basearen tamaina ere handitu egin da. Hipotesi bat daukagu ​​esku artean. Orain menpekotasuna berrestea besterik ez zen geratzen. Hori dela eta, "filtrazioak" kentzea erabaki genuen eta ea abiarazte horrek azkartuko zuen.

Datu-basearen hazkuntza amaigabearen arrazoiak

"Isurketak" kendu aurretik, merezi du ulertzea zergatik agertu diren lehenik. Horretarako, gogora dezagun zer den Erreinua.

Realm datu-base ez-erlazionala da. Objektuen arteko erlazioak Android-en zenbat ORM datu-base erlazional deskribatzen diren modu antzera deskribatzeko aukera ematen du. Aldi berean, Realm-ek objektuak zuzenean gordetzen ditu memorian eraldaketa eta mapa gutxienekin. Honek diskoko datuak oso azkar irakurtzeko aukera ematen du, hori da Erreinuaren indarra eta zergatik maite den.

(Artikulu honen helburuetarako, deskribapen hau nahikoa izango da guretzat. Erreinuari buruz gehiago irakur dezakezu cool-en dokumentazioa edo beren akademia).

Garatzaile asko ohituta daude datu-base erlazionalekin gehiago lan egitera (adibidez, ORM datu-baseak kaputxa azpian SQL duten). Eta datuen ezabaketa kaskaka bezalako gauzak sarritan emanak dirudite. Baina ez Erreinuan.

Bide batez, kaskada ezabatzeko funtzioa aspalditik eskatu da. Hau berrikuspena ΠΈ beste bat, harekin lotuta, aktiboki eztabaidatu zen. Laster egingo zelako sentsazioa zegoen. Baina gero dena lotura sendoak eta ahulak sartu ziren, eta horrek ere arazo hau automatikoki konponduko zuen. Nahiko bizia eta aktiboa zen zeregin honetan tira eskaera, oraingoz pausatu dena barne zailtasunengatik.

Datu-ihesak kaskadako ezabatu gabe

Zehazki nola isurtzen dira datuak existitzen ez den kaskada-ezabaketa batean oinarritzen bazara? Erreinuko objektuak habiaratuta badituzu, ezabatu egin behar dira.
Ikus dezagun (ia) benetako adibide bat. Objektu bat dugu 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()

Saskian dagoen produktuak eremu desberdinak ditu, argazki bat barne ImageEntity, osagai pertsonalizatuak CustomizationEntity. Gainera, saskian dagoen produktua bere produktuen multzoa duen konbinazioa izan daiteke RealmList (CartProductEntity). Zerrendatutako eremu guztiak Erreinuko objektuak dira. Id berarekin objektu berri bat (copyToRealm() / copyToRealmOrUpdate()) txertatzen badugu, objektu hau guztiz gainidatziko da. Baina barneko objektu guztiek (irudia, pertsonalizazioaEntity eta cartComboProducts) gurasoarekin konexioa galduko dute eta datu-basean geratuko dira.

Haiekiko konexioa galtzen denez, jada ez ditugu irakurtzen edo ezabatzen (espresuki sartzen ez bagara edo "taula" osoa garbitzen ez badugu). Horri "memoria ihesak" deitu genion.

Realm-ekin lan egiten dugunean, esplizituki elementu guztiak zeharkatu eta esplizituki dena ezabatu behar ditugu eragiketen aurretik. Hau egin daiteke, adibidez, honela:

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

Hau egiten baduzu, dena behar bezala funtzionatuko du. Adibide honetan, irudiaren, customizationEntityren eta cartComboProducts-en barruan beste habiaraturiko Erreinuko objekturik ez dagoela suposatuko dugu, beraz, ez dago beste habiaraturiko begiztarik eta ezabatzerik.

Irtenbide "bizkorra".

Erabaki genuen lehenengo gauza hazten ari diren objektuak garbitu eta emaitzak egiaztatzea izan zen honek gure jatorrizko arazoa konponduko ote zuen. Lehenik eta behin, irtenbiderik errazena eta intuitiboena egin zen, hau da: objektu bakoitzak bere seme-alabak kentzeaz arduratu behar du. Horretarako, habiaraturiko Erreinuko objektuen zerrenda itzultzen zuen interfaze bat aurkeztu genuen:

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

Eta gure Erreinuko objektuetan inplementatu genuen:

@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 ume guztiak zerrenda lau gisa itzultzen ditugu. Eta ume-objektu bakoitzak NestedEntityAware interfazea ere inplementatu dezake, ezabatzeko barneko Erreinuko objektuak dituela adieraziz, adibidez. 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
   )
 }
}

Eta abar, objektuen habia errepika daiteke.

Ondoren, habiaraturiko objektu guztiak modu errekurtsiboan ezabatzen dituen metodo bat idazten dugu. Metodoa (luzapen gisa egina) deleteAllNestedEntities goi-mailako objektu eta metodo guztiak lortzen ditu deleteNestedRecursively Errekurtsiboki habiatutako objektu guztiak kentzen ditu NestedEntityAware interfazea erabiliz:

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

Hau azkar hazten diren objektuekin egin genuen eta zer gertatu zen egiaztatu genuen.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Ondorioz, soluzio honekin estali genituen objektu horiek hazteari utzi zioten. Eta oinarriaren hazkunde orokorra moteldu egin zen, baina ez zen gelditu.

Irtenbide "normala".

Oinarria polikiago hazten hasi zen arren, oraindik hazi zen. Beraz, urrunago bilatzen hasi ginen. Gure proiektuak Erreinuko datuen cachearen erabilera oso aktiboa egiten du. Hori dela eta, objektu bakoitzerako habiaraturiko objektu guztiak idaztea lan handia da, gainera erroreak izateko arriskua areagotzen da, kodea aldatzean objektuak zehaztea ahaztu dezakezulako.

Interfazeak erabiltzen ez ditudala ziurtatu nahi nuen, baina dena bere kabuz funtzionatzen zuela.

Zerbait bere kabuz lan egin nahi dugunean, hausnarketa erabili behar dugu. Horretarako, klase-eremu bakoitzean zehar joan eta Erreinuko objektu bat den edo objektuen zerrenda bat den egiaztatu dezakegu:

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

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

Eremua RealmModel edo RealmList bada, gehitu eremu honen objektua habiatutako objektuen zerrenda batera. Dena goian egin genuen berdina da, hemen bakarrik egingo da. Kaskada ezabatzeko metodoa bera oso erraza da eta itxura hau du:

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

Luzapena filterRealmObject Erreinuko objektuak soilik iragazten eta pasatzen ditu. Metodoa getNestedRealmObjects hausnarketaren bidez, habiaraturiko Erreinuko objektu guztiak aurkitzen ditu eta zerrenda lineal batean jartzen ditu. Orduan gauza bera egiten dugu errekurtsiboki. Ezabatzean, objektua baliozkotasuna egiaztatu behar duzu isValid, gerta baitaiteke guraso-objektu ezberdinek habiaratuta berdinak izatea. Hobe da hori saihestea eta besterik gabe id-en sorkuntza automatikoa erabiltzea objektu berriak sortzean.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

getNestedRealmObjects metodoaren inplementazio osoa

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

Ondorioz, gure bezero-kodean "kaskading delete" erabiltzen dugu datuak aldatzeko eragiketa bakoitzerako. Esate baterako, txertatzeko eragiketa baterako itxura hau du:

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

Metodoa lehenik getManagedEntities gehitutako objektu guztiak jasotzen ditu, eta gero metodoa cascadeDelete Errekurtsiboki ezabatzen ditu bildutako objektu guztiak berriak idatzi aurretik. Azkenean, ikuspegi hau aplikazioan zehar erabiltzen dugu. Erreinuko memoria-ihesak guztiz desagertu dira. Aplikazioaren abiarazte hotz kopuruarekiko abiarazte-denboraren menpekotasunaren neurketa bera egin ondoren, emaitza ikusten dugu.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Lerro berdeak aplikazioa abiarazteko denboraren mendekotasuna erakusten du abiarazte hotz kopuruaren arabera habiatutako objektuen kaskada automatikoki ezabatzean.

Emaitzak eta ondorioak

Gero eta handiagoa den Realm datu-baseak aplikazioa oso poliki abiarazten ari zen. Eguneratze bat kaleratu dugu habiaratutako objektuen gure "kaskadako ezabaketa"rekin. Eta orain gure erabakiak aplikazioaren abiarazte-denboran nola eragin duen kontrolatzen eta ebaluatzen dugu _app_start metrikaren bidez.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Aztertzeko, 90 eguneko denbora-tarte bat hartzen dugu eta ikusten dugu: aplikazioa abiarazteko denbora, mediana zein erabiltzaileen 95. pertzentilean kokatzen dena, gutxitzen hasi zen eta ez da gehiago igotzen.

Erreinuko kaskada ezabatzeak abiarazte luze bati irabazi zion nolako istorioa

Zazpi eguneko grafikoari erreparatuz gero, _app_start metrika guztiz egokia dirudi eta segundo 1 baino gutxiagokoa da.

Gainera, merezi du gehitzea modu lehenetsian, Firebase-k jakinarazpenak bidaltzen dituela _app_start-en medianaren balioak 5 segundo gainditzen baditu. Hala ere, ikus dezakegunez, ez duzu horretan fidatu behar, baizik eta sartu eta esplizituki egiaztatu.

Realm datu-basearen berezitasuna datu-base ez-erlazionala dela da. Erabilera erraza, ORM soluzioen antzekotasuna eta objektuen lotura izan arren, ez du kaskada ezabatzerik.

Hori kontuan hartzen ez bada, habiaraturiko objektuak pilatu eta "isuri egingo dira". Datu-basea etengabe haziko da, eta horrek aplikazioaren moteltzeari edo abiarazteari eragingo dio.

Gure esperientzia partekatu nuen Erreinuko objektuen kaskada ezabatzeari buruz, oraindik ez dago kaxatik kanpo, baina denbora luzez hitz egin dena. esaten dute ΠΈ esaten dute. Gure kasuan, horrek asko azkartu zuen aplikazioa abiarazteko denbora.

Ezaugarri honen berehalako agerpenari buruz eztabaidatu arren, Erreinuko kaskada ezabatzea diseinuaren arabera egiten da. Aplikazio berri bat diseinatzen ari bazara, kontuan hartu hau. Eta Realm erabiltzen ari bazara, egiaztatu horrelako arazorik baduzu.

Iturria: www.habr.com

Gehitu iruzkin berria