Wie wird der VK-Nachrichtenbildschirm gerendert?

Was unternimmt VK, um Rendering-Verzögerungen zu reduzieren? Wie kann man eine sehr große Nachricht anzeigen und UiThread nicht beenden? Wie können Bildlaufverzögerungen in RecyclerView reduziert werden?



Meine Erfahrung basiert auf der Arbeit beim Zeichnen eines Nachrichtenbildschirms in der VK-Android-Anwendung, in dem eine große Menge an Informationen mit einem Minimum an Bremsen auf der Benutzeroberfläche angezeigt werden muss.

Ich programmiere seit fast zehn Jahren für Android, war zuvor freiberuflich für PHP / Node.js. Jetzt - ein leitender Android-Entwickler VKontakte.

Unter dem Schnitt - Video und Transkript meines Berichts von der Mobius 2019 Moskauer Konferenz.


Der Bericht enthält drei Themen



Schau auf den Bildschirm:


Diese Nachricht befindet sich irgendwo auf fünf Bildschirmen. Und sie können durchaus bei uns sein (im Falle der Weiterleitung von Nachrichten). Standardwerkzeuge funktionieren nicht mehr. Selbst auf einem Top-Gerät kann alles zurückbleiben.
Darüber hinaus ist die Benutzeroberfläche selbst sehr unterschiedlich:

  • Ladedaten und Indikatoren,
  • Servicemeldungen
  • Text (Emoji, Link, E-Mail, Hashtags),
  • Bot-Tastatur
  • ~ 40 Möglichkeiten zum Anzeigen von Anhängen,
  • Baum der weitergeleiteten Nachrichten.

Es stellt sich die Frage, wie die Anzahl der Verzögerungen so gering wie möglich gehalten werden kann. Sowohl bei einfachen Nachrichten als auch bei Massennachrichten (Randfall aus dem obigen Video).


Standardlösungen


RecyclerView und seine Add-Ons


Es gibt verschiedene Add-Ons für RecyclerView.

  • setHasFixedSize ( boolean)

Viele Leute denken, dass dieses Flag benötigt wird, wenn die Listenelemente dieselbe Größe haben. Nach der Dokumentation ist das Gegenteil der Fall. In diesem Fall ist die Größe der RecyclerView konstant und unabhängig von den Elementen (ungefähr nicht wrap_content). Durch Setzen des Flags kann die Geschwindigkeit von RecyclerView leicht erhöht werden, sodass unnötige Berechnungen vermieden werden.

  • setNestedScrollingEnabled ( boolean)

Kleinere Optimierung, die die NestedScroll-Unterstützung deaktiviert. Wir haben keine CollapsingToolbar oder andere Funktionen in Abhängigkeit von NestedScroll auf diesem Bildschirm, daher können wir dieses Flag sicher auf false setzen.

  • setItemViewCacheSize ( cache_size)

Einrichten des internen RecyclerView-Cache.

Viele Leute denken, dass die Mechanik von RecyclerView:

  • Auf dem Bildschirm wird ein ViewHolder angezeigt.
  • Es gibt einen RecycledViewPool, in dem ViewHolder gespeichert ist.
  • ViewHolder — RecycledViewPool.

In der Praxis ist alles etwas komplizierter, da zwischen diesen beiden Dingen ein Zwischencache besteht. Es heißt ItemViewCache. Was ist seine Essenz? Wenn der ViewHolder den Bildschirm verlässt, wird er nicht im RecycledViewPool, sondern im Zwischencache (ItemViewCache) abgelegt. Alle Änderungen im Adapter gelten sowohl für den sichtbaren ViewHolder als auch für den ViewHolder im ItemViewCache. Für den ViewHolder im RecycledViewPool werden die Änderungen nicht übernommen.

Über setItemViewCacheSize können wir die Größe dieses Zwischencaches festlegen.
Je größer es ist, desto schneller ist der Bildlauf über kurze Strecken, aber die Aktualisierungsvorgänge dauern länger (aufgrund von ViewHolder.onBind usw.).

Wie RecyclerView implementiert wird und wie sein Cache strukturiert ist, ist ein ziemlich großes und komplexes Thema. Sie können einen großartigen Artikel lesen, wo sie ausführlich über alles sprechen.

Optimierung OnCreate / OnBind


Eine weitere klassische Lösung ist die Optimierung von onCreateViewHolder / onBindViewHolder:

  • einfaches Layout (wir versuchen, FrameLayout oder Custom ViewGroup so oft wie möglich zu verwenden),
  • Schwere Operationen (Parsing Links / Emoji) werden asynchron in der Phase des Ladens von Nachrichten ausgeführt.
  • StringBuilder zum Formatieren von Name, Datum usw.,
  • und andere Lösungen, die die Arbeitszeit dieser Methoden verkürzen.

Tracking Adapter.onFailedToRecyclerView ()




Sie haben eine Liste, in der einige Elemente (oder Teile davon) mit Alpha animiert sind. In dem Moment, in dem View, der gerade animiert wird, den Bildschirm verlässt, wird es nicht zu RecycledViewPool weitergeleitet. Warum? RecycledViewPool erkennt, dass die Ansicht jetzt durch das Flag View.hasTransientState animiert wird, und ignoriert sie einfach. Wenn Sie das nächste Mal nach oben und unten scrollen, wird das Bild daher nicht aus RecycledViewPool aufgenommen, sondern neu erstellt.

Die richtigste Entscheidung ist, wenn ViewHolder den Bildschirm verlässt, müssen Sie alle Animationen abbrechen.



Wenn Sie so schnell wie möglich einen Hotfix benötigen oder ein fauler Entwickler sind, können Sie in der onFailedToRecycle-Methode einfach immer true zurückgeben und alles wird funktionieren, aber ich würde nicht raten, dies zu tun.



Tracking Overdraw und Profiler


Die klassische Methode zur Erkennung von Problemen ist das Überziehen und das Nachverfolgen von Profilern.

Überzeichnen - Die Anzahl der Neuzeichnungen des Pixels: Je weniger Ebenen und je weniger Neuzeichnungen des Pixels, desto schneller. Aber nach meinen Beobachtungen wirkt sich dies in der modernen Realität nicht so sehr auf die Leistung aus.



Profiler - auch bekannt als Android Monitor, der sich in Android Studio befindet. Darin können Sie alle aufgerufenen Methoden analysieren. Öffnen Sie beispielsweise Nachrichten, scrollen Sie nach oben und unten und sehen Sie, welche Methoden aufgerufen wurden und wie lange sie gedauert haben.



In der linken Hälfte befinden sich lediglich die Android-Systemaufrufe, die zum Erstellen / Rendern eines View / ViewHolder erforderlich sind. Entweder können wir sie nicht beeinflussen, oder wir müssen uns viel Mühe geben.

Die rechte Hälfte ist unser Code, der in ViewHolder ausgeführt wird.

Der Aufrufblock bei Nummer 1 ist ein Aufruf von regulären Ausdrücken: Irgendwo haben sie übersehen und vergessen, die Operation auf den Hintergrund-Thread zu übertragen, wodurch der Bildlauf um ~ 20% verlangsamt wurde.

Anrufblock unter Nummer 2 - Fresco, eine Bibliothek zur Anzeige von Bildern. Es ist an einigen Stellen nicht optimal. Es ist noch nicht klar, was mit dieser Verzögerung zu tun ist, aber wenn wir sie lösen können, werden wir weitere ~ 15% einsparen.

Das heißt, wenn wir diese Probleme beheben, können wir eine Steigerung von ~ 35% erzielen, was ziemlich cool ist.

Diffiff


Viele von Ihnen verwenden DiffUtil in seiner Standardform: Es gibt zwei Listen - aufgerufene, verglichene und gepusste Änderungen. All dies im Haupt-Thread zu tun ist etwas teuer, da die Liste sehr groß sein kann. Daher wird die DiffUtil-Berechnung normalerweise in einem Hintergrundthread ausgeführt.

ListAdapter und AsyncListDiffer erledigen dies für Sie. Der ListAdapter erweitert den regulären Adapter und startet alles asynchron - erstellen Sie einfach eine SubmitList und die gesamte Berechnung der Änderungen wird in den internen Hintergrund-Thread übertragen. Der ListAdapter kann den Fall häufiger Aktualisierungen berücksichtigen: Wenn Sie ihn dreimal hintereinander aufrufen, wird nur das letzte Ergebnis angezeigt.

DiffUtil selbst verwenden wir nur für einige strukturelle Änderungen - das Erscheinungsbild der Nachricht, ihre Änderung und Löschung. Für einige Schnellwechseldaten ist es nicht geeignet. Zum Beispiel, wenn wir ein Foto hochladen oder Audio abspielen. Solche Ereignisse treten häufig auf - mehrmals pro Sekunde. Wenn Sie DiffUtil jedes Mal ausführen, erhalten Sie viel zusätzliche Arbeit.

Animationen


Es war einmal ein Animations-Framework - eher dürftig, aber immer noch etwas. Wir haben so mit ihm gearbeitet:

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

Das Problem ist, dass der Parameter getTranslationX () vor und nach der Animation denselben Wert zurückgibt. Dies liegt daran, dass Animation die visuelle Darstellung geändert hat, aber die physikalischen Eigenschaften nicht geändert hat.



In Android 3.0 wurde das Animator-Framework angezeigt, das korrekter ist, da es die spezifischen physischen Eigenschaften des Objekts geändert hat.



Später erschien ViewPropertyAnimator und jeder versteht den Unterschied zu Animator immer noch nicht wirklich.

Ich werde erklären. Angenommen, Sie müssen die Übersetzung diagonal ausführen - verschieben Sie die Ansicht entlang der x- und y-Achse. Höchstwahrscheinlich würden Sie typischen Code schreiben:

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

Und Sie können es kürzer machen:

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

Wenn Sie view.animate () ausführen, starten Sie ViewPropertyAnimator implizit.

Warum wird es benötigt?

  1. Einfacher zu lesen und zu pflegen.
  2. Animation der Stapeloperationen.

In unserem letzten Fall haben wir zwei Eigenschaften geändert. Wenn wir dies über Animatoren tun, werden Animations-Ticks für jeden Animator separat aufgerufen. Das heißt, setTranslationX und setTranslationY werden separat aufgerufen, und View führt Aktualisierungsvorgänge separat aus.

Im Fall von ViewPropertyAnimator erfolgt die Änderung gleichzeitig, sodass aufgrund weniger Vorgänge Einsparungen erzielt werden und die Änderung der Eigenschaften selbst besser optimiert wird.

Sie können dies mit Animator erreichen, müssen jedoch mehr Code schreiben. Darüber hinaus können Sie mit ViewPropertyAnimator sicher sein, dass die Animationen so weit wie möglich optimiert werden. Warum? Android hat einen RenderNode (DisplayList). Sehr grob zwischenspeichern sie das Ergebnis von onDraw und verwenden es beim Neuzeichnen. ViewPropertyAnimator arbeitet direkt mit RenderNode zusammen und wendet Animationen darauf an, um OnDraw-Aufrufe zu vermeiden.

Viele View-Eigenschaften können sich auch direkt auf den RenderNode auswirken, jedoch nicht auf alle. Das heißt, wenn Sie ViewPropertyAnimator verwenden, wird garantiert, dass Sie den effizientesten Weg verwenden. Wenn Sie plötzlich eine Animation haben, die mit ViewPropertyAnimator nicht mehr möglich ist, sollten Sie vielleicht darüber nachdenken und sie ändern.

Animationen: TransitionManager


Normalerweise assoziieren die Leute, dass dieses Framework verwendet wird, um von einer Aktivität zu einer anderen zu wechseln. Tatsächlich kann es anders verwendet werden und die Implementierung der Animation von Strukturänderungen erheblich vereinfachen. Angenommen, wir haben einen Bildschirm, auf dem eine Sprachnachricht abgespielt wird. Wir schließen es mit einem Kreuz und der Würfel geht hoch. Wie kann man das machen? Die Animation ist ziemlich kompliziert: Der Player schließt mit Alpha, während er sich nicht durch die Übersetzung bewegt, sondern seine Höhe ändert. Gleichzeitig steigt unsere Liste und ändert auch die Höhe.



Wenn der Player Teil der Liste wäre, wäre die Animation recht einfach. Bei uns ist der Player jedoch kein Element der Liste, sondern eine völlig unabhängige Ansicht.

Vielleicht würden wir anfangen, eine Art Animator zu schreiben, dann würden wir auf Probleme stoßen, abstürzen, Krücken sägen und den Code verdoppeln. Und würde so etwas wie den Bildschirm unten bekommen.



Mit TransitionManager können Sie alles einfacher machen:

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

Alle Animationen erfolgen automatisch unter der Haube. Es sieht nach Magie aus, aber wenn Sie tief in das Innere gehen und sehen, wie es funktioniert, stellt sich heraus, dass der TransitionManager einfach alle Ansichten abonniert, die Änderungen in ihren Eigenschaften erfasst, den Diff berechnet, die erforderlichen Animatoren oder ViewPropertyAnimator erstellt und bei Bedarf alles so effizient wie möglich erledigt. Mit TransitionManager können wir Animationen im Nachrichtenbereich schnell und einfach implementieren.

Kundenspezifische Lösungen




Dies ist die grundlegendste Sache, auf der die Leistung und die folgenden Probleme beruhen. Was tun, wenn Ihre Nachricht auf 10 Bildschirmen angezeigt wird? Wenn Sie darauf achten, befinden sich alle unsere Elemente genau untereinander. Wenn wir akzeptieren, dass ViewHolder nicht eine Nachricht ist, sondern Dutzende verschiedener ViewHolder, wird alles viel einfacher.

Für uns ist es kein Problem, dass die Nachricht zu 10 Bildschirmen geworden ist, da jetzt nur sechs ViewHolders in einem konkreten Beispiel angezeigt werden. Wir haben ein einfaches Layout, der Code ist einfacher zu pflegen und es gibt keine besonderen Probleme, außer einem - wie geht das?



Es gibt einfache ViewHolders - dies sind klassische Datumstrennzeichen, Mehr laden und so weiter. Und BaseViewHolder - bedingt grundlegender ViewHolder für die Nachricht. Es hat eine grundlegende Implementierung und mehrere spezifische - TextViewHolder, PhotoViewHolder, AudioViewHolder, ReplyViewHolder und so weiter. Es gibt ungefähr 70 von ihnen.

Wofür ist BaseViewHolder verantwortlich?


BaseViewHolder ist nur für das Zeichnen des Avatars und des gewünschten Blasenstücks sowie der Linie für weitergeleitete Nachrichten verantwortlich - blau links.



Die konkrete Implementierung des Inhalts wird bereits von anderen BaseViewHolder-Erben durchgeführt: TextViewHolder zeigt nur Text an, FwdSenderViewHolder - der Autor der weitergeleiteten Nachricht, AudioMsgViewHolder - die Sprachnachricht usw.



Es gibt ein Problem: Was tun mit der Breite? Stellen Sie sich eine Nachricht auf mindestens zwei Bildschirmen vor. Es ist nicht sehr klar, welche Breite eingestellt werden soll, da die Hälfte sichtbar ist, die Hälfte nicht sichtbar ist (und noch nicht einmal erstellt wurde). Absolut alles kann nicht gemessen werden, weil es liegt. Ich muss leider ein wenig krücken. Es gibt einfache Fälle, in denen die Nachricht sehr einfach ist: Nur Text oder Sprache - besteht im Allgemeinen aus einem Element.



Verwenden Sie in diesem Fall den klassischen wrap_content. In einem komplexen Fall, wenn eine Nachricht aus mehreren Teilen besteht, nehmen wir jedem ViewHolder eine feste Breite und erzwingen sie. Speziell hier - 220 dp.



Wenn der Text sehr kurz ist und die Nachricht weitergeleitet wird, befindet sich rechts ein leeres Feld. Dem entgeht nichts, denn Leistung ist wichtiger. Mehrere Jahre lang gab es keine Beschwerden - vielleicht hat es jemand bemerkt, aber im Allgemeinen haben sich alle daran gewöhnt.



Es gibt Randfälle. Wenn wir auf eine Nachricht mit einem Aufkleber antworten, können wir die Breite speziell für einen solchen Fall angeben, damit sie hübscher aussieht.

Wir teilen uns beim Laden von Nachrichten in ViewHolders auf: Wir starten das Laden der Nachricht im Hintergrund, konvertieren sie in ein Element und sie werden direkt in ViewHolders angezeigt.



Global RecycledViewPool


Die Mechanik der Verwendung unseres Messenger ist so, dass die Leute nicht im selben Chat sitzen, sondern ständig zwischen ihnen wechseln. Beim Standardansatz werden der RecycledViewPool (und der darin enthaltene ViewHolder) einfach zerstört, wenn wir in den Chat gegangen sind und ihn verlassen haben, und jedes Mal, wenn wir Ressourcen für die Erstellung des ViewHolder aufwenden.

Dies kann durch den globalen RecycledViewPool gelöst werden:

  • RecycledViewPool lebt im Rahmen von Application als Singleton.
  • auf dem Nachrichtenbildschirm wiederverwendet, wenn der Benutzer zwischen den Bildschirmen wechselt;
  • als RecyclerView.setRecycledViewPool (Pool) festlegen.

Es gibt Fallstricke, es ist wichtig, sich an zwei Dinge zu erinnern:

  • Sie gehen zum Bildschirm, klicken zurück, beenden. Das Problem ist, dass die auf dem Bildschirm angezeigten ViewHolders weggeworfen und nicht in den Pool zurückgegeben werden. Dies wird wie folgt behoben:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • RecycledViewPool unterliegt Einschränkungen: Für jeden ViewType können nicht mehr als fünf ViewHolders gespeichert werden.

Wenn 9 Textansichten auf dem Bildschirm angezeigt werden, werden nur fünf Elemente an den RecycledViewPool zurückgegeben, und der Rest wird weggeworfen. Sie können die Größe des RecycledViewPool ändern:
RecycledViewPool.setMaxRecycledViews (viewType, size)

Es ist jedoch traurig, mit den Händen auf jeden ViewType zu schreiben, da Sie Ihren RecycledViewPool schreiben, den Standard erweitern und NoLimit erstellen können. Über den Link können Sie die fertige Implementierung herunterladen.

DiffUtil ist nicht immer nützlich


Hier ist ein klassischer Fall: Herunterladen, Abspielen einer Audiospur und einer Sprachnachricht. In diesem Fall ruft DiffUtil Spam auf.



Unser BaseViewHolder verfügt über eine abstrakte updateUploadProgress-Methode.

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

Um ein Ereignis auszulösen, müssen wir alle sichtbaren ViewHolder umgehen:

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

Dies ist eine einfache Operation. Es ist unwahrscheinlich, dass mehr als zehn ViewHolder auf dem Bildschirm angezeigt werden. Ein solcher Ansatz kann grundsätzlich nicht zurückbleiben. Wie finde ich sichtbaren ViewHolder? Eine naive Implementierung wäre ungefähr so:

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

Aber es gibt ein Problem. Der zuvor erwähnte Zwischencache, ItemViewCache, enthält aktive ViewHolders, die einfach nicht auf dem Bildschirm angezeigt werden. Der obige Code hat keine Auswirkungen auf sie. Auch wir können sie nicht direkt ansprechen. Und dann helfen uns Krücken. Erstellen Sie ein WeakSet, in dem Links zu ViewHolder gespeichert sind. Außerdem reicht es uns, dieses WeakSet einfach zu umgehen.

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-Überlagerung


Betrachten Sie das Beispiel von Geschichten. Wenn eine Person zuvor auf eine Geschichte mit einem Aufkleber reagiert hat, haben wir sie folgendermaßen angezeigt:



Sie sieht ziemlich hässlich aus. Ich wollte es besser machen, weil die Geschichten hell sind und wir dort ein kleines Quadrat haben. Wir wollten so etwas bekommen:



Es gibt ein Problem: Unsere Nachricht ist in ViewHolder aufgeteilt, sie befinden sich streng untereinander, aber hier überlappen sie sich. Sofort ist nicht klar, wie dies zu lösen ist. Sie können einen anderen ViewType "Verlauf + Aufkleber" oder "Verlauf + Sprachnachricht" erstellen. Anstelle von 70 ViewType hätten wir also 140 ... Nein, wir müssen uns etwas Bequemeres einfallen lassen.



Eine Ihrer Lieblingskrücken in Android fällt Ihnen ein. Zum Beispiel haben wir etwas getan, aber Pixel Perfect konvergiert nicht. Um dies zu beheben, müssen Sie alles löschen und von Grund auf neu schreiben, aber Faulheit. Infolgedessen können wir margin = -2dp (negativ) machen, und jetzt passt alles zusammen. Ein solcher Ansatz kann hier jedoch nicht angewendet werden. Wenn Sie einen negativen Rand festlegen, wird der Aufkleber verschoben, aber der Platz, den er belegt, bleibt leer. Aber wir haben ItemDecoration, wo itemOffset wir eine negative Zahl machen können. Und es funktioniert! Als Ergebnis erhalten wir die erwartete Überlagerung und gleichzeitig bleibt ein Paradigma bestehen, in dem jeder ViewHolder befreundet ist.

Eine schöne Lösung in einer Zeile.

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

Idlehandler


Dies ist ein Fall mit einem Sternchen, es ist komplex und wird in der Praxis nicht so oft benötigt, aber es ist wichtig, über die Existenz einer solchen Methode Bescheid zu wissen.

Zunächst erkläre ich Ihnen, wie der Haupt-UiThread-Thread funktioniert. Das allgemeine Schema: Es gibt eine Aufgabenereigniswarteschlange, in der Aufgaben über handler.post festgelegt werden, und eine Endlosschleife, die diese Warteschlange durchläuft. Das heißt, UiThread ist nur während (wahr). Wenn es Aufgaben gibt, führen wir sie aus. Wenn nicht, warten wir, bis sie angezeigt werden.



In unserer üblichen Realität ist Handler dafür verantwortlich, Aufgaben in die Warteschlange zu stellen, und Looper umgeht die Warteschlange endlos. Es gibt Aufgaben, die für die Benutzeroberfläche nicht sehr wichtig sind. Zum Beispiel liest ein Benutzer eine Nachricht - es ist für uns nicht so wichtig, wenn wir sie jetzt oder nach 20 ms auf der Benutzeroberfläche anzeigen. Der Benutzer wird den Unterschied nicht bemerken. Dann lohnt es sich vielleicht, diese Aufgabe nur dann im Hauptthread auszuführen, wenn sie kostenlos ist? Das heißt, es wäre schön zu wissen, wann die Linie awaitNewTask aufgerufen wird. In diesem Fall verfügt Looper über einen addIdleHandler, der ausgelöst wird, wenn der Code task.isEmpty ausgelöst wird.

Looper.myQueue (). AddIdleHandler ()

Und dann sieht die einfachste Implementierung von IdleHandler folgendermaßen aus:

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

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

Auf die gleiche Weise können Sie einen ehrlichen Kaltstart der Anwendung messen.

Emoji


Wir verwenden unsere benutzerdefinierten Emoji anstelle von System-Emoji. Hier ist ein Beispiel dafür, wie Emojis in verschiedenen Jahren auf verschiedenen Plattformen aussahen. Das linke und rechte Emoji sind ziemlich nett, aber in der Mitte ...



Es gibt ein zweites Problem:



Jede Reihe ist das gleiche Emoji, aber die Emotionen, die sie reproduzieren, sind unterschiedlich. Ich mag das Bild unten rechts am meisten, ich verstehe immer noch nicht, was es bedeutet.

Es gibt ein Fahrrad von VKontakte. In ~ 2014 haben wir ein Emoji leicht verändert. Vielleicht erinnert sich jemand - "Marshmallow" war. Nach seinem Wechsel begann ein Mini-Aufstand. Natürlich erreichte er nicht das Level „Return the Wall“, aber die Reaktion war ziemlich interessant. Und dies sagt uns, wie wichtig es ist, Emoji zu interpretieren.

Wie Emojis hergestellt werden: Wir haben eine große Bitmap, in der sie alle in einem großen „Atlas“ zusammengefasst sind. Es gibt mehrere davon - unter verschiedenen DPI. Und es gibt ein EmojiSpan, das Informationen enthält: Ich zeichne "so und so" Emoji, es befindet sich in so und so einer Bitmap an so und so Ort (x, y).
Und es gibt eine ReplacementSpan, mit der Sie unter Span etwas anstelle von Text anzeigen können.
Das heißt, Sie finden Emoji im Text, umschließen es mit EmojiSpan, und das System zeichnet das gewünschte Emoji anstelle des System-Emoji.



Alternativen


Aufblasen


Jemand könnte sagen, da das Aufblasen langsam ist, warum nicht einfach ein Layout mit Ihren Händen erstellen und das Aufblasen vermeiden. Und beschleunigen Sie damit alles, indem Sie den 100500 ViewHolder meiden. Es ist eine Täuschung. Bevor Sie etwas tun, lohnt es sich, es zu messen.

Android hat eine Debug-Klasse, es hat startMethodTracing und stopMethodTracing.

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

Auf diese Weise können wir Informationen über die Ausführungszeit eines bestimmten Codeteils sammeln.



Und wir sehen, dass hier das Aufblasen als solches sogar unsichtbar ist. Ein Viertel der Zeit wurde mit dem Laden von Zeichenmaterial verbracht, ein Viertel mit dem Laden von Farben. Und nur irgendwo im etc-Teil ist unser Aufblasen.

Ich habe versucht, das XML-Layout in Code zu übersetzen und etwa 0,5 ms gespeichert. Der Anstieg ist in der Tat nicht der beeindruckendste. Und der Code ist viel komplizierter geworden. Das heißt, das Umschreiben macht nicht viel Sinn.

Darüber hinaus werden in der Praxis viele überhaupt nicht auf dieses Problem stoßen, da ein langes Aufblasen normalerweise nur dann auftritt, wenn die Anwendung sehr groß wird. In unserer VKontakte-Anwendung gibt es beispielsweise ungefähr 200 bis 300 verschiedene Bildschirme, und das Laden aller Ressourcen stürzt ab. Was damit zu tun ist, ist noch unklar. Höchstwahrscheinlich müssen Sie Ihren eigenen Ressourcenmanager schreiben.

Anko


Anko ist vor kurzem veraltet. Wie auch immer, Anko ist keine Magie, sondern einfacher syntaktischer Zucker. Es übersetzt alles auf die gleiche Weise in die bedingte neue Ansicht (). Daher gibt es keinen Vorteil von Anko.

Litho / Flattern


Warum habe ich zwei völlig unabhängige Dinge kombiniert? Denn hier geht es nicht um Technologie, sondern um die Komplexität der Migration dorthin. Sie können nicht einfach ausleihen und in eine neue Bibliothek wechseln.

Es ist unklar, ob dies uns zu einer Leistungssteigerung führen wird. Und wir werden keine neuen Probleme bekommen, da Millionen von Menschen mit völlig unterschiedlichen Geräten jede Minute unsere Anwendung verwenden (Sie haben wahrscheinlich noch nicht einmal von einem Viertel davon gehört). Darüber hinaus sind Nachrichten eine sehr große Codebasis. Es ist unmöglich, alles sofort neu zu schreiben. Und es wegen des Hype der Technologie zu tun, ist dumm. Besonders wenn das Jetpack Compose irgendwo weit weg auftaucht.

Jetpack komponieren


Google verspricht uns alle Manna vom Himmel in Form dieser Bibliothek, aber es ist immer noch in Alpha. Und wann es in der Veröffentlichung sein wird - es ist nicht klar. Ob wir es in seiner jetzigen Form bekommen können, ist ebenfalls unklar. Zum Experimentieren ist es noch zu früh. Lassen Sie es im Stall herauskommen, lassen Sie die Hauptfehler schließen. Und nur dann werden wir in seine Richtung schauen.

Eine große benutzerdefinierte Ansicht


Es gibt einen anderen Ansatz, über den diejenigen, die verschiedene Instant Messenger verwenden, sprechen: "Nehmen und schreiben Sie eine große benutzerdefinierte Ansicht, keine komplizierte Hierarchie."

Was sind die Nachteile?

  • Es ist schwer zu pflegen.
  • In der gegenwärtigen Realität macht es keinen Sinn.

Mit Android 4.3 wurde das interne Caching-System in View gepumpt. Beispielsweise wird onMeasure nicht aufgerufen, wenn sich die Ansicht nicht geändert hat. Und die Ergebnisse der vorherigen Messung werden verwendet.

Unter Android 4.3-4.4 wurde ein RenderNode (DisplayList) angezeigt, der das Rendern zwischenspeichert. Schauen wir uns ein Beispiel an. Angenommen, die Liste der Dialoge enthält eine Zelle: Avatar, Titel, Untertitel, Lesestatus, Uhrzeit, einen anderen Avatar. Bedingt - 10 Elemente. Und wir haben Custom View geschrieben. In diesem Fall werden beim Ändern einer Eigenschaft alle Elemente neu gemessen. Das heißt, geben Sie einfach die zusätzlichen Ressourcen aus. Im Fall der ViewGroup, in der jedes Element eine separate Ansicht ist, wird beim Ändern einer Ansicht nur eine Ansicht ungültig (außer wenn diese Ansicht die Größe anderer beeinflusst).

Zusammenfassung


Sie haben also gelernt, dass wir den klassischen RecyclerView mit Standardoptimierungen verwenden. Es gibt einen Teil von nicht standardmäßigen, bei denen die Aufteilung der Nachricht in ViewHolder am wichtigsten und grundlegendsten ist. Natürlich kann man sagen, dass dies eng anwendbar ist, aber dieser Ansatz kann auch auf andere Dinge projiziert werden, beispielsweise auf einen großen Text von 10.000 Zeichen. Es kann in Absätze unterteilt werden, wobei jeder Absatz ein separater ViewHolder ist.

Es lohnt sich auch, alles auf @WorkerThread zu maximieren: Parsen von Links, DiffUtils - und dabei @UiThead so weit wie möglich entladen.

Mit dem globalen RecycledViewPool können Sie zwischen Nachrichtenbildschirmen wechseln und nicht jedes Mal einen ViewHolder erstellen.

Aber es gibt noch andere wichtige Dinge, die wir noch nicht entschieden haben, zum Beispiel ein langes Aufblasen oder vielmehr das Laden von Daten aus Ressourcen.

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

All Articles