Layouts für Android – Ein Performance-Vergleich

12. Juli 2018

Jeder der native Android-Apps entwickelt, kommt tagtäglich mit Layouts in Berührung. Jedes Widget bzw. jede View, sei es ein einfacher Text, ein Button oder ein Bild muss über Layouts auf dem Handybildschirm positioniert werden. Dabei stehen verschiedene Layouts zur Verfügung, die auch ineinander verschachtelt werden können. Die zur Verfügung stehenden Layout-Arten haben sich über lange Zeit nicht geändert. Die wichtigsten sind hier aufgelistet:

  • FrameLayout: Jede View wird relativ zum Container positioniert, unabhängig von anderen Widgets. Überlappungen sind daher möglich.
  • LinearLayout: Alle Widgets werden je nach Anforderung unter- oder nebeneinander angeordnet. Überlappungen sind ausgeschlossen.
  • RelativeLayout: Ein RelativeLayout verhält sich zunächst wie ein FrameLayout, jedoch können Views auch relativ zu anderen Views positioniert werden.

Bei der IO 2016 hat Google ein neues Layout vorgestellt: das ConstraintLayout. Anders als die herkömmlichen Layouts ist es nicht in die Plattform integriert, sondern wird in einer eigenen Support-Library ausgeliefert. Der Vorteil hierbei ist, dass es keine plattformspezifischen Unterschiede im Verhalten gibt, die App dadurch aber auch größer wird. Wie beim RelativeLayout kann man die Position von Views relativ zur Position anderer Views bestimmen. Es ist aber flexibler und kann dadurch Verschachtelungen vieler Layouts verhindern. Das soll das ConstraintLayout für komplexe Layouts performanter machen – das verspricht zumindest Google. Wenn es nach den Google-Entwicklern geht, ist das RelativeLayout tot, und es soll nur noch das ConstraintLayout verwendet werden. Aber ist das auch sinnvoll? Wir haben die Performance zwischen der herkömmlichen Kombination von RelativeLayouts und LinearLayouts mit dem ConstraintLayout verglichen.

Vom XML zur fertigen View

Um die Ergebnisse von Performance-Messungen interpretieren zu können, ist es zunächst hilfreich zu wissen, wie das Rendern einer View bei Android funktioniert. Der ganze Render-Prozess besteht aus vier Phasen: Inflation, Measurement, Layout und Draw.

Als Inflation bezeichnet man bei Android das Parsen von Layouts im XML-Format und die anschließende Erstellung der View-Objekte. Die meisten Apps bei Jamit Labs verwenden eine einzige Activity, in der Fragments den eigentlichen Inhalt darstellen. Der Inflation-Prozess passiert dort in der sprechenden Methode onCreateView. Bei Activities ohne Fragments erfolgt die Inflation in der onCreate-Methode über die Methode setContentView. Aber im Grunde passiert bei Fragments und Activities das Gleiche.
Sobald alle View-Objekte erzeugt und initialisiert wurden, folgt die Measurement-Phase. Hier wird die Methode onMeasure vom System für das oberste Element der View-Hierarchie aufgerufen. Dieses ruft wiederum dieselbe Methode auf alle Kindelemente auf, die dann ihre Breite und Höhe festlegen. Je nachdem, was im XML festgelegt wurde, kann die übergeordnete View dabei über sogenannte MeasureSpecs Einschränkungen wie eine exakte oder maximale Höhe/Breite festlegen.
Hat jede View in der Hierarchie ihre Maße erhalten, wird in der Layout-Phase erneut der ganze Baum durchlaufen. Jede ViewGroup positioniert die untergeordneten Views anhand deren zuvor bestimmten Größe.
Als letzten Schritt zeichnen sich die einzelnen Views über die onDraw Methode.

Test-Verfahren

Das Ziel des Performance-Vergleich ist es natürlich, herauszufinden, welches Layout insgesamt am schnellsten rendert. Das Android-Framework bietet leider keine Möglichkeit herauszufinden, wann die Draw-Phase beendet ist, über einen ViewTreeObserver.OnGlobalLayoutChangeListener kann man aber immerhin herausfinden, wann die Layout-Phase abgeschlossen ist. Da die zu vergleichenden Layouts selbst nur Container sind und onDraw nicht implementieren, ist dies zum Vergleich aber ausreichend.

Es kann aber auch interessant sein zu wissen, wie lange die Ausführung der einzelnen Phasen dauert. Eine Möglichkeit um die Dauer der Inflation-Phase zu errechnen, ist die Zeit zu messen, die die Ausführung der onCreateView-Methode benötigt. Dies funktioniert natürlich nur, solange dort neben der Inflation kein zusätzlicher Code ausgeführt wird. Für den Performance-Test ist das aber kein Problem.
Um die Dauer der Measure- und Layout-Phasen herauszufinden, werden die entsprechenden Methoden überschrieben und darin die Zeit gemessen, die die Ausführung der Methoden benötigt. Das sieht im Kotlin-Code dann so aus:

class TrackingConstraintLayout: ConstraintLayout {
    // ...
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        Log.d("LAYOUT_TEST", "ConstraintLayout: onMeasure took ${measureTimeMillis {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }} ms")
    }
}

Als Test-Geräte wurden ein Samsung Galaxy Note 8 mit Android 7.1.1 und ein LG G2 mit Android 4.4 ausgewählt, um auch eventuelle Unterschiede zwischen einem aktuellen Topgerät und einem älteren Gerät herausfinden zu können.

Das ConstraintLayout wird grundsätzlich in der zum Zeitpunkt des Tests aktuellen stabile Version 1.1.2 getestet. Die erste Alpha-Version der Version 2.0 wird in einem Test auch untersucht.

Um sicherzugehen, dass Ergebnisse nicht durch Zufall zustande kamen, wird jede Messung 5x durchgeführt. Damit Ausreißer möglichst wenig Einfluss haben, wird der Median in den Ergebnissen verwendet.

Genug Einleitung, auf zum Test

Das Ziel dieses Artikels ist es, herauszufinden, wie sich die verschiedenen Layout-Typen in der Praxis schlagen. Deshalb bietet es sich auch an, praxisnahe Beispiellayouts zu verwenden. Die meisten Apps haben irgendeine Art von Login, deshalb wurde als erstes Test-Layout ein Login-Screen ausgewählt. Dieser hat typischerweise eine Fortschrittsanzeige, einen kurzen erklärenden Text, Eingabefelder mit Beschriftungen und Buttons.

Mockup eines typischen Login-Screens

Das oben stehende Mockup wurde sowohl mit Hilfe des Constraint Layouts in flacher Hierarchie, als auch mit einem RelativeLayout und verschachtelten LinearLayouts erstellt. Auf dem Endgerät ist im Rendering kein Unterschied feststellbar, die XMLs sehen jedoch sehr unterschiedlich aus. Im konventionellen Ansatz sind fünf zusätzliche Layouts vorhanden.

<ConstraintLayout>
    <TextView />
    <TextView />
    <TextView />
    <TextView />
    <TextView />
    <EditText />
    <TextView />
    <EditText />
    <Button />
    <Button />
</ConstraintLayout>
<RelativeLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
            <TextView />
            <TextView />
        </LinearLayout>
        <TextView />
        <LinearLayout>
            <TextView />
            <EditText />
        </LinearLayout>
        <LinearLayout>
            <TextView />
            <EditText />
        </LinearLayout>
    </LinearLayout>
    <LinearLayout>
        <Button />
        <Button />
    </LinearLayout>
</RelativeLayout>

Mit der oben beschriebenen Methodik wurden für beide Layouttypen die Dauer der einzelnen Render-Phasen berechnet. Die Ergebnisse sind ziemlich überraschend (alle Angaben in ms, Median aus 5 Messungen):

onCreateView onMeasure onLayout GlobalLayoutChangeListener dispatchDraw
Samsung / RelativeLayout 41.38 4.73 0.63 54.81 3.09
Samsung / ConstraintLayout 78.78 39.64 0.57 127.62 2.79
LG / RelativeLayout 23.86 19.27 0.51 55.18 4.96
LG / ConstraintLayout 29.66 36.94 0.26 76.88 4.56

Auf dem aktuellen Samsung-Gerät ist das ConstraintLayout um ein Vielfaches langsamer als die herkömmliche Layout-Kombination aus Relative- und LinearLayouts. Das Inflaten des Layouts und insbesonders die Measure-Phase tragen dazu bei. Durch die flache Hierarchie ist das ConstraintLayout etwas schneller in der Layout-Phase und beim eigentlichen Zeichnen, wobei es sich hier um Millisekundenbruchteile handelt, die nicht ins Gewicht fallen. Die Messwerte sind alle ziemlich stabil, die höchste Standardabweichung bei den 5 Messungen sind 2 Millisekunden für die onMeasure-Methode und den GlobalLayoutChangeListener, alle anderen Standardabweichungen sind unter einer Millisekunde.
Auch auf dem älteren LG-Gerät schneidet das ConstraintLayout schlechter ab als die herkömmliche Layoutkombination. Das Inflaten geht insgesamt schneller als beim Samsung-Gerät. Eine mögliche Erklärung hierfür ist die kleinere Bildschirmauflösung. Die Tendenzen sind aber die Gleichen: Das ConstraintLayout ist minimal schneller in der Layout- und Drawphase, aber erheblich langsamer in der Inflation- und Measurephase.

Vergleich der ConstraintLayout-Versionen

onCreateView onMeasure onLayout GlobalLayoutChangeListener dispatchDraw
v1.1.2 stable 78.78 39.64 0.57 127.62 2.79
v2.0.0 alpha 1 78.97 37.27 0.53 124.00 2.42

Im Direktvergleich der aktuellen stabilen Version 1.1.2 des ConstraintLayouts mit der erst kürzlich (Juni 2018) veröffentlichten Alpha-Version ist die Alpha-Version in der Measure-Phase 6% besser, allerdings liegt diese Abweichung gerade so noch im Bereich der Standardabweichung aus den fünf Messungen, ist also nicht signifikant. Der Test wurde mit dem Samsung Galaxy Note 8 und dem oben abgebildeten Loginscreen-Layout durchgeführt.

Bewertung

Das ConstraintLayout ist ohne Frage eine praktische Ergänzung für die Android Layout-Landschaft. Guidelines, Chains, automatische Animationen und vieles mehr können das Erstellen von Layouts vereinfachen. Dennoch kommt diese "Zauberei" nicht ohne Performance-Einbußen: Anders als von Google versprochen ist ein ConstraintLayout nicht mindestens so schnell wie ein RelativeLayout mit verschachtelten Layouts, sondern kann deutlich langsamer sein. Unabhängig von der Performance geben zusätzliche Layouts dem XML auch mehr Struktur und machen das Markup einfacher zu lesen und zu bearbeiten. Die vielen zusätzlichen Attribute wie layout_constraintTop_toBottomOf plustern das Markup auf, und erschweren es im Vergleich zum LinearLayout, Elemente einzufügen, da jedes Mal die Attribute weiterer Elemente bearbeitet werden müssen.

Unser Tipp also: Dort ein ConstraintLayout verwenden, wo es sinnig ist, wenn das gleiche Layout aber mit herkömmlichen Layouts ohne Mehraufwand umgesetzt werden kann, darf auch ruhig noch ein RelativeLayout eingesetzt werden.