Comment s'affiche l'écran de message VK?

Que fait VK pour réduire les retards de rendu? Comment afficher un très gros message et ne pas tuer UiThread? Comment réduire les délais de défilement dans RecyclerView?



Mon expérience est basée sur le travail de dessin d'un écran de message dans l'application VK Android, dans lequel il est nécessaire d'afficher une énorme quantité d'informations avec un minimum de freins sur l'interface utilisateur.

Je programme pour Android depuis près de dix ans, j'étais auparavant freelance pour PHP / Node.js. Maintenant - un développeur Android senior VKontakte.

Sous la coupe - vidéo et transcription de mon rapport de la conférence Mobius 2019 à Moscou.


Le rapport révèle trois sujets



Regarde l'écran:


Ce message est quelque part sur cinq écrans. Et ils pourraient bien être avec nous (dans le cas du transfert de messages). Les outils standard ne fonctionneront plus. Même sur un appareil haut de gamme, tout peut prendre du retard.
Eh bien, en plus de cela, l'interface utilisateur elle-même est assez diversifiée:

  • dates et indicateurs de chargement,
  • messages de service
  • texte (emoji, lien, email, hashtags),
  • clavier bot
  • ~ 40 façons d'afficher les pièces jointes,
  • arborescence des messages transférés.

La question se pose: comment réduire au maximum le nombre de décalages? À la fois dans le cas de messages simples et dans le cas de messages en masse (cas de bord de la vidéo ci-dessus).


Solutions standard


RecyclerView et ses modules complémentaires


Il existe différents modules complémentaires pour RecyclerView.

  • setHasFixedSize ( boolean)

Beaucoup de gens pensent que ce drapeau est nécessaire lorsque les éléments de la liste sont de la même taille. Mais en fait, à en juger par la documentation, le contraire est vrai. C'est lorsque la taille de RecyclerView est constante et indépendante des éléments (à peu près, pas wrap_content). La définition de l'indicateur permet d'augmenter légèrement la vitesse du RecyclerView afin d'éviter des calculs inutiles.

  • setNestedScrollingEnabled ( boolean)

Optimisation mineure qui désactive la prise en charge de NestedScroll. Nous n'avons pas CollapsingToolbar ou d'autres fonctionnalités en fonction de NestedScroll sur cet écran, nous pouvons donc définir ce drapeau sur false en toute sécurité.

  • setItemViewCacheSize ( cache_size)

Configuration du cache RecyclerView interne.

Beaucoup de gens pensent que les mécanismes de RecyclerView sont:

  • il y a un ViewHolder affiché sur l'écran;
  • il y a un RecycledViewPool stockant ViewHolder;
  • ViewHolder — RecycledViewPool.

En pratique, tout est un peu plus compliqué, car il y a un cache intermédiaire entre ces deux choses. Il s'appelle ItemViewCache. Quelle est son essence? Lorsque le ViewHolder quitte l'écran, il n'est pas placé dans le RecycledViewPool, mais dans le cache intermédiaire (ItemViewCache). Toutes les modifications apportées à l'adaptateur s'appliquent à la fois au ViewHolder visible et au ViewHolder à l'intérieur de ItemViewCache. Et pour le ViewHolder à l'intérieur de RecycledViewPool, les modifications ne sont pas appliquées.

Grâce à setItemViewCacheSize, nous pouvons définir la taille de ce cache intermédiaire.
Plus il est grand, plus le défilement sera rapide sur de courtes distances, mais les opérations de mise à jour prendront plus de temps (en raison de ViewHolder.onBind, etc.).

Comment RecyclerView est implémenté et comment son cache est structuré est un sujet assez vaste et complexe. Vous pouvez lire un excellent article, où ils parlent en détail de tout.

Optimisation OnCreate / OnBind


Une autre solution classique consiste à optimiser onCreateViewHolder / onBindViewHolder:

  • mise en page facile (nous essayons d'utiliser autant que possible FrameLayout ou Custom ViewGroup),
  • les opérations lourdes (parsing links / emoji) se font de manière asynchrone au stade du chargement des messages,
  • StringBuilder pour la mise en forme du nom, de la date, etc.,
  • et d'autres solutions qui réduisent le temps de travail de ces méthodes.

Tracking Adapter.onFailedToRecyclerView ()




Vous disposez d'une liste dans laquelle certains éléments (ou une partie d'entre eux) sont animés en alpha. Au moment où View, en cours d'animation, quitte l'écran, il ne passe pas à RecycledViewPool. Pourquoi? RecycledViewPool voit que la vue est maintenant animée par l'indicateur View.hasTransientState et l'ignore simplement. Par conséquent, la prochaine fois que vous faites défiler vers le haut ou vers le bas, la photo ne sera pas prise à partir de RecycledViewPool, mais sera recréée à nouveau.

La décision la plus correcte est lorsque ViewHolder quitte l'écran, vous devez annuler toutes les animations.



Si vous avez besoin d'un correctif dès que possible ou si vous êtes un développeur paresseux, alors dans la méthode onFailedToRecycle, vous pouvez simplement toujours retourner true et tout fonctionnera, mais je ne le conseillerais pas.



Suivi du découvert et du profileur


La manière classique de détecter les problèmes est le sur-retrait et le suivi du profileur.

Overdraw - le nombre de redessins du pixel: moins il y a de couches et moins le pixel est redessiné, plus vite. Mais selon mes observations, dans les réalités modernes, cela n'affecte pas tellement les performances.



Profiler - alias Android Monitor, qui est dans Android Studio. Dans celui-ci, vous pouvez analyser toutes les méthodes appelées. Par exemple, ouvrez des messages, faites défiler vers le haut et vers le bas et voyez quelles méthodes ont été appelées et combien de temps elles ont pris.



Tout ce qui se trouve dans la moitié gauche est les appels système Android nécessaires pour créer / rendre un View / ViewHolder. Soit nous ne pouvons pas les influencer, soit nous devrons consacrer beaucoup d'efforts.

La moitié droite est notre code qui s'exécute dans ViewHolder.

Le bloc d'appel au numéro 1 est un appel à des expressions régulières: quelque part, ils ont oublié et oublié de mettre l'opération sur le thread d'arrière-plan, ralentissant ainsi le défilement de ~ 20%.

Bloc d'appel au numéro 2 - Fresco, une bibliothèque pour afficher des images. Il n'est pas optimal à certains endroits. On ne sait pas encore quoi faire avec ce décalage, mais si nous pouvons le résoudre, nous économiserons encore ~ ​​15%.

Autrement dit, en résolvant ces problèmes, nous pouvons obtenir une augmentation de ~ 35%, ce qui est plutôt cool.

Difficile


Beaucoup d'entre vous utilisent DiffUtil sous sa forme standard: il existe deux listes - appelées, comparées et poussées. Faire tout cela sur le thread principal est un peu cher car la liste peut être très longue. Ainsi, le calcul DiffUtil s'exécute généralement sur un thread d'arrière-plan.

ListAdapter et AsyncListDiffer le font pour vous. Le ListAdapter étend l'adaptateur normal et démarre tout de manière asynchrone - il suffit de créer une liste de soumission et le calcul complet des modifications s'envole vers le thread d'arrière-plan interne. Le ListAdapter peut prendre en compte le cas de mises à jour fréquentes: si vous l'appelez trois fois de suite, il ne prendra que le dernier résultat.

DiffUtil lui-même, nous utilisons uniquement pour certains changements structurels - l'apparence du message, son changement et sa suppression. Pour certaines données à changement rapide, elles ne conviennent pas. Par exemple, lorsque nous téléchargeons une photo ou lisons de l'audio. De tels événements se produisent souvent - plusieurs fois par seconde, et si vous exécutez DiffUtil à chaque fois, vous aurez beaucoup de travail supplémentaire.

Des animations


Il était une fois un cadre d'animation - plutôt maigre, mais toujours quelque chose. Nous avons travaillé avec lui comme ceci:

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

Le problème est que le paramètre getTranslationX () renverra la même valeur avant et après l'animation. C'est parce que l'animation a changé la représentation visuelle, mais n'a pas changé les propriétés physiques.



Dans Android 3.0, le framework Animator est apparu, ce qui est plus correct car il a modifié la propriété physique spécifique de l'objet.



Plus tard, ViewPropertyAnimator est apparu et tout le monde ne comprend toujours pas vraiment sa différence avec Animator.

Je vais expliquer. Disons que vous devez effectuer la traduction en diagonale - déplacez la vue le long des axes x, y. Très probablement, vous écririez du code typique:

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

Et vous pouvez le raccourcir:

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

Lorsque vous exécutez view.animate (), vous lancez implicitement ViewPropertyAnimator.

Pourquoi est-il nécessaire?

  1. Lecture et maintenance du code plus faciles.
  2. Animation d'opérations par lots.

Dans notre dernier cas, nous avons modifié deux propriétés. Lorsque nous le faisons via des animateurs, les ticks d'animation seront appelés séparément pour chaque animateur. Autrement dit, setTranslationX et setTranslationY seront appelés séparément et View effectuera les opérations de mise à jour séparément.

Dans le cas de ViewPropertyAnimator, le changement se produit en même temps, il y a donc une économie due à moins d'opérations et le changement des propriétés lui-même est mieux optimisé.

Vous pouvez y parvenir avec Animator, mais vous devrez écrire plus de code. De plus, en utilisant ViewPropertyAnimator, vous pouvez être sûr que les animations seront optimisées autant que possible. Pourquoi? Android a un RenderNode (DisplayList). Très grossièrement, ils mettent en cache le résultat de onDraw et l'utilisent lors du redessin. ViewPropertyAnimator fonctionne directement avec RenderNode et lui applique des animations, en évitant les appels onDraw.

De nombreuses propriétés d'affichage peuvent également affecter directement le RenderNode, mais pas toutes. Autrement dit, lorsque vous utilisez ViewPropertyAnimator, vous êtes assuré d'utiliser la manière la plus efficace. Si vous avez soudainement une sorte d'animation qui ne peut pas être faite avec ViewPropertyAnimator, alors vous devriez peut-être y penser et la changer.

Animations: TransitionManager


Habituellement, les gens associent que ce cadre est utilisé pour passer d'une activité à une autre. En fait, il peut être utilisé différemment et simplifier considérablement la mise en œuvre de l'animation des changements structurels. Supposons que nous ayons un écran sur lequel un message vocal est lu. Nous le fermons avec une croix, et le dé monte. Comment faire? L'animation est assez compliquée: le joueur se ferme avec alpha, tout en ne se déplaçant pas par translation, mais change de hauteur. Dans le même temps, notre liste monte et change également la hauteur.



Si le joueur faisait partie de la liste, l'animation serait assez simple. Mais pour nous, le joueur n'est pas un élément de la liste, mais une vision complètement indépendante.

Peut-être que nous commencerions à écrire une sorte d'animateur, puis nous rencontrerions des problèmes, des plantages, commencerions à scier des béquilles et doubler le code. Et obtiendrait quelque chose comme l'écran ci-dessous.



Avec TransitionManager, vous pouvez tout simplifier:

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

Toute animation se déroule automatiquement sous le capot. Cela ressemble à de la magie, mais si vous allez profondément à l'intérieur et voyez comment cela fonctionne, il s'avère que le TransitionManager s'abonne simplement à toutes les vues, capture les modifications de leurs propriétés, calcule les différences, crée les animateurs nécessaires ou ViewPropertyAnimator si nécessaire, et fait tout aussi efficacement que possible. TransitionManager nous permet de créer des animations dans la section des messages rapidement et facilement.

Solutions personnalisées




C'est la chose la plus fondamentale sur laquelle reposent les performances et les problèmes qui en découlent. Que faire lorsque votre message est sur 10 écrans? Si vous faites attention, tous nos éléments sont situés exactement les uns sous les autres. Si nous acceptons que ViewHolder n'est pas un seul message, mais des dizaines de ViewHolders différents, alors tout devient beaucoup plus simple.

Ce n'est pas un problème pour nous que le message soit devenu 10 écrans, car maintenant nous affichons seulement six ViewHolders dans un exemple concret. Nous avons obtenu une mise en page facile, le code est plus facile à maintenir et il n'y a pas de problèmes particuliers, sauf un - comment faire?



Il existe des ViewHolders simples - ce sont des séparateurs de date classiques, Charger plus, etc. Et BaseViewHolder - ViewHolder de base conditionnelle pour le message. Il a une implémentation de base et plusieurs spécifiques - TextViewHolder, PhotoViewHolder, AudioViewHolder, ReplyViewHolder et ainsi de suite. Il y en a environ 70.

De quoi est responsable BaseViewHolder?


BaseViewHolder est uniquement responsable du dessin de l'avatar et du morceau de bulle souhaité, ainsi que de la ligne des messages transférés - bleu à gauche.



L'implémentation concrète du contenu est déjà effectuée par d'autres héritiers de BaseViewHolder: TextViewHolder affiche uniquement le texte, FwdSenderViewHolder - l'auteur du message transféré, AudioMsgViewHolder - le message vocal, etc.



Il y a un problème: que faire de la largeur? Imaginez un message sur au moins deux écrans. La largeur à définir n'est pas très claire, car la moitié est visible, la moitié n'est pas visible (et n'a même pas encore été créée). Absolument tout ne peut pas être mesuré, car il repose. Je dois béquiller un peu, hélas. Il y a des cas simples où le message est très simple: purement texte ou voix - en général, se compose d'un élément.



Dans ce cas, utilisez le wrap_content classique. Pour un cas complexe, lorsqu'un message est composé de plusieurs éléments, nous prenons et forceons chaque ViewHolder sur une largeur fixe. Plus précisément ici - 220 dp.



Si le texte est très court et que le message est transmis, il y a un espace vide à droite. Il n'y a pas d'échappatoire à cela, car les performances sont plus importantes. Pendant plusieurs années, il n'y a eu aucune plainte - peut-être que quelqu'un l'a remarqué, mais en général, tout le monde s'y est habitué.



Il y a des cas marginaux. Si nous répondons à un message avec un autocollant, nous pouvons spécifier la largeur spécifiquement pour un tel cas, afin qu'il soit plus joli.

Nous nous divisons en ViewHolders au stade du chargement des messages: nous démarrons le chargement en arrière-plan du message, le convertissons en élément, ils sont directement affichés dans ViewHolders.



Global RecycledViewPool


Les mécanismes d'utilisation de notre messager sont tels que les gens ne s'assoient pas dans le même chat, mais passent constamment entre eux. Dans l'approche standard, lorsque nous sommes entrés dans le chat et l'avons laissé, le RecycledViewPool (et le ViewHolder qu'il contient) sont simplement détruits, et chaque fois que nous dépensons des ressources pour créer le ViewHolder.

Cela peut être résolu par Global RecycledViewPool:

  • dans le cadre d'Application, RecycledViewPool vit comme un singleton;
  • réutilisé sur l'écran de message lorsque l'utilisateur passe d'un écran à l'autre;
  • défini comme RecyclerView.setRecycledViewPool (pool).

Il y a des pièges, il est important de se rappeler deux choses:

  • vous allez à l'écran, cliquez sur retour, quittez. Le problème est que les ViewHolders qui étaient à l'écran sont jetés et ne sont pas retournés à la piscine. Ceci est résolu comme suit:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • RecycledViewPool a des limites: pas plus de cinq ViewHolders peuvent être stockés pour chaque ViewType.

Si 9 TextViews s'affichent à l'écran, seuls cinq éléments seront renvoyés au RecycledViewPool et les autres seront jetés. Vous pouvez changer la taille du RecycledViewPool:
RecycledViewPool.setMaxRecycledViews (viewType, size)

Mais il est en quelque sorte triste de prescrire pour chaque ViewType avec vos mains, car vous pouvez écrire votre RecycledViewPool, en étendant le standard et en faire NoLimit. Par le lien, vous pouvez télécharger l'implémentation terminée.

DiffUtil n'est pas toujours utile


Voici un cas classique - téléchargement, lecture d'une piste audio et d'un message vocal. Dans ce cas, DiffUtil appelle le spam.



Notre BaseViewHolder a une méthode abstraite updateUploadProgress.

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

Pour lancer un événement, nous devons contourner tous les ViewHolder visibles:

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

Il s'agit d'une opération simple, il est peu probable que nous ayons plus de dix ViewHolder à l'écran. Une telle approche ne peut pas être en retard sur le principe. Comment trouver ViewHolder visible? Une implémentation naïve serait quelque chose comme ceci:

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

Mais il y a un problème. Le cache intermédiaire que j'ai mentionné plus tôt, ItemViewCache, contient des ViewHolders actifs qui n'apparaissent tout simplement pas à l'écran. Le code ci-dessus ne les affectera pas. Directement, nous ne pouvons pas non plus y répondre. Et puis des béquilles viennent à notre aide. Créez un WeakSet qui stocke les liens vers ViewHolder. De plus, il nous suffit de contourner simplement ce 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)
    }
}

Superposition de ViewHolder


Prenons l'exemple des histoires. Auparavant, si une personne réagissait à une histoire avec un autocollant, nous la présentions comme ceci:



elle a l'air assez moche. Je voulais faire mieux, car les histoires ont un contenu lumineux et nous avons un petit carré là-bas. Mais nous voulions obtenir quelque chose comme ceci:



Il y a un problème: notre message est divisé en ViewHolder, ils sont situés strictement les uns sous les autres, mais ici ils se chevauchent. Immédiatement, on ne sait pas comment résoudre ce problème. Vous pouvez créer un autre ViewType «historique + autocollant» ou «historique + message vocal». Donc, au lieu de 70 ViewType, nous en aurions 140 ... Non, nous devons trouver quelque chose de plus pratique.



Une de vos béquilles préférées dans Android me vient à l'esprit. Par exemple, nous faisions quelque chose, mais Pixel Perfect ne converge pas. Pour résoudre ce problème, vous devez tout supprimer et écrire à partir de zéro, mais la paresse. En conséquence, nous pouvons faire une marge = -2dp (négative), et maintenant tout se met en place. Mais une telle approche ne peut pas être utilisée ici. Si vous définissez une marge négative, l'autocollant se déplacera, mais la place qu'il occupait restera vide. Mais nous avons ItemDecoration, où itemOffset nous pouvons faire un nombre négatif. Et il fonctionne! En conséquence, nous obtenons la superposition attendue et en même temps il reste un paradigme où chaque ViewHolder est ami.

Une belle solution en une seule ligne.

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

Idlehandler


C'est un cas avec un astérisque, c'est complexe et pas si souvent nécessaire en pratique, mais il est important de connaître l'existence d'une telle méthode.

Tout d'abord, je vais vous expliquer le fonctionnement du thread principal UiThread. Le schéma général: il existe une file d'attente d'événements de tâches dans laquelle les tâches sont définies via handler.post, et une boucle infinie qui traverse cette file d'attente. Autrement dit, UiThread est juste pendant (vrai). S'il y a des tâches, on les exécute, sinon, on attend qu'elles apparaissent.



Dans nos réalités habituelles, Handler est responsable de la mise des tâches dans la file d'attente, et Looper contourne sans cesse la file d'attente. Il y a des tâches qui ne sont pas très importantes pour l'interface utilisateur. Par exemple, un utilisateur lit un message - ce n'est pas si important pour nous lorsque nous l'afficherons sur l'interface utilisateur, maintenant ou après 20 ms. L'utilisateur ne remarquera pas la différence. Alors, cela vaut peut-être la peine d'exécuter cette tâche sur le thread principal uniquement lorsqu'elle est gratuite? Autrement dit, il serait bon pour nous de savoir quand la ligne waitNewTask est appelée. Dans ce cas, Looper a un addIdleHandler qui se déclenche lorsque le code tasks.isEmpty se déclenche.

Looper.myQueue (). AddIdleHandler ()

Et puis l'implémentation la plus simple de IdleHandler ressemblera à ceci:

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

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

De la même manière, vous pouvez mesurer un démarrage à froid honnête de l'application.

Emoji


Nous utilisons nos emoji personnalisés au lieu de ceux du système. Voici un exemple de l'apparence des emojis sur différentes plateformes au cours des différentes années. Les emoji gauche et droit sont plutôt sympas, mais au milieu ...



Il y a un deuxième problème:



chaque ligne est le même emoji, mais les émotions qu'ils reproduisent sont différentes. J'aime le bas à droite le plus, je ne comprends toujours pas ce que cela signifie.

Il y a un vélo de VKontakte. En ~ 2014, nous avons légèrement changé un emoji. Peut-être que quelqu'un se souvient - "Marshmallow" l'était. Après son changement, une mini-émeute a commencé. Bien sûr, il n'a pas atteint le niveau «retour du mur», mais la réaction a été assez intéressante. Et cela nous indique l'importance d'interpréter les emoji.

Comment les émojis sont faits: nous avons un grand bitmap, où ils sont tous assemblés dans un grand «atlas». Il y en a plusieurs - sous différents DPI. Et il y a un EmojiSpan qui contient des informations: je dessine "tel ou tel" emoji, il est dans tel ou tel bitmap à tel ou tel endroit (x, y).
Et il y a un ReplacementSpan qui vous permet d'afficher quelque chose au lieu de texte sous Span.
Autrement dit, vous trouvez des emoji dans le texte, enveloppez-les avec EmojiSpan, et le système dessine les emoji souhaités au lieu du système.



Alternatives


Gonfler


Quelqu'un peut dire que comme le gonflage est lent, pourquoi ne pas simplement créer une disposition avec vos mains, en évitant de gonfler. Et accélérez ainsi tout en évitant le 100500 ViewHolder. C'est une illusion. Avant de faire quelque chose, il vaut la peine de le mesurer.

Android a une classe Debug, il a startMethodTracing et stopMethodTracing.

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

Cela nous permettra de collecter des informations sur le temps d'exécution d'un morceau de code particulier.



Et on voit qu'ici gonfler en tant que tel est même invisible. Un quart du temps a été consacré au chargement de dessins, un quart au chargement de couleurs. Et ce n'est que quelque part dans la partie etc. que notre gonflement.

J'ai essayé de traduire la disposition XML en code et j'ai enregistré environ 0,5 ms. L'augmentation, en fait, n'est pas la plus impressionnante. Et le code est devenu beaucoup plus compliqué. Autrement dit, la réécriture n'a pas beaucoup de sens.

De plus, en pratique, beaucoup ne rencontreront pas du tout ce problème, car un long gonflement ne se produit généralement que lorsque l'application devient très volumineuse. Dans notre application VKontakte, par exemple, il y a environ 200 à 300 écrans différents, et le chargement de toutes les ressources plante. Que faire de cela n'est pas encore clair. Très probablement, vous devrez écrire votre propre gestionnaire de ressources.

Anko


Anko est récemment devenu obsolète. Quoi qu'il en soit, Anko n'est pas magique, mais un sucre syntaxique simple. Il traduit tout dans la nouvelle vue conditionnelle () de la même manière. Par conséquent, il n'y a aucun avantage d'Anko.

Litho / Flutter


Pourquoi ai-je combiné deux choses complètement indépendantes? Parce qu'il ne s'agit pas de technologie, mais de la complexité de la migration vers celle-ci. Vous ne pouvez pas simplement emprunter et passer à une nouvelle bibliothèque.

On ne sait pas si cela nous donnera un coup de fouet sur les performances. Et nous n'obtiendrons pas de nouveaux problèmes, car des millions de personnes avec des appareils complètement différents utilisent notre application chaque minute (vous n'en avez probablement même pas entendu parler environ un quart). De plus, les messages sont une très grande base de code. Il est impossible de tout réécrire instantanément. Et le faire à cause du battage médiatique de la technologie est stupide. Surtout quand le Jetpack Compose se profile quelque part très loin.

Jetpack compose


Google nous promet tous de la manne céleste sous la forme de cette bibliothèque, mais elle est toujours en alpha. Et quand ce sera dans la version - ce n'est pas clair. Nous ne savons pas non plus si nous pouvons l'obtenir sous sa forme actuelle. Il est trop tôt pour expérimenter. Laissez-le sortir en stable, laissez les principaux bugs se fermer. Et c'est seulement alors que nous regarderons dans sa direction.

Une grande vue personnalisée


Il existe une autre approche dont parlent ceux qui utilisent divers messageries instantanées: «prenez et écrivez une grande vue personnalisée, pas de hiérarchie compliquée».

Quels sont les inconvénients?

  • C'est difficile à maintenir.
  • Cela n'a pas de sens dans les réalités actuelles.

Avec Android 4.3, le système de mise en cache interne dans View a été pompé. Par exemple, onMeasure n'est pas appelé si la vue n'a pas changé. Et les résultats de la mesure précédente sont utilisés.

Avec Android 4.3-4.4, un RenderNode (DisplayList) est apparu, mettant en cache le rendu. Regardons un exemple. Supposons qu'il y ait une cellule dans la liste des dialogues: avatar, titre, sous-titre, état de lecture, heure, un autre avatar. Conditionnellement - 10 éléments. Et nous avons écrit Custom View. Dans ce cas, lorsque vous modifiez une propriété, nous mesurerons à nouveau tous les éléments. Autrement dit, il suffit de dépenser les ressources supplémentaires. Dans le cas du ViewGroup, où chaque élément est une vue distincte, lorsque vous changez une vue, nous invaliderons une seule vue (sauf lorsque cette vue affecte la taille des autres).

Sommaire


Vous avez donc appris que nous utilisons le RecyclerView classique avec des optimisations standard. Il y en a une partie non standard, où le plus important et le plus fondamental est de diviser le message en ViewHolder. Bien sûr, vous pouvez dire que cela est étroitement applicable, mais cette approche peut également être projetée sur d'autres choses, par exemple, sur un grand texte de 10 000 caractères. Il peut être divisé en paragraphes, où chaque paragraphe est un ViewHolder distinct.

Il vaut également la peine de tout maximiser sur @WorkerThread: analyse des liens, DiffUtils - déchargeant ainsi @UiThead autant que possible.

Le Global RecycledViewPool vous permet de parcourir les écrans de messages et de ne pas créer un ViewHolder à chaque fois.

Mais il y a d'autres choses importantes que nous n'avons pas encore décidées, par exemple, un long gonflement, ou plutôt, le chargement des données à partir des ressources.

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

All Articles