Hi,
My apps always were kind of slow when I added many elements to a page or a layout, even if they are just Labels and Buttons. So I used the Xamarin Profiler and ILSpy to figure out what exactly it making it slow.
Let's say we have a simple page with many labels, like this:
public class LabelPage : ContentPage
{
StackLayout stack = new StackLayout ();
Label topLabel = new Label ();
public LabelPage ()
{
Content = new ScrollView { Content = stack };
stack.Children.Add (topLabel);
}
protected override void OnAppearing ()
{
base.OnAppearing ();
Stopwatch watch = new Stopwatch ();
watch.Start ();
for (int i = 0; i < 500; i++) {
stack.Children.Add (new Label { Text = "#" + i });
}
watch.Stop ();
topLabel.Text = "without suppress:" + watch.ElapsedMilliseconds + " ms";
}
}
We create a StackLayout and add 500 labels to it. On a Nexus 5, it takes about 4 seconds! (Of course usually, you would use a ListView for this. It's a simplified example.)
What happens now, is that every time Children.Add is called, the "ChildAdded" event of VisualElement is fired. The "VisualElementPackager" (in Xamarin.Forms.Platform.Android) listens to this event and executes the following method of VisualElementPackager:
private void OnChildAdded(object sender, ElementEventArgs e)
{
VisualElement visualElement = e.Element as VisualElement;
if (visualElement != null)
{
this.AddChild(visualElement, null, null, false);
}
this.EnsureChildOrder();
}
Surprisingly, getting a renderer for the new child element and adding it only takes about 20% of the time in my use case. "EnsureChildOrder" uses 80% of the time. Lets take a look what is does:
private void EnsureChildOrder()
{
for (int i = 0; i < this.renderer.Element.get_LogicalChildren().get_Count(); i++)
{
VisualElement visualElement = (VisualElement)this.renderer.Element.get_LogicalChildren().get_Item(i);
if (visualElement != null)
{
IVisualElementRenderer visualElementRenderer = Platform.GetRenderer(visualElement);
this.renderer.ViewGroup.BringChildToFront(visualElementRenderer.ViewGroup);
}
}
}
As it turnes out, most of the time is spent on calling the android/java method "BringChildToFront". After adding a new child, the children are reordered again, and again, and again, even tough they were all in the right order in the first place!
=> If you have any layout with N children, the method "BringChildToFront" is executed N*(N+1)/2 times. So 10 children 55 times, 50 children 1275 times, and for 100 children the method is called 5050 times!
So I used Mono.Cecil to change the EnsureChildOrder method so it is only executed if a specific constant is not set. (source code: https://gist.github.com/tobiasschulz/468e2508afef1d0da465). Then I modified my page:
VisualElementPackager.SuppressBringChildToFront = true;
for (int i = 0; i < 500; i++) {
stack.Children.Add (new Label { Text = "#" + i });
}
VisualElementPackager.SuppressBringChildToFront = false;
watch.Stop ();
And suddenly, it only takes about 800 milliseconds to render 500 labels, instead of 4300 milliseconds!