2014-10-14 10 views
14

Stiamo provando a rilasciare alcune App produttive con Xamarin.Forms ma uno dei nostri problemi principali è la lentezza generale tra la pressione dei pulsanti e la visualizzazione dei contenuti. Dopo alcuni esperimenti, abbiamo scoperto che anche un semplice ContentPage con 40 etichette prendono più di 100 ms per presentarsi:Perché Xamarin.Forms è così lento durante la visualizzazione di alcune etichette (specialmente su Android)?

public static class App 
{ 
    public static DateTime StartTime; 

    public static Page GetMainPage() 
    {  
     return new NavigationPage(new StartPage()); 
    } 
} 

public class StartPage : ContentPage 
{ 
    public StartPage() 
    { 
     Content = new Button { 
      Text = "Start", 
      Command = new Command(o => { 
       App.StartTime = DateTime.Now; 
       Navigation.PushAsync(new StopPage()); 
      }), 
     }; 
    } 
} 

public class StopPage : ContentPage 
{ 
    public StopPage() 
    { 
     Content = new StackLayout(); 
     for (var i = 0; i < 40; i++) 
      (Content as StackLayout).Children.Add(new Label{ Text = "Label " + i }); 
    } 

    protected override void OnAppearing() 
    { 
     ((Content as StackLayout).Children[0] as Label).Text = "Stop after " + (DateTime.Now - App.StartTime).TotalMilliseconds + " ms"; 

     base.OnAppearing(); 
    } 
} 

Soprattutto su Android si ottiene è peggio i più etichette si sta cercando di visualizzare. Il primo pulsante premuto (che è fondamentale per l'utente) richiede anche ~ 300 ms. Abbiamo bisogno di mostrare qualcosa sullo schermo in meno di 30 ms per creare una buona esperienza utente.

Perché ci vuole così tanto tempo con Xamarin.Forms per visualizzare alcune etichette semplici? E come risolvere questo problema per creare un'app per la spedizione?

Esperimenti

Il codice può essere biforcuta su GitHub a https://github.com/perpetual-mobile/XFormsPerformance

Ho anche scritto un piccolo esempio per dimostrare che il codice simile utilizzando le API native da Xamarin.Android è significativamente più veloce e fa non si ottiene più lento quando si aggiunge più contenuti: https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api

+1

Il team di assistenza Xamarin ha creato un problema di bugzilla: https://bugzilla.xamarin.com/show_bug.cgi?id=23822 – Rodja

risposta

15

Xamarin Support Team mi ha scritto:

Il team è a conoscenza del problema e stanno lavorando per ottimizzare il codice di inizializzazione UI. Potresti notare alcuni miglioramenti nelle prossime versioni di .

Aggiornamento: dopo sette mesi di funzionamento al minimo, Xamarin cambiato lo stato bug report a 'CONFERMATO'.

Buono a sapersi. Quindi dobbiamo essere pazienti. Fortunatamente Sean McKay su Xamarin Forum ha suggerito di sovrascrivere tutto il codice di layout per migliorare le prestazioni: https://forums.xamarin.com/discussion/comment/87393#Comment_87393

Ma il suo suggerimento significa anche che dobbiamo scrivere nuovamente il codice completo dell'etichetta. Ecco una versione di una FixedLabel che non fa i costosi cicli di layout e ha alcune caratteristiche come proprietà associabili per testo e colore. L'utilizzo di questa opzione invece di Label migliora le prestazioni dell'80% e oltre in base al numero di etichette e al luogo in cui si verificano.

public class FixedLabel : View 
{ 
    public static readonly BindableProperty TextProperty = BindableProperty.Create<FixedLabel,string>(p => p.Text, ""); 

    public static readonly BindableProperty TextColorProperty = BindableProperty.Create<FixedLabel,Color>(p => p.TextColor, Style.TextColor); 

    public readonly double FixedWidth; 

    public readonly double FixedHeight; 

    public Font Font; 

    public LineBreakMode LineBreakMode = LineBreakMode.WordWrap; 

    public TextAlignment XAlign; 

    public TextAlignment YAlign; 

    public FixedLabel(string text, double width, double height) 
    { 
     SetValue(TextProperty, text); 
     FixedWidth = width; 
     FixedHeight = height; 
    } 

    public Color TextColor { 
     get { 
      return (Color)GetValue(TextColorProperty); 
     } 
     set { 
      if (TextColor != value) 
       return; 
      SetValue(TextColorProperty, value); 
      OnPropertyChanged("TextColor"); 
     } 
    } 

    public string Text { 
     get { 
      return (string)GetValue(TextProperty); 
     } 
     set { 
      if (Text != value) 
       return; 
      SetValue(TextProperty, value); 
      OnPropertyChanged("Text"); 
     } 
    } 

    protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint) 
    { 
     return new SizeRequest(new Size(FixedWidth, FixedHeight)); 
    } 
} 

L'Android renderer si presenta così:

public class FixedLabelRenderer : ViewRenderer 
{ 
    public TextView TextView; 

    protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e) 
    { 
     base.OnElementChanged(e); 

     var label = Element as FixedLabel; 
     TextView = new TextView(Context); 
     TextView.Text = label.Text; 
     TextView.TextSize = (float)label.Font.FontSize; 
     TextView.Gravity = ConvertXAlignment(label.XAlign) | ConvertYAlignment(label.YAlign); 
     TextView.SetSingleLine(label.LineBreakMode != LineBreakMode.WordWrap); 
     if (label.LineBreakMode == LineBreakMode.TailTruncation) 
      TextView.Ellipsize = Android.Text.TextUtils.TruncateAt.End; 

     SetNativeControl(TextView); 
    } 

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName == "Text") 
      TextView.Text = (Element as FixedLabel).Text; 

     base.OnElementPropertyChanged(sender, e); 
    } 

    static GravityFlags ConvertXAlignment(Xamarin.Forms.TextAlignment xAlign) 
    { 
     switch (xAlign) { 
      case Xamarin.Forms.TextAlignment.Center: 
       return GravityFlags.CenterHorizontal; 
      case Xamarin.Forms.TextAlignment.End: 
       return GravityFlags.End; 
      default: 
       return GravityFlags.Start; 
     } 
    } 

    static GravityFlags ConvertYAlignment(Xamarin.Forms.TextAlignment yAlign) 
    { 
     switch (yAlign) { 
      case Xamarin.Forms.TextAlignment.Center: 
       return GravityFlags.CenterVertical; 
      case Xamarin.Forms.TextAlignment.End: 
       return GravityFlags.Bottom; 
      default: 
       return GravityFlags.Top; 
     } 
    } 
} 

E qui iOS Render:

public class FixedLabelRenderer : ViewRenderer<FixedLabel, UILabel> 
{ 
    protected override void OnElementChanged(ElementChangedEventArgs<FixedLabel> e) 
    { 
     base.OnElementChanged(e); 

     SetNativeControl(new UILabel(RectangleF.Empty) { 
      BackgroundColor = Element.BackgroundColor.ToUIColor(), 
      AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor), 
      LineBreakMode = ConvertLineBreakMode(Element.LineBreakMode), 
      TextAlignment = ConvertAlignment(Element.XAlign), 
      Lines = 0, 
     }); 

     BackgroundColor = Element.BackgroundColor.ToUIColor(); 
    } 

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName == "Text") 
      Control.AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor); 

     base.OnElementPropertyChanged(sender, e); 
    } 

    // copied from iOS LabelRenderer 
    public override void LayoutSubviews() 
    { 
     base.LayoutSubviews(); 
     if (Control == null) 
      return; 
     Control.SizeToFit(); 
     var num = Math.Min(Bounds.Height, Control.Bounds.Height); 
     var y = 0f; 
     switch (Element.YAlign) { 
      case TextAlignment.Start: 
       y = 0; 
       break; 
      case TextAlignment.Center: 
       y = (float)(Element.FixedHeight/2 - (double)(num/2)); 
       break; 
      case TextAlignment.End: 
       y = (float)(Element.FixedHeight - (double)num); 
       break; 
     } 
     Control.Frame = new RectangleF(0, y, (float)Element.FixedWidth, num); 
    } 

    static UILineBreakMode ConvertLineBreakMode(LineBreakMode lineBreakMode) 
    { 
     switch (lineBreakMode) { 
      case LineBreakMode.TailTruncation: 
       return UILineBreakMode.TailTruncation; 
      case LineBreakMode.WordWrap: 
       return UILineBreakMode.WordWrap; 
      default: 
       return UILineBreakMode.Clip; 
     } 
    } 

    static UITextAlignment ConvertAlignment(TextAlignment xAlign) 
    { 
     switch (xAlign) { 
      case TextAlignment.Start: 
       return UITextAlignment.Left; 
      case TextAlignment.End: 
       return UITextAlignment.Right; 
      default: 
       return UITextAlignment.Center; 
     } 
    } 
} 
+0

Ma come fai a sapere le dimensioni dell'etichetta a livello di vista? Voglio dire che si differenziano in base a visualizzazioni e caratteri nativi, ecc. –

+1

Spesso usiamo http://xforms-kickstarter.com/#referring-to-the-screen-size per determinare quanto spazio dovrebbe avere qualcosa. A volte è necessario utilizzare Device.OnPlatform per tenere conto delle specifiche della piattaforma. Oltre tutto, non è una grande soluzione. È una soluzione sporca che rende possibile non dare calci a Xamarin.Form dai nostri progetti dei clienti. – Rodja

+0

Il bug è stato risolto come risolto all'inizio dell'anno. Hai testato per vedere se le prestazioni sono più accettabili ora? –

4

cosa si sta misurando qui è la somma di:

  • il tempo necessario per creare la pagina
  • al layout che
  • animare sullo schermo

Su un nexus5, osservo volte nella grandezza di 300 ms per la prima convocazione, e di 120ms per quelli successivi.

Questo perché OnAppearing() verrà richiamato solo quando la vista è completamente animata sul posto.

Si può facilmente misurare il tempo di animazione, sostituendo la tua app con:

public class StopPage : ContentPage 
{ 
    public StopPage() 
    { 
    } 

    protected override void OnAppearing() 
    { 
     System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");   
     base.OnAppearing(); 
    } 
} 

e rispettare i tempi come:

134.045 ms 
2.796 ms 
3.554 ms 

questo dà alcune intuizioni: - non c'è animazione PushAsync su android (c'è su iPhone, prendendo 500ms) - la prima volta che si spinge la pagina, si paga una tassa di 120ms, a causa di una nuova allocazione. - XF sta facendo un buon lavoro a riutilizzare, se possibile, i renderizzatori di immagini

Ciò che ci interessa è il tempo per la visualizzazione delle 40 etichette, nient'altro. Cambiamo nuovamente il codice:

public class StopPage : ContentPage 
{ 
    public StopPage() 
    { 
    } 

    protected override void OnAppearing() 
    { 
     App.StartTime = DateTime.Now; 

     Content = new StackLayout(); 
     for (var i = 0; i < 40; i++) 
      (Content as StackLayout).Children.Add(new Label{ Text = "Label " + i }); 

     System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms"); 

     base.OnAppearing(); 
    } 
} 

volte osservati (su 3 chiamate):

264.015 ms 
186.772 ms 
189.965 ms 
188.696 ms 

Questo è ancora un po 'troppo, ma come contentView viene impostato prima, questa è la misurazione 40 cicli di layout, ogni nuova etichetta sta ridisegnando lo schermo.Cambiamo che:

public class StopPage : ContentPage 
{ 
    public StopPage() 
    { 
    } 

    protected override void OnAppearing() 
    { 
     App.StartTime = DateTime.Now; 

     var layout = new StackLayout(); 
     for (var i = 0; i < 40; i++) 
      layout.Children.Add(new Label{ Text = "Label " + i }); 

     Content = layout; 
     System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms"); 

     base.OnAppearing(); 
    } 
} 

E qui sono le mie misure:

178.685 ms 
110.221 ms 
117.832 ms 
117.072 ms 

questo sta diventando molto ragionevole, esp. dato che stai disegnando (istanziando e misurando) 40 etichette quando il tuo schermo può solo visualizzare 20.

C'è ancora un po 'di spazio per migliorare, ma la situazione non è così brutta come sembra. La regola dei 30 ms per i dispositivi mobili afferma che tutto ciò che richiede più di 30 ms deve essere asincrono e non bloccare l'interfaccia utente. Qui ci vuole un po 'più di 30ms per cambiare pagina, ma dal punto di vista dell'utente, non lo percepisco come lento.

+1

Per rendere dichiarazioni sul rendimento delle interazioni è necessario misurare il tempo tra l'evento click attivato fino a quando non viene attivato il contenuto appare sullo schermo; avviare il timer in "OnAppearing" è troppo tardi. – Rodja

+0

Sì, Xamarin.Forms esegue alcune misurazioni piuttosto costose quando si aggiungono viste a un layout già visibile (credo che questo sia parte del problema). Questo è il motivo per cui ho aggiunto tutti i contenuti ** prima ** "OnAppearing". Penso che Xamarin.Forms dovrebbe fare le misure del layout in un unico passaggio per risparmiare tempo. – Rodja

+0

Prova diversi numeri di etichette. Con un'etichetta è piuttosto veloce (~ 10 ms). Più etichette aggiungi più lentamente diventa. Ho scritto un piccolo esempio utilizzando Xamarin.Android su https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api che mostra che non vi è alcuna perdita di prestazioni quando si utilizzano più etichette con gli apis nativi . E per favore ricorda: questo è solo un esempio minimo. Altre viste e layout aggiungono anche lentezza all'utilizzo di Xamarin.Forms. Questo non è ragionevole !!! – Rodja

1

Nei setter dei Text e TextColor proprietà della classe FixedLabel, il codice dice:

Se il nuovo valore è "diverso" da quello corrente, non fare nulla!

dovrebbe essere con la condizione opposta, in modo che se il nuovo valore è stesso come quello attuale, allora non c'è niente da fare.

Problemi correlati