¿Cómo se visualiza la pantalla del mensaje VK?

¿Qué está haciendo VK para reducir los retrasos de representación? ¿Cómo mostrar un mensaje muy grande y no matar a UiThread? ¿Cómo reducir los retrasos de desplazamiento en RecyclerView?



Mi experiencia se basa en el trabajo de dibujar una pantalla de mensaje en la aplicación VK de Android, en la que es necesario mostrar una gran cantidad de información con un mínimo de frenos en la interfaz de usuario.

He estado programando para Android durante casi diez años, anteriormente era freelance para PHP / Node.js. Ahora, un desarrollador senior de Android VKontakte.

Bajo el corte: video y transcripción de mi informe de la conferencia Mobius 2019 Moscú.


El informe revela tres temas.



Mira a la pantalla:


Este mensaje está en algún lugar en cinco pantallas. Y bien pueden estar con nosotros (en el caso de reenviar mensajes). Las herramientas estándar ya no funcionarán. Incluso en un dispositivo superior, todo puede retrasarse.
Bueno, además de esto, la interfaz de usuario en sí es bastante diversa:

  • fechas de carga e indicadores,
  • mensajes de servicio
  • texto (emoji, enlace, correo electrónico, hashtags),
  • teclado bot
  • ~ 40 formas de mostrar archivos adjuntos,
  • Árbol de mensajes reenviados.

Surge la pregunta: ¿cómo hacer que el número de rezagos sea lo más pequeño posible? Tanto en el caso de mensajes simples como en el caso de mensajes masivos (caso marginal del video anterior).


Soluciones estandar


RecyclerView y sus complementos


Hay varios complementos para RecyclerView.

  • setHasFixedSize ( boolean)

Mucha gente piensa que esta bandera es necesaria cuando los elementos de la lista son del mismo tamaño. Pero, de hecho, a juzgar por la documentación, lo contrario es cierto. Esto es cuando el tamaño de RecyclerView es constante e independiente de los elementos (aproximadamente, no wrap_content). Establecer la bandera ayuda a aumentar ligeramente la velocidad de RecyclerView para evitar cálculos innecesarios.

  • setNestedScrollingEnabled ( boolean)

Optimización menor que deshabilita la compatibilidad con NestedScroll. No tenemos CollapsingToolbar u otras funciones que dependen de NestedScroll en esta pantalla, por lo que podemos configurar este indicador de forma segura en falso.

  • setItemViewCacheSize ( cache_size)

Configuración de la memoria caché interna RecyclerView.

Mucha gente piensa que la mecánica de RecyclerView es:

  • hay un ViewHolder en la pantalla;
  • hay un RecycledViewPool que almacena ViewHolder;
  • ViewHolder — RecycledViewPool.

En la práctica, todo es un poco más complicado, porque hay una caché intermedia entre estas dos cosas. Se llama ItemViewCache. ¿Cuál es su esencia? Cuando ViewHolder abandona la pantalla, no se coloca en RecycledViewPool, sino en el caché intermedio (ItemViewCache). Todos los cambios en el adaptador se aplican tanto a ViewHolder visible como a ViewHolder dentro de ItemViewCache. Y para ViewHolder dentro de RecycledViewPool, los cambios no se aplican.

A través de setItemViewCacheSize podemos establecer el tamaño de este caché intermedio.
Cuanto más grande sea, más rápido será el desplazamiento en distancias cortas, pero las operaciones de actualización tomarán más tiempo (debido a ViewHolder.onBind, etc.).

Cómo se implementa RecyclerView y cómo se estructura su caché es un tema bastante amplio y complejo. Puedes leer un gran articulo, donde hablan en detalle sobre todo.

Optimización OnCreate / OnBind


Otra solución clásica es optimizar onCreateViewHolder / onBindViewHolder:

  • diseño sencillo (intentamos usar FrameLayout o Custom ViewGroup tanto como sea posible),
  • las operaciones pesadas (análisis de enlaces / emoji) se realizan de forma asíncrona en la etapa de carga de mensajes,
  • StringBuilder para formatear nombre, fecha, etc.,
  • y otras soluciones que reducen el tiempo de trabajo de estos métodos.

Adaptador de seguimiento.onFailedToRecyclerView ()




Tiene una lista en la que algunos elementos (o parte de ellos) están animados con alfa. En el momento en que View, estando en proceso de animación, abandona la pantalla, no se dirige a RecycledViewPool. ¿Por qué? RecycledViewPool ve que la Vista ahora está animada por el indicador View.hasTransientState, y simplemente la ignora. Por lo tanto, la próxima vez que se desplace hacia arriba y hacia abajo, la imagen no se tomará de RecycledViewPool, sino que se creará de nuevo.

La decisión más correcta es cuando ViewHolder abandona la pantalla, debe cancelar todas las animaciones.



Si necesita una revisión tan pronto como sea posible o si es un desarrollador perezoso, en el método onFailedToRecycle simplemente puede devolver verdadero y todo funcionará, pero no recomendaría hacerlo.



Seguimiento de sobregiro y perfilador


La forma clásica de detectar problemas es el sobregiro y el seguimiento del generador de perfiles.

Sobregiro: el número de redibujos del píxel: cuantas menos capas y menos redibujado el píxel, más rápido. Pero según mis observaciones, en las realidades modernas, esto no afecta tanto el rendimiento.



Profiler: también conocido como Android Monitor, que se encuentra en Android Studio. En él, puede analizar todos los métodos llamados. Por ejemplo, abra los mensajes, desplácese hacia arriba y hacia abajo y vea qué métodos se llamaron y cuánto tardaron.



Todo lo que está en la mitad izquierda son las llamadas al sistema Android que se necesitan para crear / renderizar una Vista / ViewHolder. No podemos influir en ellos o tendremos que dedicar mucho esfuerzo.

La mitad derecha es nuestro código que se ejecuta en ViewHolder.

El bloque de llamadas en el número 1 es una llamada a expresiones regulares: en algún lugar pasaron por alto y olvidaron transferir la operación al subproceso de fondo, lo que ralentizó el desplazamiento en ~ 20%.

Llame al bloque en el número 2 - Fresco, una biblioteca para mostrar imágenes. No es óptimo en algunos lugares. Todavía no está claro qué hacer con este retraso, pero si podemos resolverlo, ahorraremos otro ~ 15%.

Es decir, al solucionar estos problemas, podemos obtener un aumento de ~ 35%, lo cual es bastante bueno.

Diferido


Muchos de ustedes usan DiffUtil en su forma estándar: hay dos listas: cambios llamados, comparados e impulsados. Hacer todo esto en el hilo principal es un poco costoso porque la lista puede ser muy grande. Por lo general, el cálculo de DiffUtil se ejecuta en un hilo de fondo.

ListAdapter y AsyncListDiffer hacen esto por usted. El ListAdapter extiende el adaptador normal e inicia todo de forma asincrónica: solo haga un submitList y el cálculo completo de los cambios se traslada al hilo interno de fondo. ListAdapter puede tener en cuenta el caso de actualizaciones frecuentes: si lo llama tres veces seguidas, solo tomará el último resultado.

DiffUtil se utiliza solo para algunos cambios estructurales: la apariencia del mensaje, su cambio y eliminación. Para algunos datos de cambio rápido, no es adecuado. Por ejemplo, cuando subimos una foto o reproducimos audio. Tales eventos ocurren a menudo, varias veces por segundo, y si ejecuta DiffUtil cada vez, obtendrá mucho trabajo adicional.

Animaciones


Había una vez un marco de animación, bastante escaso, pero todavía algo. Trabajamos con él así:

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

El problema es que el parámetro getTranslationX () devolverá el mismo valor antes y después de la animación. Esto se debe a que la animación cambió la representación visual, pero no cambió las propiedades físicas.



En Android 3.0, apareció el marco Animator, que es más correcto porque cambió la propiedad física específica del objeto.



Más tarde, ViewPropertyAnimator apareció y todos todavía no entienden su diferencia con Animator.

Lo explicaré. Digamos que necesita hacer la traducción en diagonal: cambie la vista a lo largo de los ejes x, y. Lo más probable es que escriba un código típico:

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

Y puedes hacerlo más corto:

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

Cuando ejecuta view.animate (), inicia implícitamente ViewPropertyAnimator.

¿Por qué es necesario?

  1. Más fácil de leer y mantener el código.
  2. Animación de operaciones por lotes.

En nuestro último caso, cambiamos dos propiedades. Cuando hacemos esto a través de animadores, los ticks de animación se llamarán por separado para cada Animador. Es decir, setTranslationX y setTranslationY se llamarán por separado, y View realizará las operaciones de actualización por separado.

En el caso de ViewPropertyAnimator, el cambio se produce al mismo tiempo, por lo que hay un ahorro debido a menos operaciones y el cambio en las propiedades en sí está mejor optimizado.

Puede lograr esto con Animator, pero tendrá que escribir más código. Además, con ViewPropertyAnimator, puede estar seguro de que las animaciones se optimizarán tanto como sea posible. ¿Por qué? Android tiene un RenderNode (DisplayList). En términos generales, almacenan en caché el resultado de onDraw y lo usan al volver a dibujar. ViewPropertyAnimator trabaja directamente con RenderNode y le aplica animaciones, evitando llamadas onDraw.

Muchas propiedades de Vista también pueden afectar directamente el RenderNode, pero no todas. Es decir, cuando usa ViewPropertyAnimator, tiene la garantía de usar la forma más eficiente. Si de repente tienes algún tipo de animación que no se puede hacer con ViewPropertyAnimator, entonces quizás deberías pensarlo y cambiarlo.

Animaciones: TransitionManager


Por lo general, las personas asocian que este marco se usa para pasar de una Actividad a otra. De hecho, se puede usar de manera diferente y simplificar enormemente la implementación de la animación de los cambios estructurales. Supongamos que tenemos una pantalla en la que se reproduce un mensaje de voz. Lo cerramos con una cruz, y el dado sube. ¿Cómo hacerlo? La animación es bastante complicada: el jugador cierra con alfa, mientras se mueve no a través de la traducción, sino que cambia su altura. Al mismo tiempo, nuestra lista sube y también cambia la altura.



Si el jugador fuera parte de la lista, entonces la animación sería bastante simple. Pero con nosotros, el jugador no es un elemento de la lista, sino una Vista completamente independiente.

Quizás comenzaríamos a escribir algún tipo de Animator, luego encontraríamos problemas, fallas, comenzaríamos a cortar muletas y duplicaríamos el código. Y obtendría algo como la pantalla de abajo.



Con TransitionManager, puede simplificar todo:

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

Toda la animación ocurre automáticamente debajo del capó. Parece mágico, pero si profundiza y ve cómo funciona, resulta que TransitionManager simplemente se suscribe a todas las vistas, captura los cambios en sus propiedades, calcula diff, crea los animadores necesarios o ViewPropertyAnimator cuando es necesario, y hace todo lo más eficientemente posible. TransitionManager nos permite hacer animaciones en la sección de mensajes de forma rápida y fácil de implementar.

Soluciones personalizadas




Esto es lo más fundamental en el que se basan el rendimiento y los problemas que siguen. ¿Qué hacer cuando su mensaje está en 10 pantallas? Si presta atención, todos nuestros elementos se encuentran exactamente uno debajo del otro. Si aceptamos que ViewHolder no es un solo mensaje, sino docenas de ViewHolders diferentes, entonces todo se vuelve mucho más simple.

No es un problema para nosotros que el mensaje se haya convertido en 10 pantallas, porque ahora solo mostramos seis ViewHolders en un ejemplo concreto. Tenemos un diseño fácil, el código es más fácil de mantener y no hay problemas especiales, excepto uno: ¿cómo hacer esto?



Hay ViewHolders simples: estos son separadores de fechas clásicos, cargar más, etc. Y BaseViewHolder: ViewHolder condicionalmente básico para el mensaje. Tiene una implementación básica y varias específicas: TextViewHolder, PhotoViewHolder, AudioViewHolder, ReplyViewHolder, etc. Hay alrededor de 70 de ellos.

¿De qué es responsable BaseViewHolder?


BaseViewHolder solo es responsable de dibujar el avatar y el trozo de burbuja deseado, así como la línea para los mensajes reenviados: azul a la izquierda.



La implementación concreta del contenido ya la llevan a cabo otros herederos de BaseViewHolder: TextViewHolder muestra solo texto, FwdSenderViewHolder, el autor del mensaje reenviado, AudioMsgViewHolder, el mensaje de voz, etc.



Hay un problema: ¿qué hacer con el ancho? Imagine un mensaje en al menos dos pantallas. No está muy claro qué ancho establecer, porque la mitad es visible, la mitad no es visible (y ni siquiera se ha creado aún). Absolutamente todo no se puede medir, porque yace. Tengo que mudar un poco, por desgracia. Hay casos simples en los que el mensaje es muy simple: solo texto o voz, en general, consta de un elemento.



En este caso, use el clásico wrap_content. Para un caso complejo, cuando un mensaje consta de varias piezas, tomamos y forzamos cada ViewHolder un ancho fijo. Específicamente aquí: 220 dp.



Si el texto es muy corto y se reenvía el mensaje, hay un espacio vacío a la derecha. No hay escapatoria de esto, porque el rendimiento es más importante. Durante varios años, no hubo quejas, tal vez alguien se dio cuenta, pero en general, todos se acostumbraron.



Hay casos extremos. Si respondemos a un mensaje con una pegatina, entonces podemos especificar el ancho específicamente para tal caso, para que se vea más bonito.

Nos dividimos en ViewHolders en la etapa de carga de mensajes: comenzamos la carga en segundo plano del mensaje, lo convertimos en elemento, se muestran directamente en ViewHolders.



Global RecycledViewPool


La mecánica del uso de nuestro messenger es tal que las personas no se sientan en el mismo chat, sino que constantemente se interponen entre ellas. En el enfoque estándar, cuando entramos en el chat y lo dejamos, el RecycledViewPool (y el ViewHolder en él) simplemente se destruyen, y cada vez que gastamos recursos creando el ViewHolder.

Esto se puede resolver con RecycledViewPool global:

  • dentro del marco de la aplicación, RecycledViewPool vive como un singleton;
  • reutilizado en la pantalla de mensajes cuando el usuario camina entre pantallas;
  • establecer como RecyclerView.setRecycledViewPool (pool).

Hay dificultades, es importante recordar dos cosas:

  • vas a la pantalla, haces clic en atrás y sales. El problema es que esos ViewHolders que estaban en la pantalla se descartan y no se devuelven al grupo. Esto se arregla de la siguiente manera:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • RecycledViewPool tiene limitaciones: no se pueden almacenar más de cinco ViewHolders para cada ViewType.

Si se muestran 9 TextViews en la pantalla, solo se devolverán cinco elementos al RecycledViewPool, y el resto se desechará. Puede cambiar el tamaño de RecycledViewPool:
RecycledViewPool.setMaxRecycledViews (viewType, size)

Pero es un poco triste escribir en cada ViewType con las manos, porque puede escribir su RecycledViewPool, expandir el estándar y convertirlo en NoLimit. Por el enlace puede descargar la implementación terminada.

DiffUtil no siempre es útil


Aquí hay un caso clásico: descargar, reproducir una pista de audio y un mensaje de voz. En este caso, DiffUtil llama spam.



Nuestro BaseViewHolder tiene un método abstracto updateUploadProgress.

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

Para lanzar un evento, debemos omitir todos los ViewHolder visibles:

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

Esta es una operación simple, es poco probable que tengamos más de diez ViewHolder en la pantalla. Tal enfoque no puede retrasarse en principio. ¿Cómo encontrar ViewHolder visible? Una implementación ingenua sería algo como esto:

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

Pero hay un problema. El caché intermedio que mencioné anteriormente, ItemViewCache, contiene ViewHolders activos que simplemente no aparecen en la pantalla. El código anterior no los afectará. Directamente, nosotros tampoco podemos abordarlos. Y luego las muletas vienen en nuestra ayuda. Cree un WeakSet que almacene enlaces a ViewHolder. A continuación, solo tenemos que pasar por alto este 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)
    }
}

ViewHolder Overlay


Considere el ejemplo de las historias. Anteriormente, si una persona reaccionaba a una historia con una pegatina, la mostramos así: se



ve bastante feo. Quería hacerlo mejor, porque las historias son de contenido brillante, y tenemos un pequeño cuadrado allí. Pero queríamos obtener algo como esto:



hay un problema: nuestro mensaje está dividido en ViewHolder, están ubicados estrictamente uno debajo del otro, pero aquí se superponen. Inmediatamente no está claro cómo resolver esto. Puede crear otro ViewType "historial + etiqueta" o "historial + mensaje de voz". Entonces, en lugar de 70 ViewType, tendríamos 140 ... No, necesitamos encontrar algo más conveniente.



Me viene a la mente una de sus muletas favoritas en Android. Por ejemplo, estábamos haciendo algo, pero Pixel Perfect no converge. Para solucionar esto, debe eliminar todo y escribir desde cero, pero la pereza. Como resultado, podemos hacer margen = -2dp (negativo), y ahora todo encaja. Pero tal enfoque no puede usarse aquí. Si establece un margen negativo, la pegatina se moverá, pero el lugar que ocupó permanecerá vacío. Pero tenemos ItemDecoration, donde itemOffset podemos hacer un número negativo. ¡Y funciona! Como resultado, obtenemos la superposición esperada y, al mismo tiempo, permanece un paradigma en el que cada ViewHolder es amigo.

Una hermosa solución en una sola línea.

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

Idlehandler


Este es un caso con un asterisco, es complejo y no se necesita con tanta frecuencia en la práctica, pero es importante saber acerca de la existencia de dicho método.

Primero, te diré cómo funciona el hilo principal de UiThread. El esquema general: hay una cola de eventos de tareas en la que las tareas se establecen a través de handler.post, y un bucle infinito que pasa por esta cola. Es decir, UiThread es solo mientras (verdadero). Si hay tareas, las ejecutamos, si no, esperamos hasta que aparezcan.



En nuestras realidades habituales, Handler es responsable de poner las tareas en la cola, y Looper la evita sin cesar. Hay tareas que no son muy importantes para la interfaz de usuario. Por ejemplo, un usuario lee un mensaje; no es tan importante para nosotros cuando lo mostramos en la interfaz de usuario, ahora o después de 20 ms. El usuario no notará la diferencia. Entonces, ¿quizás valga la pena ejecutar esta tarea en el hilo principal solo cuando es gratis? Es decir, sería bueno para nosotros saber cuándo se llama a la línea waitNewTask. Para este caso, Looper tiene un addIdleHandler que se activa cuando se activa el código task.isEmpty.

Looper.myQueue (). AddIdleHandler ()

Y luego la implementación más simple de IdleHandler se verá así:

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

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

Del mismo modo, puede medir un arranque en frío honesto de la aplicación.

Emoji


Utilizamos nuestros emoji personalizados en lugar de los del sistema. Aquí hay un ejemplo de cómo se veían los emojis en diferentes plataformas en diferentes años. Los emoji izquierdo y derecho son bastante agradables, pero en el medio ...



Hay un segundo problema:



cada fila es el mismo emoji, pero las emociones que reproducen son diferentes. Me gusta la parte inferior derecha, todavía no entiendo lo que significa.

Hay una bicicleta de VKontakte. En ~ 2014, cambiamos ligeramente un emoji. Tal vez alguien recuerda: "Marshmallow" era. Después de su cambio, comenzó un mini disturbio. Por supuesto, no alcanzó el nivel de "devolver el muro", pero la reacción fue bastante interesante. Y esto nos dice sobre la importancia de interpretar emoji.

Cómo se hacen los emojis: tenemos un gran mapa de bits, donde todos se ensamblan en un gran "atlas". Hay varios de ellos, bajo diferentes DPI. Y hay un EmojiSpan que contiene información: dibujo emoji "tal y tal", está en tal y tal mapa de bits en tal y tal ubicación (x, y).
Y hay un ReplacementSpan que le permite mostrar algo en lugar de texto en Span.
Es decir, encuentra emoji en el texto, lo envuelve con EmojiSpan y el sistema dibuja el emoji deseado en lugar del sistema.



Alternativas


Inflar


Alguien puede decir que, dado que el inflado es lento, ¿por qué no crear un diseño con las manos y evitar inflarlo? Y de ese modo, acelere todo evitando el ViewHolder 100500. Es una ilusión. Antes de hacer algo, vale la pena medirlo.

Android tiene una clase de depuración, tiene startMethodTracing y stopMethodTracing.

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

Nos permitirá recopilar información sobre el tiempo de ejecución de un código en particular.



Y vemos que aquí inflar como tal es incluso invisible. Una cuarta parte del tiempo se la pasó cargando dibujables, una cuarta parte cargando colores. Y solo en algún lugar de la parte de etc. está nuestro inflado.

Traté de traducir el diseño XML en código y lo guardé en algún lugar alrededor de 0,5 ms. El aumento, de hecho, no es el más impresionante. Y el código se ha vuelto mucho más complicado. Es decir, reescribir no tiene mucho sentido.

Además, en la práctica, muchos no encontrarán este problema en absoluto, porque un inflado largo generalmente ocurre solo cuando la aplicación se vuelve muy grande. En nuestra aplicación VKontakte, por ejemplo, hay aproximadamente 200-300 pantallas diferentes, y la carga de todos los recursos se bloquea. Qué hacer con esto aún no está claro. Lo más probable es que tenga que escribir su propio administrador de recursos.

Anko


Anko ha quedado en desuso recientemente. De todos modos, Anko no es magia, sino un simple azúcar sintáctico. Traduce todo a la nueva Vista condicional () de la misma manera. Por lo tanto, no hay beneficio de Anko.

Litho / Flutter


¿Por qué combiné dos cosas completamente ajenas? Porque no se trata de tecnología, sino de la complejidad de la migración a ella. No puede simplemente pedir prestado y pasar a una nueva biblioteca.

No está claro si esto nos dará un aumento de rendimiento. Y no tendremos nuevos problemas, porque millones de personas con dispositivos completamente diferentes usan nuestra aplicación cada minuto (probablemente ni siquiera haya escuchado acerca de una cuarta parte de ellos). Además, los mensajes son una base de código muy grande. Es imposible reescribir todo al instante. Y hacerlo por la exageración de la tecnología es estúpido. Especialmente cuando el Jetpack Compose aparece en algún lugar lejano.

Jetpack componer


Google nos promete maná del cielo en forma de esta biblioteca, pero todavía está en alfa. Y cuándo estará en el lanzamiento, no está claro. Si podemos obtenerlo en su forma actual tampoco está claro. Es muy temprano para experimentar. Deja que salga estable, deja que los errores principales se cierren. Y solo entonces miraremos en su dirección.

Una gran vista personalizada


Hay otro enfoque del que hablan quienes usan varios mensajeros instantáneos: "tomar y escribir una vista personalizada grande, sin jerarquía complicada".

¿Cuales son las desventajas?

  • Es difícil de mantener.
  • No tiene sentido en las realidades actuales.

Con Android 4.3, se bombeó el sistema de almacenamiento en caché interno dentro de View. Por ejemplo, onMeasure no se llama si la Vista no ha cambiado. Y se utilizan los resultados de la medición anterior.

Con Android 4.3-4.4, apareció un RenderNode (DisplayList), el procesamiento en caché. Veamos un ejemplo. Supongamos que hay una celda en la lista de cuadros de diálogo: avatar, título, subtítulo, estado de lectura, hora, otro avatar. Condicionalmente - 10 elementos. Y escribimos Vista personalizada. En este caso, al cambiar una propiedad, volveremos a medir todos los elementos. Es decir, solo gasta los recursos adicionales. En el caso de ViewGroup, donde cada elemento es una Vista separada, cuando cambia una Vista, invalidaremos solo una Vista (excepto cuando esta Vista afecte el tamaño de otras).

Resumen


Entonces, has aprendido que usamos el clásico RecyclerView con optimizaciones estándar. Hay una parte de los no estándar, donde lo más importante y fundamental es dividir el mensaje en ViewHolder. Por supuesto, puede decir que esto es de aplicación limitada, pero este enfoque también se puede proyectar en otras cosas, por ejemplo, en un texto grande de 10 mil caracteres. Se puede dividir en párrafos, donde cada párrafo es un ViewHolder separado.

También vale la pena maximizar todo en @WorkerThread: analizar enlaces, DiffUtils, descargando así @UiThead tanto como sea posible.

El RecycledViewPool global le permite caminar entre pantallas de mensajes y no crear un ViewHolder cada vez.

Pero hay otras cosas importantes que aún no hemos decidido, por ejemplo, un largo inflado, o más bien, cargando datos de los recursos.

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

All Articles