Depois descobrimos que o aplicativo Dodo Pizza leva em média 3 segundos para iniciar, e alguns "sortudos" levam de 15 a 20 segundos.
Abaixo do corte está uma história com um final feliz: sobre o crescimento do banco de dados do Realm, vazamentos de memória, como salvamos objetos aninhados e, em seguida, nos reunimos e consertamos tudo.

![]()
O autor do artigo: Maxim Kachinkin é um desenvolvedor Android da Dodo Pizza.
Três segundos de um clique no ícone do aplicativo para o onResume () da primeira atividade é infinito. E para alguns usuários, o tempo de inicialização atingiu 15-20 segundos. Como isso é possível?
Um resumo muito curto para quem não tem tempo para ler
Realm. , . . , — 1 . — - -.
Pesquisa e análise do problema
Hoje, qualquer aplicativo móvel deve ser iniciado rapidamente e responsivo. Mas não é apenas o aplicativo móvel. A experiência do usuário de interagir com um serviço e uma empresa é algo complexo. Por exemplo, em nosso caso, a velocidade de entrega é um dos indicadores-chave para um serviço de pizza. Se a entrega for rápida, a pizza estará quente e o cliente que quiser comer agora não terá que esperar muito. Para o aplicativo, por sua vez, é importante criar a sensação de um atendimento rápido, pois se o aplicativo iniciar apenas 20 segundos, quanto tempo demorará para uma pizza?
No início, nós próprios nos deparamos com o fato de que às vezes o aplicativo é iniciado por alguns segundos, e depois começaram a chegar reclamações de outros colegas de que era “longo”. Mas não conseguimos repetir esta situação de forma estável.
Quanto tempo? De acordo comDocumentação do Google , se uma inicialização a frio de um aplicativo leva menos de 5 segundos, ela é considerada "normal". O aplicativo Dodo Pizza Android foi lançado (de acordo com a métrica _app_start do Firebase ) em uma inicialização a frio em uma média de 3 segundos - "Não é ótimo, não é terrível", como dizem.
Mas então começaram a surgir reclamações de que o aplicativo foi lançado por muito, muito, muito tempo! Para começar, decidimos medir o que é "muito, muito, muito longo". E usamos o rastreamento de início do aplicativo Firebase trace para isso .

Este rastreio padrão mede o tempo entre o momento em que o usuário abre o aplicativo e o momento em que o onResume () da primeira ativação é executado. O Firebase console chama essa métrica de _app_start. Descobriu-se que:
- Os usuários acima do percentil 95 têm um tempo de inicialização de quase 20 segundos (alguns têm mais), apesar de um tempo médio de inicialização a frio de menos de 5 segundos.
- O tempo de inicialização não é constante, mas cresce com o tempo. Mas às vezes são observadas quedas. Encontramos esse padrão quando aumentamos a escala de análise para 90 dias.

Dois pensamentos me vieram à mente:
- Algo está vazando.
- Este “algo” é descartado após a liberação e vaza novamente.
“Provavelmente algo com o banco de dados”, pensamos, e estávamos certos. Em primeiro lugar, usamos o banco de dados como cache e o limpamos durante a migração. Em segundo lugar, o banco de dados é carregado quando o aplicativo é iniciado. Tudo se encaixa.
O que há de errado com o banco de dados Realm
Começamos a verificar como o conteúdo do banco de dados muda ao longo do tempo de vida do aplicativo, desde a primeira instalação e posteriormente no processo de uso ativo. Você pode visualizar o conteúdo do banco de dados Realm através do Stetho ou com mais detalhes e visualmente abrindo o arquivo através do Realm Studio . Para visualizar o conteúdo do banco de dados via ADB, copie o arquivo de banco de dados Realm:
adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}
Tendo examinado o conteúdo do banco de dados em momentos diferentes, descobrimos que o número de objetos de um determinado tipo está aumentando constantemente.

A imagem mostra um fragmento do Realm Studio para dois arquivos: à esquerda - o banco de dados do aplicativo após algum tempo após a instalação, à direita - após o uso ativo. Pode-se ver que o número de objetos
ImageEntity
e MoneyType
tem crescido significativamente (a imagem mostra o número de objetos de cada tipo).
Relação de crescimento do banco de dados com tempos de inicialização
O crescimento descontrolado do banco de dados é muito ruim. Mas como isso afeta o tempo de inicialização do aplicativo? É muito fácil medi-lo por meio do ActivityManager. A partir do Android 4.4, o logcat exibe um log com a string exibida e a hora. Esse tempo é igual ao intervalo desde o momento em que o aplicativo foi iniciado até o final da renderização da atividade. Durante este tempo, os eventos ocorrem:
- Iniciando o processo.
- Inicialização do objeto.
- Criação e inicialização da atividade.
- Criação de layout.
- Renderização de aplicativos.
Adequado para nós. Se você executar o ADB com os sinalizadores -S e -W, poderá obter uma saída estendida com o horário de início:
adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN
Se você reunir
grep -i WaitTime
tempo a partir daí , pode automatizar a coleta dessa métrica e ver os resultados graficamente. O gráfico a seguir mostra a dependência do tempo de inicialização do aplicativo no número de inicializações a frio do aplicativo.

Ao mesmo tempo, a dependência do tamanho e do crescimento da base era a mesma, que passou de 4 MB para 15 MB. No total, verifica-se que com o tempo (com o crescimento das inicializações a frio), tanto o tempo de inicialização do aplicativo quanto o tamanho do banco de dados aumentaram. Temos uma hipótese em nossas mãos. Agora só faltava confirmar a dependência. Portanto, decidimos remover os "vazamentos" e ver se isso vai acelerar o lançamento.
Razões para o crescimento infinito do banco de dados
Antes de remover os "vazamentos", vale a pena entender por que eles apareceram. Para fazer isso, vamos lembrar o que é Realm.
O Realm é um banco de dados não relacional. Ele permite que você descreva relacionamentos entre objetos de uma maneira semelhante à que muitos bancos de dados relacionais ORM descrevem no Android. Ao mesmo tempo, o Realm salva objetos diretamente na memória com o menor número de transformações e mapeamentos. Isso permite que você leia os dados do disco muito rapidamente, o que é um ponto forte do Realm e pelo qual é apreciado.
(Para os fins deste artigo, esta descrição será suficiente para nós. Você pode ler mais sobre o Realm na documentação legal ou em sua academia ).
Muitos desenvolvedores estão acostumados a trabalhar mais com bancos de dados relacionais (por exemplo, bancos de dados ORM com SQL sob o capô). E coisas como exclusão de dados em cascata muitas vezes parecem uma coisa natural. Mas não no Reino.
A propósito, o recurso de exclusão em cascata foi solicitado há muito tempo. Esta revisão e outra relacionada a ela foram ativamente discutidas. Havia a sensação de que logo seria feito. Mas então tudo se transformou na introdução de elos fortes e fracos, o que também resolveria automaticamente esse problema. Para esta tarefa, houve uma solicitação de pull bastante animada e ativa , que foi pausada por enquanto devido a dificuldades internas.
Vazamento de dados sem exclusão em cascata
Como exatamente os dados vazam se você espera uma exclusão em cascata inexistente? Se você tiver objetos Realm aninhados, eles deverão ser excluídos.
Considere um exemplo (quase) do mundo real. Temos um objeto
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()
O produto no carrinho possui campos diferentes, incluindo uma imagem
ImageEntity
, ingredientes personalizados CustomizationEntity
. Além disso, o produto na cesta pode ser uma combinação com seu próprio conjunto de produtos RealmList (CartProductEntity)
. Todos os campos listados são objetos de Realm. Se inserirmos um novo objeto (copyToRealm () / copyToRealmOrUpdate ()) com o mesmo id, este objeto será completamente sobrescrito. Mas todos os objetos internos (imagem, customizationEntity e cartComboProducts) perderão a conexão com o pai e permanecerão no banco de dados.
Uma vez que a conexão com eles foi perdida, nós não os lemos mais ou os apagamos (a menos que nos referamos explicitamente a eles ou limpemos toda a “tabela”). Chamamos isso de "vazamentos de memória".
Quando trabalhamos com Realm, devemos passar explicitamente por todos os elementos e deletar explicitamente tudo antes de tais operações. Isso pode ser feito, por exemplo, assim:
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()
}
//
Se você fizer isso, tudo funcionará como deveria. Neste exemplo, supomos que não há outros Realms aninhados dentro da imagem, customizationEntity e cartComboProducts, portanto, não há outros loops e exclusões aninhados.
Solução rápida
Em primeiro lugar, decidimos limpar os objetos de crescimento mais rápido e verificar os resultados - se isso resolverá nosso problema original. Primeiramente, foi feita a solução mais simples e intuitiva, a saber: cada objeto deveria ser responsável por retirar seus filhos depois de si mesmo. Para fazer isso, apresentamos a seguinte interface, que retornou uma lista de seus objetos Realm aninhados:
interface NestedEntityAware {
fun getNestedEntities(): Collection<RealmObject?>
}
E nós o implementamos em nossos objetos de 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
)
}
}
À medida
getNestedEntities
que devolvemos a todas as crianças uma lista plana. E cada objeto filho também pode implementar a interface NestedEntityAware, informando que possui objetos internos de Realm a serem excluídos, por exemplo 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
)
}
}
E assim por diante, o aninhamento de objetos pode ser repetido.
Em seguida, escrevemos um método que remove recursivamente todos os objetos aninhados. O método (feito na forma de uma extensão)
deleteAllNestedEntities
obtém todos os objetos de nível superior e deleteNestedRecursively
remove recursivamente todos os objetos aninhados usando a 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()
}
}
}
Fizemos isso com os objetos de crescimento mais rápido e verificamos o que aconteceu.

Como resultado, os objetos que cobrimos com esta solução pararam de crescer. E o crescimento geral da base desacelerou, mas não parou.
A solução "normal"
A base, embora tenha começado a crescer mais lentamente, ainda estava crescendo. Então, começamos a procurar mais. Em nosso projeto, o cache de dados no Realm é usado de forma muito ativa. Portanto, escrever todos os objetos aninhados para cada objeto é trabalhoso, além do aumento do risco de erro, porque você pode esquecer de especificar os objetos ao alterar o código.
Eu queria ter certeza de não usar interfaces, mas fazer tudo funcionar sozinho.
Quando queremos que algo funcione por conta própria, temos que usar reflexão. Para fazer isso, podemos percorrer cada campo da classe e verificar se é um objeto de Realm ou uma lista de objetos:
RealmModel::class.java.isAssignableFrom(field.type)
RealmList::class.java.isAssignableFrom(field.type)
Se o campo for um RealmModel ou RealmList, adicione o objeto desse campo à lista de objetos aninhados. Tudo está exatamente igual ao que fizemos acima, só que aqui será feito sozinho. O próprio método de exclusão em cascata é muito simples e se parece com isto:
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()
}
}
}
}
A extensão
filterRealmObject
filtra e passa apenas objetos Realm. O método getNestedRealmObjects
encontra todos os objetos Realm aninhados por meio de reflexão e os adiciona a uma lista linear. Então fazemos o mesmo recursivamente. Ao excluir, você precisa verificar a validade do objeto isValid
, porque pode ser que diferentes objetos pais tenham os mesmos objetos aninhados. É melhor evitar isso e apenas usar a geração automática de id ao criar novos objetos.

Implementação completa do método 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)
}
Como resultado, em nosso código de cliente, usamos uma "exclusão em cascata" para cada operação de alteração de dados. Por exemplo, para uma operação de inserção, é assim:
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 }
))
}
Primeiro, o método
getManagedEntities
obtém todos os objetos adicionados e, em seguida, cascadeDelete
remove recursivamente todos os objetos coletados antes de gravar novos. Acabamos usando essa abordagem em todo o aplicativo. Os vazamentos de memória do reino desapareceram completamente. Tendo realizado a mesma medição da dependência do tempo de inicialização do número de arranques a frio da aplicação, vemos o resultado.

A linha verde mostra a dependência do tempo de inicialização do aplicativo no número de inicializações a frio durante a exclusão automática em cascata de objetos aninhados.
Resultados e conclusões
O sempre crescente banco de dados do Realm estava retardando muito o lançamento do aplicativo. Lançamos uma atualização com nossa própria "exclusão em cascata" de objetos aninhados. E agora rastreamos e avaliamos como nossa decisão afetou o tempo de inicialização do aplicativo por meio da métrica _app_start.

Para a análise, tomamos um intervalo de tempo de 90 dias e vemos: o tempo de inicialização do aplicativo, tanto a mediana quanto o que cai no percentil 95 de usuários, começou a diminuir e não aumenta mais.

Se você olhar o gráfico de sete dias, a métrica _app_start parece completamente adequada e tem menos de 1 segundo.
Devemos também adicionar que, por padrão, o Firebase envia notificações se o valor mediano _app_start exceder 5 segundos. No entanto, como podemos ver, você não deve confiar nisso, mas sim entrar e verificar explicitamente.
A peculiaridade do banco de dados Realm é que ele é um banco de dados não relacional. Apesar de seu uso simples, da semelhança de trabalhar com soluções ORM e vincular objetos, não possui uma exclusão em cascata.
Se isso não for levado em consideração, os objetos aninhados se acumularão, "vazarão". O banco de dados crescerá constantemente, o que por sua vez afetará a desaceleração ou a inicialização do aplicativo.
Eu compartilhei nossa experiência, quão rápido uma cascata deleta objetos no Reino, o que não está fora da caixa, mas que teve muito papo e conversa . Em nosso caso, isso acelerou muito o tempo de inicialização do aplicativo.
Apesar da discussão sobre o aparecimento iminente desse recurso, a falta de exclusão em cascata no Realm é feita por design. Considere isso se estiver projetando um novo aplicativo. E se você já estiver usando o Realm - verifique se você tem algum desses problemas.