Como a tela da mensagem VK é renderizada?

O que a VK está fazendo para reduzir os atrasos na renderização? Como exibir uma mensagem muito grande e não matar o UiThread? Como reduzir os atrasos na rolagem no RecyclerView?



Minha experiência é baseada no trabalho de desenhar uma tela de mensagem no aplicativo VK Android, na qual é necessário mostrar uma enorme quantidade de informações com o mínimo de freios na interface do usuário.

Venho programando para Android há quase dez anos, antes era freelancer para PHP / Node.js. Agora - um desenvolvedor sênior do Android VKontakte.

Sob o corte - vídeo e transcrição do meu relatório da conferência Mobius 2019 em Moscou.


O relatório revela três tópicos



Olhe para a tela:


Esta mensagem está em algum lugar em cinco telas. E eles podem muito bem estar conosco (no caso de encaminhar mensagens). Ferramentas padrão não funcionarão mais. Mesmo em um dispositivo superior, tudo pode ficar lento.
Bem, além disso, a própria interface do usuário é bastante diversa:

  • datas e indicadores de carregamento,
  • mensagens de serviço
  • texto (emoji, link, email, hashtags),
  • teclado bot
  • ~ 40 maneiras de exibir anexos,
  • árvore de mensagens encaminhadas.

Surge a pergunta: como tornar o número de defasagens o menor possível? Tanto no caso de mensagens simples, como no de mensagens em massa (case-edge do vídeo acima).


Soluções padrão


RecyclerView e seus complementos


Existem vários complementos para o RecyclerView.

  • setHasFixedSize ( boolean)

Muitas pessoas pensam que esse sinalizador é necessário quando os itens da lista são do mesmo tamanho. Mas, de fato, a julgar pela documentação, o oposto é verdadeiro. É quando o tamanho do RecyclerView é constante e independente dos elementos (aproximadamente, não wrap_content). A configuração do sinalizador ajuda a aumentar levemente a velocidade do RecyclerView para evitar cálculos desnecessários.

  • setNestedScrollingEnabled ( boolean)

Otimização secundária que desativa o suporte ao NestedScroll. Não temos a CollapsingToolbar ou outros recursos, dependendo do NestedScroll nessa tela, para que possamos definir com segurança esse sinalizador como false.

  • setItemViewCacheSize ( cache_size)

Configurando o cache interno do RecyclerView.

Muitas pessoas pensam que a mecânica do RecyclerView é:

  • existe um ViewHolder exibido na tela;
  • existe um RecycledViewPool que armazena o ViewHolder;
  • ViewHolder — RecycledViewPool.

Na prática, tudo é um pouco mais complicado, porque há um cache intermediário entre essas duas coisas. É chamado de ItemViewCache. Qual é a sua essência? Quando o ViewHolder sai da tela, ele não é colocado no RecycledViewPool, mas no cache intermediário (ItemViewCache). Todas as alterações no adaptador se aplicam ao ViewHolder visível e ao ViewHolder dentro do ItemViewCache. E para o ViewHolder dentro do RecycledViewPool, as alterações não são aplicadas.

Através do setItemViewCacheSize, podemos definir o tamanho desse cache intermediário.
Quanto maior, mais rápido o deslocamento ocorrerá em curtas distâncias, mas as operações de atualização levarão mais tempo (devido ao ViewHolder.onBind, etc.).

Como o RecyclerView é implementado e como seu cache é estruturado é um tópico bastante amplo e complexo. Você pode ler um ótimo artigo, onde eles falam em detalhes sobre tudo.

Otimização OnCreate / OnBind


Outra solução clássica é otimizar onCreateViewHolder / onBindViewHolder:

  • layout fácil (tentamos usar o FrameLayout ou o Custom ViewGroup o máximo possível),
  • operações pesadas (links de análise / emoji) são realizadas de forma assíncrona no estágio de carregamento da mensagem,
  • StringBuilder para formatar nome, data, etc.,
  • e outras soluções que reduzem o tempo de trabalho desses métodos.

Acompanhamento Adapter.onFailedToRecyclerView ()




Você tem uma lista na qual alguns elementos (ou parte deles) são animados com alfa. No momento em que o View, estando em processo de animação, sai da tela, ele não vai para o RecycledViewPool. Por quê? O RecycledViewPool vê que o View agora é animado pelo sinalizador View.hasTransientState e simplesmente o ignora. Portanto, na próxima vez que você rolar para cima e para baixo, a foto não será tirada do RecycledViewPool, mas será criada novamente.

A decisão mais correta é que, quando o ViewHolder sai da tela, é necessário cancelar todas as animações.



Se você precisar de um hotfix o mais rápido possível ou se for um desenvolvedor lento, no método onFailedToRecycle você poderá simplesmente retornar sempre true e tudo funcionará, mas eu não recomendaria isso.



Rastreamento de overdraw e profiler


A maneira clássica de detectar problemas é o excesso e o rastreamento do criador de perfil.

Overdraw - o número de redesenhos do pixel: quanto menos camadas e menos redesenhar o pixel, mais rápido. Mas, de acordo com minhas observações, nas realidades modernas, isso não afeta muito o desempenho.



Profiler - também conhecido como Android Monitor, que está no Android Studio. Nele, você pode analisar todos os métodos chamados. Por exemplo, abra mensagens, role para cima e para baixo e veja quais métodos foram chamados e quanto tempo levaram.



Tudo o que está na metade esquerda são as chamadas do sistema Android necessárias para criar / renderizar um View / ViewHolder. Nós não podemos influenciá-los ou precisaremos gastar muito esforço.

A metade direita é o nosso código que é executado no ViewHolder.

O bloco de chamadas no número 1 é uma chamada para expressões regulares: em algum lugar eles ignoraram e esqueceram de transferir a operação para o encadeamento em segundo plano, diminuindo a rolagem em ~ 20%.

Bloco de chamada no número 2 - Fresco, uma biblioteca para exibição de imagens. Em alguns lugares, não é o ideal, mas ainda não está claro o que fazer com esse atraso, mas se conseguirmos resolvê-lo, economizaremos outros ~ 15%.

Ou seja, corrigindo esses problemas, podemos obter um aumento de ~ 35%, o que é bem legal.

Diffiff


Muitos de vocês usam o DiffUtil em sua forma padrão: existem duas listas - chamadas, comparadas e empurradas. Fazer tudo isso no thread principal é um pouco caro, porque a lista pode ser muito grande. Normalmente, o cálculo do DiffUtil é executado em um encadeamento em segundo plano.

ListAdapter e AsyncListDiffer fazem isso por você. O ListAdapter estende o adaptador regular e inicia tudo de forma assíncrona - basta fazer um submitList e todo o cálculo das alterações voa para o encadeamento interno em segundo plano. O ListAdapter pode levar em consideração o caso de atualizações frequentes: se você o chamar três vezes seguidas, ele levará apenas o último resultado.

No próprio DiffUtil, usamos apenas algumas mudanças estruturais - a aparência da mensagem, sua alteração e exclusão. Para alguns dados de troca rápida, isso não é adequado. Por exemplo, quando carregamos uma foto ou reproduzimos áudio. Esses eventos ocorrem com freqüência - várias vezes por segundo e, se você executar o DiffUtil a cada vez, terá muito trabalho extra.

Animações


Era uma vez um quadro de animação - bastante escasso, mas ainda assim. Trabalhamos com ele assim:

view.startAnimation(TranslateAnimation(fromX = 0, toX = 300))

O problema é que o parâmetro getTranslationXX () retornará o mesmo valor antes e depois da animação. Isso ocorre porque a Animação alterou a representação visual, mas não alterou as propriedades físicas.



No Android 3.0, a estrutura do Animator apareceu, o que é mais correto porque alterou a propriedade física específica do objeto.



Mais tarde, o ViewPropertyAnimator apareceu e todos ainda não entendem sua diferença do Animator.

Eu vou explicar Digamos que você precise fazer a tradução na diagonal - mude a Visualização ao longo dos eixos x, y. Muito provavelmente você escreveria um código típico:

val animX = ObjectAnimator.ofFloat(view, “translationX”, 100f)
val animY = ObjectAnimator.ofFloat(view, “translationY”, 200f)
AnimatorSet().apply {
    playTogether(animX, animY)
    start()
}

E você pode torná-lo mais curto:

view.animate().translationX(100f).translationY(200f) 

Ao executar o view.animate (), você inicia implicitamente o ViewPropertyAnimator.

Por que é necessário?

  1. Mais fácil de ler e manter o código.
  2. Animação de operações em lote.

No nosso último caso, alteramos duas propriedades. Quando fazemos isso por meio de animadores, os marcadores de animação serão chamados separadamente para cada Animador. Ou seja, setTranslationX e setTranslationY serão chamados separadamente e o View executará operações de atualização separadamente.

No caso do ViewPropertyAnimator, a alteração ocorre ao mesmo tempo, portanto, há uma economia devido a menos operações e a alteração nas propriedades em si é melhor otimizada.

Você pode conseguir isso com o Animator, mas precisará escrever mais código. Além disso, usando o ViewPropertyAnimator, você pode ter certeza de que as animações serão otimizadas o máximo possível. Por quê? O Android tem um RenderNode (DisplayList). De maneira geral, eles armazenam em cache o resultado do onDraw e o usam ao redesenhar. O ViewPropertyAnimator trabalha diretamente com o RenderNode e aplica animações a ele, evitando chamadas onDraw.

Muitas propriedades do modo de exibição também podem afetar diretamente o RenderNode, mas não todos. Ou seja, ao usar o ViewPropertyAnimator, você garante a utilização da maneira mais eficiente. Se de repente você tiver algum tipo de animação que não pode ser feita com o ViewPropertyAnimator, talvez deva pensar e mudar.

Animações: TransitionManager


Geralmente, as pessoas associam que essa estrutura é usada para passar de uma atividade para outra. De fato, ele pode ser usado de maneira diferente e simplificar bastante a implementação da animação de mudanças estruturais. Suponha que tenhamos uma tela na qual uma mensagem de voz é reproduzida. Nós a fechamos com uma cruz e o dado sobe. Como fazer isso? A animação é bastante complicada: o jogador fecha com alfa, enquanto não se move pela tradução, mas muda de altura. Ao mesmo tempo, nossa lista sobe e também altera a altura.



Se o jogador fizesse parte da lista, a animação seria bem simples. Mas conosco o jogador não é um elemento da lista, mas uma Visão completamente independente.

Talvez começássemos a escrever algum tipo de Animador, depois encontraríamos problemas, falhas, começaríamos a serrar muletas e duplicar o código. E obteria algo como a tela abaixo.



Com o TransitionManager, você pode simplificar tudo:

TransitionManager.beginDelayedTransition(
        viewGroup = <LinearLayoutManager>,
        transition = AutoTransition())
playerView.visibility = View.GONE

Toda animação acontece automaticamente sob o capô. Parece mágica, mas se você se aprofundar e ver como funciona, o TransitionManager simplesmente assina todo o View, captura as alterações em suas propriedades, calcula o diff, cria os animadores necessários ou o ViewPropertyAnimator, quando necessário, e faz tudo da maneira mais eficiente possível. O TransitionManager nos permite fazer animações na seção de mensagens de forma rápida e fácil de implementar.

Soluções personalizadas




Essa é a coisa mais fundamental na qual o desempenho e os problemas a seguir se baseiam. O que fazer quando sua mensagem está em 10 telas? Se você prestar atenção, todos os nossos elementos estão localizados exatamente um debaixo do outro. Se aceitarmos que o ViewHolder não é uma mensagem, mas dezenas de ViewHolders diferentes, tudo se torna muito mais simples.

Não é um problema para nós que a mensagem tenha se tornado 10 telas, porque agora exibimos apenas seis ViewHolders em um exemplo concreto. Temos um layout fácil, o código é mais fácil de manter e não há problemas especiais, exceto um - como fazer isso?



Existem ViewHolders simples - são separadores de data clássicos, Carregar mais e assim por diante. E BaseViewHolder - ViewHolder condicionalmente básico para a mensagem. Ele tem uma implementação básica e várias específicas - TextViewHolder, PhotoViewHolder, AudioViewHolder, ReplyViewHolder e assim por diante. Existem cerca de 70 deles.

O que é o BaseViewHolder responsável?


O BaseViewHolder é o único responsável por desenhar o avatar e a parte desejada do balão, bem como a linha de mensagens encaminhadas - azul à esquerda.



A implementação concreta do conteúdo já é realizada por outros herdeiros BaseViewHolder: TextViewHolder exibe apenas texto, FwdSenderViewHolder - o autor da mensagem encaminhada, AudioMsgViewHolder - a mensagem de voz e assim por diante.



Há um problema: o que fazer com a largura? Imagine uma mensagem em pelo menos duas telas. Não está muito claro qual largura definir, porque metade é visível, metade não é visível (e ainda nem foi criada). Absolutamente tudo não pode ser medido, porque estabelece. Eu tenho que pegar um pouco, infelizmente. Existem casos simples em que a mensagem é muito simples: puramente texto ou voz - em geral, consiste em um item.



Nesse caso, use o wrap_content clássico. Para um caso complexo, quando uma mensagem consiste em várias partes, pegamos e forçamos cada ViewHolder uma largura fixa. Especificamente aqui - 220 dp.



Se o texto for muito curto e a mensagem for encaminhada, haverá um espaço vazio à direita. Não há como escapar disso, porque o desempenho é mais importante. Por vários anos, não houve queixas - talvez alguém tenha notado, mas, em geral, todos se acostumaram.



Existem casos extremos. Se respondermos a uma mensagem com um adesivo, podemos especificar a largura especificamente para esse caso, para que fique mais bonito.

Dividimos em ViewHolders no estágio de carregamento de mensagens: iniciamos o carregamento em segundo plano da mensagem, convertemos em item, eles são exibidos diretamente no ViewHolders.



Global RecycledViewPool


A mecânica de usar nosso messenger é tal que as pessoas não se sentam no mesmo bate-papo, mas andam constantemente entre eles. Na abordagem padrão, quando entramos no chat e o deixamos, o RecycledViewPool (e o ViewHolder nele) são simplesmente destruídos e toda vez que gastamos recursos criando o ViewHolder.

Isso pode ser resolvido pelo RecycledViewPool global:

  • dentro da estrutura do aplicativo, RecycledViewPool vive como um singleton;
  • reutilizado na tela de mensagens quando o usuário caminha entre as telas;
  • definido como RecyclerView.setRecycledViewPool (pool).

Existem armadilhas, é importante lembrar duas coisas:

  • você vai para a tela, clique em voltar, saia. O problema é que os ViewHolders que estavam na tela são jogados fora e não retornam ao pool. Isso é corrigido da seguinte maneira:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • O RecycledViewPool tem limitações: não mais que cinco ViewHolders podem ser armazenados para cada ViewType.

Se 9 TextViews forem exibidos na tela, apenas cinco itens serão retornados ao RecycledViewPool e o restante será jogado fora. Você pode alterar o tamanho do RecycledViewPool:
RecycledViewPool.setMaxRecycledViews (viewType, tamanho)

Mas, de alguma forma, é triste prescrever para cada ViewType com as mãos, porque você pode escrever seu RecycledViewPool, expandindo o padrão e torná-lo NoLimit. Pelo link, você pode baixar a implementação concluída.

DiffUtil nem sempre é útil


Aqui está um caso clássico - faça o download, reproduzindo uma faixa de áudio e uma mensagem de voz. Nesse caso, o DiffUtil chama spam.



Nosso BaseViewHolder possui um método updateUploadProgress abstrato.

abstract class BaseViewHolder : ViewHolder {
    fun updateUploadProgress(attachId: Int, progress: Float)
    …        
}

Para lançar um evento, precisamos ignorar todos os ViewHolder visíveis:

fun onUploadProgress(attachId: Int, progress: Float) {
    forEachActiveViewHolder {
        it.updateUploadProgress(attachId, progress)
    }
}

Esta é uma operação simples, é improvável que tenhamos mais de dez ViewHolder na tela. Tal abordagem não pode ficar em princípio. Como encontrar o ViewHolder visível? Uma implementação ingênua seria algo como isto:

val firstVisiblePosition = <...>
val lastVisiblePosition = <...>
for (i in firstVisiblePosition.. lastVisiblePosition) {
    val viewHolder = recycler.View.findViewHolderForAdapterPosition(i)
    viewHolder.updateUploadProgress(..)
}

Mas há um problema. O cache intermediário que mencionei anteriormente, ItemViewCache, contém ViewHolders ativos que simplesmente não aparecem na tela. O código acima não os afetará. Diretamente, nós também não podemos abordá-los. E então muletas vêm em nosso auxílio. Crie um WeakSet que armazena links para o ViewHolder. Além disso, é suficiente simplesmente ignorarmos esse WeakSet.

class Adapter : RecyclerView.Adapter {
    val activeViewHolders = WeakSet<ViewHolder>()
        
    fun onBindViewHolder(holder: ViewHolder, position: Int) {
        activeViewHolders.add(holder)
    }

    fun onViewRecycled(holder: ViewHolder) {
        activeViewHolders.remove(holder)
    }
}

Sobreposição do ViewHolder


Considere o exemplo de histórias. Anteriormente, se uma pessoa reagia a uma história com um adesivo, a exibíamos da seguinte maneira:



parece muito feio. Eu queria fazer melhor, porque as histórias são um conteúdo brilhante e temos uma pequena praça lá. Mas queríamos obter algo assim:



Há um problema: nossa mensagem é dividida no ViewHolder, eles estão localizados estritamente um no outro, mas aqui eles se sobrepõem. Imediatamente não está claro como resolver isso. Você pode criar outro ViewType "histórico + adesivo" ou "histórico + mensagem de voz". Então, em vez de 70 ViewType, teríamos 140 ... Não, precisamos criar algo mais conveniente.



Uma de suas muletas favoritas no Android vem à mente. Por exemplo, estávamos fazendo algo, mas o Pixel Perfect não converge. Para corrigir isso, você precisa excluir tudo e escrever do zero, mas com preguiça. Como resultado, podemos criar margem = -2dp (negativo), e agora tudo se encaixa. Mas apenas essa abordagem não pode ser usada aqui. Se você definir uma margem negativa, o adesivo se moverá, mas o local ocupado permanecerá vazio. Mas temos ItemDecoration, onde itemOffset podemos criar um número negativo. E funciona! Como resultado, obtemos a sobreposição esperada e, ao mesmo tempo, permanece um paradigma em que cada ViewHolder é amigo.

Uma solução bonita em uma linha.

class OffsetItemDecoration : RecyclerViewItemDecoration() {
    overrride fun getItemOffsets(offset: Rect, …) {
        offset.top = -100dp
    }
}

Idlehandler


Este é um caso com um asterisco, é complexo e nem sempre é necessário na prática, mas é importante saber sobre a existência desse método.

Primeiro, mostrarei como o principal thread do UiThread funciona. O esquema geral: há uma fila de eventos de tarefas na qual as tarefas são definidas por meio de handler.post e um loop infinito que passa por essa fila. Ou seja, o UiThread é apenas enquanto (verdadeiro). Se houver tarefas, nós as executamos; caso contrário, esperamos até que elas apareçam.



Em nossas realidades usuais, Handler é responsável por colocar tarefas na fila, e Looper ignora indefinidamente a fila. Existem tarefas que não são muito importantes para a interface do usuário. Por exemplo, um usuário lê uma mensagem - não é tão importante para nós quando a exibimos na interface do usuário, agora ou após 20 ms. O usuário não notará a diferença. Então, talvez valha a pena executar esta tarefa no thread principal apenas quando estiver livre? Ou seja, seria bom sabermos quando a linha waititNewTask é chamada. Nesse caso, o Looper possui um addIdleHandler que é acionado quando o código task.isEmpty é acionado.

Looper.myQueue (). AddIdleHandler ()

E a implementação mais simples do IdleHandler terá a seguinte aparência:

@AnyThread
class IdleHandler {
    private val handler = Handler(Looper.getMainLooper())

    fun post(task: Runnable) {
        handler.post {
            Looper.myQueue().addIdleHandler {
                task.run()
                return@addIdleHandler false
            }
        }
    }
}

Da mesma forma, você pode medir um início a frio honesto do aplicativo.

Emoji


Usamos nosso emoji personalizado em vez dos do sistema. Aqui está um exemplo de como os emojis pareciam em plataformas diferentes em anos diferentes. Os emoji esquerdo e direito são muito bons, mas no meio ...



Há um segundo problema:



cada linha é o mesmo emoji, mas as emoções que eles reproduzem são diferentes. Eu gosto mais do canto inferior direito, ainda não entendo o que isso significa.

Há uma bicicleta da VKontakte. Em ~ 2014, alteramos ligeiramente um emoji. Talvez alguém se lembre - "Marshmallow" foi. Após sua mudança, um mini-motim começou. Obviamente, ele não alcançou o nível de "retorno ao muro", mas a reação foi bastante interessante. E isso nos diz sobre a importância da interpretação de emoji.

Como os emojis são feitos: temos um grande mapa de bits, onde todos são reunidos em um grande "atlas". Existem vários deles - sob diferentes DPI. E há um EmojiSpan que contém informações: eu desenho emoji "tal e tal", ele está em tal e tal bitmap em tal e tal local (x, y).
E existe um ReplacementSpan que permite exibir algo em vez de texto em Span.
Ou seja, você encontra emoji no texto, envolve-o com o EmojiSpan e o sistema desenha o emoji desejado em vez do emoji do sistema.



Alternativas


Inflar


Alguém pode dizer que, como o inflar é lento, por que não criar um layout com as mãos, evitando o inflamento. E, assim, acelere tudo, evitando o 100500 ViewHolder. É uma ilusão. Antes de fazer algo, vale a pena medi-lo.

O Android tem uma classe Debug, tem startMethodTracing e stopMethodTracing.

Debug.startMethodTracing(“trace»)
inflate(...)
Debug.stopMethodTracing()

Isso nos permitirá coletar informações sobre o tempo de execução de um pedaço de código específico.



E vemos que aqui inflar como tal é até invisível. Um quarto do tempo foi gasto carregando drawable, um quarto estava carregando cores. E apenas em algum lugar na parte etc é o nosso inflar.

Tentei traduzir o layout XML em código e salvei em torno de 0,5 ms. O aumento, de fato, não é o mais impressionante. E o código se tornou muito mais complicado. Ou seja, reescrever não faz muito sentido.

Além disso, na prática, muitos não encontrarão esse problema, porque um inflamento longo geralmente ocorre apenas quando o aplicativo se torna muito grande. Em nosso aplicativo VKontakte, por exemplo, existem aproximadamente 200 a 300 telas diferentes e o carregamento de todos os recursos trava. O que fazer com isso ainda não está claro. Provavelmente, você precisará escrever seu próprio gerenciador de recursos.

Anko


Anko tornou-se obsoleto recentemente. Enfim, Anko não é mágico, mas simples açúcar sintático. Ele traduz tudo para a nova View () condicional da mesma maneira. Portanto, não há benefício da Anko.

Litho / Flutter


Por que eu combinei duas coisas completamente não relacionadas? Porque não se trata de tecnologia, mas da complexidade da migração para ela. Você não pode simplesmente pedir emprestado e mudar para uma nova biblioteca.

Não está claro se isso nos dará um aumento de desempenho. E não teremos novos problemas, porque milhões de pessoas com dispositivos completamente diferentes usam nosso aplicativo a cada minuto (você provavelmente nem ouviu falar de um quarto deles). Além disso, as mensagens são uma base de código muito grande. É impossível reescrever tudo instantaneamente. E fazê-lo por causa do hype da tecnologia é estúpido. Especialmente quando o Jetpack Compose aparece em algum lugar distante.

Jetpack compor


O Google nos promete um maná do céu na forma desta biblioteca, mas ainda está em alfa. E quando será no lançamento - não está claro. Também não é claro se podemos obtê-lo em sua forma atual. É muito cedo para experimentar. Deixe sair estável, deixe os principais erros fecharem. E só então vamos olhar na direção dele.

Uma visualização personalizada grande


Há outra abordagem sobre a qual aqueles que usam vários mensageiros instantâneos falam: "pegue e escreva uma grande Visualização Personalizada, sem hierarquia complicada".

Quais são as desvantagens?

  • É difícil de manter.
  • Não faz sentido nas realidades atuais.

Com o Android 4.3, o sistema de cache interno do View foi bombeado. Por exemplo, onMeasure não será chamado se a Visualização não tiver sido alterada. E os resultados da medição anterior são usados.

Com o Android 4.3-4.4, um RenderNode (DisplayList) apareceu, fazendo o cache da renderização. Vejamos um exemplo. Suponha que haja uma célula na lista de diálogos: avatar, título, legenda, status de leitura, hora, outro avatar. Condicionalmente - 10 elementos. E escrevemos o modo de exibição personalizado. Nesse caso, ao alterar uma propriedade, mediremos novamente todos os elementos. Ou seja, basta gastar os recursos extras. No caso do ViewGroup, em que cada elemento é uma View separada, quando você altera uma View, invalidaremos apenas uma View (exceto quando essa View afeta o tamanho de outras).

Sumário


Então, você aprendeu que usamos o RecyclerView clássico com otimizações padrão. Há uma parte não padronizada, onde o mais importante e fundamental é dividir a mensagem no ViewHolder. Obviamente, você pode dizer que isso é aplicável de maneira restrita, mas essa abordagem também pode ser projetada em outras coisas, por exemplo, em um texto grande de 10 mil caracteres. Ele pode ser dividido em parágrafos, onde cada parágrafo é um ViewHolder separado.

Também vale a pena maximizar tudo no @WorkerThread: analisar links, DiffUtils - descarregando assim o @UiThead o máximo possível.

O RecycledViewPool global permite percorrer as telas de mensagens e não criar um ViewHolder a cada vez.

Mas há outras coisas importantes que ainda não decidimos, por exemplo, um aumento excessivo, ou melhor, o carregamento de dados de recursos.

, Mobius 2019 Piter , . , , SQLite, . Mobius 2020 Piter .

All Articles