Wydajność platformy Xamarin.iOS

Niska wydajność aplikacji przedstawia się na wiele sposobów. Może to sprawić, że aplikacja wydaje się nie odpowiadać, może powodować powolne przewijanie i może zmniejszyć żywotność baterii. Jednak optymalizacja wydajności wymaga więcej niż tylko zaimplementowania wydajnego kodu. Należy również rozważyć środowisko użytkownika dotyczące wydajności aplikacji. Na przykład zapewnienie, że operacje są wykonywane bez blokowania użytkownikowi wykonywania innych działań, mogą pomóc w ulepszaniu środowiska użytkownika.

W tym dokumencie opisano techniki, których można użyć do zwiększenia wydajności i użycia pamięci w aplikacjach platformy Xamarin.iOS.

Uwaga

Przed przeczytaniem tego artykułu należy najpierw przeczytać artykuł Wydajność międzyplatformowa, w którym omówiono techniki specyficzne dla platformy, aby poprawić użycie pamięci i wydajność aplikacji utworzonych przy użyciu platformy Xamarin.

Unikaj silnych odwołań okrągłych

W niektórych sytuacjach istnieje możliwość utworzenia silnych cykli odwołań, które mogą uniemożliwić obiektom odzyskanie pamięci przez moduł odśmiecenia pamięci. Rozważmy na przykład przypadek, w którym podklasa NSObjectpochodna, taka jak klasa dziedziczona z UIViewklasy , jest dodawana do kontenera pochodnego NSObjecti jest silnie przywoływana z Objective-Cklasy , jak pokazano w poniższym przykładzie kodu:

class Container : UIView
{
    public void Poke ()
    {
    // Call this method to poke this object
    }
}

class MyView : UIView
{
    Container parent;
    public MyView (Container parent)
    {
        this.parent = parent;
    }

    void PokeParent ()
    {
        parent.Poke ();
    }
}

var container = new Container ();
container.AddSubview (new MyView (container));

Po utworzeniu Container wystąpienia ten kod będzie miał silne odwołanie do Objective-C obiektu w języku C#. MyView Podobnie wystąpienie będzie mieć silne odwołanie do Objective-C obiektu.

Ponadto wywołanie metody , aby container.AddSubview zwiększyć liczbę odwołań w wystąpieniu niezarządzanych MyView . W takim przypadku środowisko uruchomieniowe platformy Xamarin.iOS tworzy GCHandle wystąpienie w celu zachowania MyView aktywności obiektu w kodzie zarządzanym, ponieważ nie ma gwarancji, że żadne obiekty zarządzane będą do niego odwoływać się. Z perspektywy MyView kodu zarządzanego obiekt zostanie odzyskany po AddSubview wywołaniu, gdyby nie obiekt GCHandle.

Niezarządzany MyView obiekt będzie miał GCHandle wskazanie obiektu zarządzanego , znanego jako silny link. Obiekt zarządzany będzie zawierać odwołanie do Container wystąpienia. Z kolei Container wystąpienie będzie mieć zarządzane odwołanie do MyView obiektu.

W sytuacjach, gdy zawarty obiekt przechowuje łącze do kontenera, dostępnych jest kilka opcji obsługi odwołania cyklicznego:

  • Ręczne przerwanie cyklu przez ustawienie linku do kontenera na null.
  • Ręcznie usuń zawarty obiekt z kontenera.
  • Wywołaj metodę Dispose na obiektach.
  • Unikaj odwołania cyklicznego utrzymującego słabe odwołanie do kontenera. Aby uzyskać więcej informacji na temat słabych odwołań.

Używanie funkcji WeakReferences

Jednym ze sposobów zapobiegania cyklu jest użycie słabego odwołania od elementu podrzędnego do elementu nadrzędnego. Na przykład powyższy kod można napisać w następujący sposób:

class Container : UIView
{
    public void Poke ()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    WeakReference<Container> weakParent;
    public MyView (Container parent)
    {
        this.weakParent = new WeakReference<Container> (parent);
    }

    void PokeParent ()
    {
        if (weakParent.TryGetTarget (out var parent))
            parent.Poke ();
    }
}

var container = new Container ();
container.AddSubview (new MyView (container));

W tym miejscu zawarty obiekt nie będzie utrzymywać elementu nadrzędnego przy życiu. Jednak rodzic utrzymuje dziecko przy życiu za pośrednictwem połączenia wykonanego do container.AddSubView.

Dzieje się tak również w interfejsach API systemu iOS korzystających ze wzorca delegata lub źródła danych, gdzie klasa równorzędna zawiera implementację; na przykład podczas ustawiania Delegate właściwość lub właściwość DataSourceUITableView w klasie .

W przypadku klas, które są tworzone wyłącznie ze względu na implementację protokołu, na przykład IUITableViewDataSource, co można zrobić, to zamiast tworzyć podklasę, można po prostu zaimplementować interfejs w klasie i zastąpić metodę, a następnie przypisać DataSource właściwość do thisklasy .

Słaby atrybut

Program Xamarin.iOS 11.10 wprowadził [Weak] atrybut . Podobnie jak WeakReference <T>, [Weak] można użyć do przerywania silnych odwołań okrągłych, ale przy użyciu jeszcze mniejszego kodu.

Rozważmy następujący kod, który używa WeakReference <T>polecenia :

public class MyFooDelegate : FooDelegate {
    WeakReference<MyViewController> controller;
    public MyFooDelegate (MyViewController ctrl) => controller = new WeakReference<MyViewController> (ctrl);
    public void CallDoSomething ()
    {
        MyViewController ctrl;
        if (controller.TryGetTarget (out ctrl)) {
            ctrl.DoSomething ();
        }
    }
}

Użycie równoważnego kodu [Weak] jest znacznie bardziej zwięzłe:

public class MyFooDelegate : FooDelegate {
    [Weak] MyViewController controller;
    public MyFooDelegate (MyViewController ctrl) => controller = ctrl;
    public void CallDoSomething () => controller.DoSomething ();
}

Poniżej przedstawiono inny przykład użycia w [Weak] kontekście wzorca delegowania:

public class MyViewController : UIViewController
{
    WKWebView webView;

    protected MyViewController (IntPtr handle) : base (handle) { }

    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();
        webView = new WKWebView (View.Bounds, new WKWebViewConfiguration ());
        webView.UIDelegate = new UIDelegate (this);
        View.AddSubview (webView);
    }
}

public class UIDelegate : WKUIDelegate
{
    [Weak] MyViewController controller;

    public UIDelegate (MyViewController ctrl) => controller = ctrl;

    public override void RunJavaScriptAlertPanel (WKWebView webView, string message, WKFrameInfo frame, Action completionHandler)
    {
        var msg = $"Hello from: {controller.Title}";
        var alertController = UIAlertController.Create (null, msg, UIAlertControllerStyle.Alert);
        alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
        controller.PresentViewController (alertController, true, null);
        completionHandler ();
    }
}

Usuwanie obiektów z silnymi odwołaniami

Jeśli istnieje silne odwołanie i trudno jest usunąć zależność, wyczyść metodę Dispose wskaźnika nadrzędnego.

W przypadku kontenerów zastąpij metodę Dispose , aby usunąć zawarte obiekty, jak pokazano w poniższym przykładzie kodu:

class MyContainer : UIView
{
    public override void Dispose ()
    {
        // Brute force, remove everything
        foreach (var view in Subviews)
        {
              view.RemoveFromSuperview ();
        }
        base.Dispose ();
    }
}

W przypadku obiektu podrzędnego, który utrzymuje silne odwołanie do elementu nadrzędnego, wyczyść odwołanie do elementu nadrzędnego w implementacji Dispose :

class MyChild : UIView
{
    MyContainer container;
    public MyChild (MyContainer container)
    {
        this.container = container;
    }
    public override void Dispose ()
    {
        container = null;
    }
}

Aby uzyskać więcej informacji na temat wydawania silnych odwołań, zobacz Release IDisposable Resources (Zwalnianie zasobów IDisposable). W tym miejscu znajduje się również więcej informacji dotyczących odzyskiwania pamięci.

Więcej informacji

Aby uzyskać więcej informacji, zobacz Rules to Avoid Retain Cycles on Cocoa With Love( Czy jest to usterka w usłudze MonoTouch GC w witrynie StackOverflow) i Why can't MonoTouch GC kill managed objects with refcount 1? on StackOverflow (Dlaczego nie można zabić zarządzanych obiektów MonoTouch GC z refcount > 1? w witrynie StackOverflow).

Optymalizowanie widoków tabel

Użytkownicy oczekują bezproblemowego przewijania i szybkiego ładowania dla UITableView wystąpień. Jednak wydajność przewijania może cierpieć, gdy komórki zawierają głęboko zagnieżdżone hierarchie widoku lub gdy komórki zawierają złożone układy. Istnieją jednak techniki, których można użyć, aby uniknąć niskiej UITableView wydajności:

  • Użyj ponownie komórek. Aby uzyskać więcej informacji, zobacz Ponowne używanie komórek.
  • Zmniejsz liczbę widoków podrzędnych.
  • Zawartość komórki pamięci podręcznej pobierana z usługi internetowej.
  • Buforuj wysokość dowolnych wierszy, jeśli nie są identyczne.
  • Utwórz komórkę i inne widoki nieprzezroczyste.
  • Unikaj skalowania i gradientów obrazów.

Zbiorczo te techniki mogą pomóc w UITableView bezproblemowym przewijaniu wystąpień.

Ponowne używanie komórek

Podczas wyświetlania setek wierszy w obiekcie UITableViewbyłoby to strata pamięci do utworzenia setek UITableViewCell obiektów, gdy tylko niewielka liczba z nich jest wyświetlana na ekranie jednocześnie. Zamiast tego tylko komórki widoczne na ekranie można załadować do pamięci, a zawartość jest ładowana do tych ponownie użytych komórek. Zapobiega to utworzeniu wystąpienia setek dodatkowych obiektów, oszczędzaniu czasu i pamięci.

W związku z tym, gdy komórka zniknie z ekranu, jego widok można umieścić w kolejce do ponownego użycia, jak pokazano w poniższym przykładzie kodu:

class MyTableSource : UITableViewSource
{
    public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
    {
        // iOS will create a cell automatically if one isn't available in the reuse pool
        var cell = (MyCell) tableView.DequeueReusableCell (MyCellId, indexPath);

        // Perform required cell actions
        return cell;
    }
}

Gdy użytkownik przewija, wywołuje GetCell przesłonięcie, UITableView aby zażądać wyświetlenia nowych widoków. To zastąpienie wywołuje metodę DequeueReusableCell , a jeśli komórka jest dostępna do ponownego użycia, zostanie zwrócona.

Aby uzyskać więcej informacji, zobacz Ponowne użycie komórek w wypełnianiu tabeli przy użyciu danych.

Używanie nieprzezroczystych widoków

Upewnij się, że wszystkie widoki, które nie mają zdefiniowanej przezroczystości, mają swój Opaque zestaw właściwości. Zapewni to optymalne renderowanie widoków przez system rysunku. Jest to szczególnie ważne, gdy widok jest osadzony w UIScrollViewobiekcie lub jest częścią złożonej animacji. W przeciwnym razie system rysowania będzie komkładować widoki z inną zawartością, co może znacząco wpłynąć na wydajność.

Unikaj tłustych XIB

Chociaż XIB zostały w dużej mierze zastąpione przez scenorysy, istnieją pewne okoliczności, w których XIB mogą być nadal używane. Po załadowaniu pliku XIB do pamięci wszystkie jego zawartość są ładowane do pamięci, w tym do wszystkich obrazów. Jeśli XIB zawiera widok, który nie jest natychmiast używany, pamięć jest marnowana. W związku z tym w przypadku korzystania z xiB upewnij się, że istnieje tylko jeden identyfikator XIB na kontroler widoku, a jeśli to możliwe, rozdziel hierarchię widoku kontrolera widoku na oddzielne XIB.

Optymalizowanie zasobów obrazu

Obrazy to niektóre z najdroższych zasobów używanych przez aplikacje i są często przechwytywane w wysokiej rozdzielczości. W związku z tym podczas wyświetlania obrazu z pakietu aplikacji w elemecie UIImageViewupewnij się, że obraz i UIImageView mają identyczny rozmiar. Skalowanie obrazów w czasie wykonywania może być kosztowną operacją, szczególnie jeśli UIImageView element jest osadzony w obiekcie UIScrollView.

Aby uzyskać więcej informacji, zobacz Optymalizowanie zasobów obrazów w przewodniku dotyczącym wydajności międzyplatformowych.

Testowanie na urządzeniach

Rozpocznij wdrażanie i testowanie aplikacji na urządzeniu fizycznym tak szybko, jak to możliwe. Symulatory nie pasują idealnie do zachowań i ograniczeń urządzeń, dlatego ważne jest, aby jak najszybciej przetestować scenariusz urządzenia w świecie rzeczywistym.

W szczególności symulator nie w żaden sposób symuluje ograniczeń pamięci ani procesora CPU urządzenia fizycznego.

Synchronizowanie animacji z odświeżaniem ekranu

Gry mają tendencję do ciasnych pętli do uruchamiania logiki gry i aktualizowania ekranu. Typowe szybkości klatek ramek wahają się od trzydziestu do sześćdziesiąt ramek na sekundę. Niektórzy deweloperzy uważają, że powinni aktualizować ekran tak wiele razy, jak to możliwe na sekundę, łącząc symulację gry z aktualizacjami ekranu i może być kuszony, aby przejść poza sześćdziesiąt klatek na sekundę.

Jednak serwer wyświetlania wykonuje aktualizacje ekranu na górnym limicie sześciudziesiąt razy na sekundę. W związku z tym próba zaktualizowania ekranu szybciej niż ten limit może prowadzić do rozerwania ekranu i mikrowycinania. Najlepiej jest strukturę kodu, aby aktualizacje ekranu zostały zsynchronizowane z aktualizacją wyświetlania. Można to osiągnąć przy użyciu CoreAnimation.CADisplayLink klasy , która jest czasomierzem odpowiednim dla wizualizacji i gier, które działają na sześćdziesiąt klatek na sekundę.

Unikaj przezroczystości animacji rdzeni

Unikanie przezroczystości animacji rdzenia poprawia wydajność komponowania map bitowych. Ogólnie rzecz biorąc, unikaj przezroczystych warstw i rozmytych obramowań, jeśli to możliwe.

Unikanie generowania kodu

Należy unikać dynamicznego generowania kodu za pomocą System.Reflection.Emit środowiska uruchomieniowego języka dynamicznego lub środowiska uruchomieniowego języka dynamicznego, ponieważ jądro systemu iOS uniemożliwia dynamiczne wykonywanie kodu.

Podsumowanie

W tym artykule opisano i omówiono techniki zwiększania wydajności aplikacji utworzonych za pomocą platformy Xamarin.iOS. Łącznie te techniki mogą znacznie zmniejszyć ilość pracy wykonywanej przez procesor CPU i ilość pamięci zużywanej przez aplikację.