Сите корисници го земаат здраво за готово брзото стартување и одговорниот интерфејс во мобилните апликации. Ако апликацијата трае долго време за да се стартува, корисникот почнува да се чувствува тажен и лут. Можете лесно да го расипете искуството на клиентите или целосно да го изгубите корисникот дури и пред да почне да ја користи апликацијата.
Еднаш откривме дека апликацијата Dodo Pizza во просек трае 3 секунди, а на некои „среќници“ им се потребни 15-20 секунди.
Под сечењето е приказна со среќен крај: за растот на базата на податоци на Realm, истекување на меморијата, како акумулиравме вгнездени предмети, а потоа се собравме и поправивме сè.

Автор на статијата: — Андроид развивач во Dodo Pizza.
Три секунди од кликнување на иконата на апликацијата до onResume() од првата активност е бесконечност. И за некои корисници, времето за стартување достигна 15-20 секунди. Како е воопшто можно ова?
Многу кратко резиме за оние кои немаат време да читаат
Нашата база на податоци на Кралството растеше бескрајно. Некои вгнездени објекти не беа избришани, туку постојано се акумулираа. Времето за стартување на апликацијата постепено се зголемуваше. Потоа го поправивме, а времето за стартување дојде до целта - стана помалку од 1 секунда и повеќе не се зголемуваше. Статијата содржи анализа на ситуацијата и две решенија - брзо и нормално.
Пребарување и анализа на проблемот
Денес, секоја мобилна апликација мора брзо да се стартува и да реагира. Но, не се работи само за мобилната апликација. Корисничкото искуство за интеракција со услуга и компанија е сложена работа. На пример, во нашиот случај, брзината на испорака е еден од клучните показатели за услугата на пица. Ако доставата е брза, пицата ќе биде топла, а клиентот кој сака да јаде сега нема да мора долго да чека. За апликацијата, пак, важно е да се создаде чувство на брза услуга, бидејќи ако апликацијата трае само 20 секунди за да се стартува, тогаш колку време ќе треба да ја чекате пицата?
Отпрвин, ние самите се соочивме со фактот дека понекогаш апликацијата траеше неколку секунди за да се стартува, а потоа почнавме да слушаме поплаки од други колеги за тоа колку време трае. Но, не можевме постојано да ја повторуваме оваа ситуација.
Колку е долго? Според , ако ладниот почеток на апликацијата трае помалку од 5 секунди, тогаш тоа се смета за „како нормално“. Лансирана апликација за Android Dodo Pizza (според метриката на Firebase ) во во просек за 3 секунди - „Не одлично, не страшно“, како што велат.
Но, тогаш почнаа да се појавуваат поплаки дека апликацијата траеше многу, многу, многу долго за да се стартува! За почеток, решивме да измериме што е „многу, многу, многу долго“. И ние користевме Firebase trace за ова .

Оваа стандардна трага го мери времето помеѓу моментот кога корисникот ја отвора апликацијата и моментот кога се извршува onResume() од првата активност. Во конзолата Firebase оваа метрика се нарекува _app_start. Се покажа дека:
- Времето на стартување за корисниците над 95-от перцентил е скоро 20 секунди (некои и подолго), и покрај тоа што просечното време на ладно стартување е помало од 5 секунди.
- Времето на стартување не е константна вредност, туку расте со текот на времето. Но, понекогаш има капки. Оваа шема ја најдовме кога ја зголемивме скалата на анализа на 90 дена.

Две мисли ми паднаа на ум:
- Нешто протекува.
- Ова „нешто“ се ресетира по ослободувањето и потоа повторно протекува.
„Веројатно нешто со базата на податоци“, си помисливме и бевме во право. Прво, ја користиме базата на податоци како кеш за време на миграцијата, ја бришеме. Второ, базата на податоци се вчитува кога ќе започне апликацијата. Сè се вклопува заедно.
Што не е во ред со базата на податоци на Realm
Почнавме да проверуваме како се менува содржината на базата на податоци во текот на животниот век на апликацијата, од првата инсталација и понатаму за време на активното користење. Можете да ја видите содржината на базата на податоци на Realm преку или подетално и јасно со отворање на датотеката преку . За да ја видите содржината на базата на податоци преку ADB, копирајте ја датотеката со базата на податоци на Realm:
adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}Откако ја разгледавме содржината на базата на податоци во различни периоди, откривме дека бројот на објекти од одреден тип постојано се зголемува.

Сликата покажува фрагмент од Realm Studio за две датотеки: лево - базата на апликацијата некое време по инсталацијата, десно - по активна употреба. Се гледа дека бројот на предмети ImageEntity и MoneyType значително порасна (на екранот е прикажан бројот на објекти од секој тип).
Врска помеѓу растот на базата на податоци и времето на стартување
Неконтролираното растење на базата на податоци е многу лошо. Но, како тоа влијае на времето за стартување на апликацијата? Сосема е лесно да се измери ова преку ActivityManager. Од Android 4.4, logcat го прикажува дневникот со низата Прикажани и времето. Ова време е еднакво на интервалот од моментот на стартување на апликацијата до крајот на прикажувањето на активноста. Во тоа време се случуваат следниве настани:
- Започнете го процесот.
- Иницијализирање на објекти.
- Креирање и иницијализација на активности.
- Креирање распоред.
- Рендерирање на апликацијата.
Ни одговара. Ако користите ADB со знаменцата -S и -W, можете да добиете продолжен излез со времето на стартување:
adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN Ако го грабнеш од таму grep -i WaitTime време, можете да го автоматизирате собирањето на оваа метрика и визуелно да ги погледнете резултатите. Графиконот подолу ја покажува зависноста на времето за стартување на апликацијата од бројот на ладни стартувања на апликацијата.

Во исто време, имаше иста природа на врската помеѓу големината и растот на базата на податоци, која порасна од 4 MB на 15 MB. Севкупно, излегува дека со текот на времето (со растот на студените стартови), се зголемија и времето за лансирање на апликацијата и големината на базата на податоци. Имаме хипотеза на нашите раце. Сега остана само да се потврди зависноста. Затоа, решивме да ги отстраниме „протекувањето“ и да видиме дали тоа ќе го забрза лансирањето.
Причини за бесконечен раст на базата на податоци
Пред да се отстранат „протекувањето“, вреди да се разбере зошто тие се појавија на прво место. За да го направите ова, да се потсетиме што е Кралството.
Realm е не-релациона база на податоци. Тоа ви овозможува да ги опишете односите помеѓу објектите на сличен начин како и колку ORM релациони бази на податоци на Android се опишани. Во исто време, Realm складира објекти директно во меморијата со најмалку трансформации и мапирања. Ова ви овозможува многу брзо да читате податоци од дискот, што е силата на Realm и зошто е сакан.
(За целите на оваа статија, овој опис ќе ни биде доволен. Можете да прочитате повеќе за Кралството на кул или во нивните ).
Многу програмери се навикнати да работат повеќе со релациони бази на податоци (на пример, ORM бази на податоци со SQL под капакот). И нештата како каскадно бришење податоци често изгледаат како дадени. Но, не во Кралството.
Патем, функцијата за каскадно бришење е побарана долго време. Ова и , поврзан со него, активно се дискутираше. Имаше чувство дека тоа наскоро ќе биде направено. Но, тогаш сè се претвори во воведување на силни и слаби врски, кои исто така автоматски би го решиле овој проблем. Беше доста жив и активен на оваа задача , кој засега е паузиран поради внатрешни потешкотии.
Протекување податоци без каскадно бришење
Како точно протекуваат податоци ако се потпирате на непостоечко каскадно бришење? Ако сте вгнездени објекти на Кралството, тогаш тие мора да се избришат.
Ајде да погледнеме (речиси) вистински пример. Имаме објект 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() Производот во количката има различни полиња, вклучувајќи слика ImageEntity, прилагодени состојки CustomizationEntity. Исто така, производот во количката може да биде комбинација со сопствен сет на производи RealmList (CartProductEntity). Сите наведени полиња се објекти на Кралството. Ако вметнеме нов објект (copyToRealm() / copyToRealmOrUpdate()) со истиот id, тогаш овој објект ќе биде целосно препишан. Но, сите внатрешни објекти (слика, прилагодувањеEntity и cartComboProducts) ќе ја изгубат врската со родителот и ќе останат во базата на податоци.
Бидејќи врската со нив е изгубена, ние веќе не ги читаме или бришеме (освен ако експлицитно не им пристапиме или не ја исчистиме целата „табела“). Ова го нарековме „протекување на меморијата“.
Кога работиме со Realm, мора експлицитно да ги поминеме сите елементи и експлицитно да избришеме сè пред таквите операции. Ова може да се направи, на пример, вака:
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()
}
// и потом уже сохраняемАко го направите ова, тогаш сè ќе функционира како што треба. Во овој пример, претпоставуваме дека нема други вгнездени објекти на Кралството во сликата, приспособување Ентите и cartComboProducts, така што нема други вгнездени јамки и бришења.
„Брзо“ решение
Првото нешто што решивме да го направиме е да ги исчистиме објектите кои најбрзо растат и да ги провериме резултатите за да видиме дали ова ќе го реши нашиот првичен проблем. Прво, беше направено наједноставното и најинтуитивното решение, имено: секој предмет треба да биде одговорен за отстранување на своите деца. За да го направите ова, воведовме интерфејс кој враќа список на неговите вгнездени објекти на Кралството:
interface NestedEntityAware {
fun getNestedEntities(): Collection<RealmObject?>
}И ние го имплементиравме во нашите објекти во Кралството:
@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 ги враќаме сите деца како рамен список. И секој дете објект исто така може да го имплементира интерфејсот NestedEntityAware, што покажува дека има внатрешни објекти на Realm за бришење, на пример 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
)
}
}И така натаму, гнездење на предмети може да се повтори.
Потоа пишуваме метод кој рекурзивно ги брише сите вгнездени објекти. Метод (направен како продолжение) deleteAllNestedEntities ги добива сите објекти и метод на највисоко ниво deleteNestedRecursively Рекурзивно ги отстранува сите вгнездени објекти користејќи го интерфејсот 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()
}
}
}Го направивме ова со објектите кои најбрзо растат и проверивме што се случило.

Како резултат на тоа, оние предмети што ги покривавме со ова решение престанаа да растат. И целокупниот раст на основата забави, но не запре.
„Нормалното“ решение
Иако основата почна да расте побавно, таа сепак растеше. Така почнавме да бараме понатаму. Нашиот проект многу активно го користи кеширањето на податоци во Realm. Затоа, пишувањето на сите вгнездени објекти за секој објект е трудоинтензивно, плус ризикот од грешки се зголемува, бидејќи можете да заборавите да наведете објекти при менување на кодот.
Сакав да се уверам дека не користам интерфејси, туку дека сè работи само по себе.
Кога сакаме нешто да работи само по себе, мораме да користиме размислување. За да го направите ова, можеме да поминеме низ секое поле за класа и да провериме дали е објект на Realm или листа на објекти:
RealmModel::class.java.isAssignableFrom(field.type)
RealmList::class.java.isAssignableFrom(field.type)Ако полето е RealmModel или RealmList, тогаш додајте го објектот од ова поле во список со вгнездени објекти. Сè е точно исто како што направивме погоре, само овде тоа ќе се направи само по себе. Самиот метод на каскадно бришење е многу едноставен и изгледа вака:
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()
}
}
}
}
Продолжување filterRealmObject филтрира и минува само објекти на Кралството. Метод getNestedRealmObjects преку рефлексија, ги наоѓа сите вгнездени објекти на Кралството и ги става во линеарна листа. Потоа го правиме истото рекурзивно. Кога бришете, треба да го проверите предметот за валидност isValid, бидејќи може да се случи различни родителски објекти да имаат вгнездени идентични. Подобро е да се избегне ова и едноставно да се користи автоматско генерирање на идентификација при креирање на нови објекти.

Целосна имплементација на методот 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)
}
Како резултат на тоа, во кодот на нашиот клиент користиме „каскадно бришење“ за секоја операција за модификација на податоците. На пример, за операција за вметнување изгледа вака:
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 }
))
} Прво метод getManagedEntities ги прима сите додадени објекти, а потоа методот cascadeDelete Рекурзивно ги брише сите собрани објекти пред да напише нови. На крајот го користиме овој пристап низ целата апликација. Протекувањето на меморијата во Realm целосно исчезна. Откако го извршивме истото мерење на зависноста на времето на стартување од бројот на ладни стартови на апликацијата, го гледаме резултатот.

Зелената линија ја покажува зависноста на времето за стартување на апликацијата од бројот на ладни стартувања при автоматско каскадно бришење на вгнездени објекти.
Резултати и заклучоци
Постојано растечката база на податоци на Realm предизвикуваше апликацијата да се стартува многу бавно. Издадовме ажурирање со наше „каскадно бришење“ на вгнездени објекти. И сега следиме и оценуваме како нашата одлука влијаеше на времето за стартување на апликацијата преку метриката _app_start.

За анализа, земаме временски период од 90 дена и гледаме: времето за лансирање на апликацијата, и средното и она што паѓа на 95-от перцентил на корисници, почна да се намалува и повеќе не се зголемува.

Ако ја погледнете табелата од седум дена, метриката _app_start изгледа сосема соодветна и е помала од 1 секунда.
Исто така, вреди да се додаде дека стандардно, Firebase испраќа известувања ако средната вредност на _app_start надмине 5 секунди. Сепак, како што можеме да видиме, не треба да се потпрете на ова, туку да влезете и да го проверите експлицитно.
Посебната работа за базата на податоци на Realm е тоа што таа е не-релациона база на податоци. И покрај неговата леснотија на користење, сличноста со ORM решенијата и поврзувањето на објекти, нема каскадно бришење.
Ако ова не се земе предвид, тогаш вгнездените предмети ќе се акумулираат и ќе „истекуваат“. Базата на податоци постојано ќе расте, што пак ќе влијае на забавување или стартување на апликацијата.
Го споделив нашето искуство за тоа како брзо да се направи каскадно бришење на објекти во Realm, за кое сè уште не е излезено, но се зборува долго време и . Во нашиот случај, ова во голема мера го забрза времето за стартување на апликацијата.
И покрај дискусијата за претстојното појавување на оваа функција, отсуството на каскадно бришење во Realm е направено со дизајн. Ако дизајнирате нова апликација, тогаш земете го предвид ова. И ако веќе користите Realm, проверете дали имате такви проблеми.
Извор: www.habr.com

