Rendimiento de Xamarin.iOS

El mal rendimiento de una aplicación se manifiesta de muchas formas. Puede hacer que parezca que una aplicación deja de responder, puede ocasionar un desplazamiento lento y puede reducir la duración de la batería. La optimización del rendimiento conlleva mucho más que la mera implementación de código eficaz. También debe tenerse en cuenta la experiencia de rendimiento de la aplicación del usuario. Por ejemplo, asegurarse de que las operaciones se ejecuten sin evitar que el usuario realice otras actividades puede ayudar a mejorar su experiencia.

Este documento describe las técnicas que pueden usarse para mejorar el rendimiento y el uso de memoria de las aplicaciones Xamarin.iOS.

Nota:

Antes de leer este artículo, debería leer Rendimiento multiplataforma, donde se explican técnicas no específicas de una plataforma para mejorar el uso de memoria y el rendimiento de las aplicaciones compiladas con la plataforma Xamarin.

Evitar referencias circulares seguras

En algunas situaciones es posible crear referencias circulares seguras que podrían impedir que el recolector de elementos no utilizados reclame memoria de los objetos. Por ejemplo, considere el caso en el que una subclase derivada de NSObject, como una clase que hereda de UIView, se agrega a un contenedor derivado de NSObject y se hace referencia firmemente a esta desde Objective-C, como se muestra en el ejemplo de código siguiente:

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));

Cuando este código cree la instancia Container, el objeto de C# tendrá una referencia fuerte a un objeto de Objective-C. De forma similar, la instancia MyView también tendrá una referencia fuerte a un objeto de Objective-C.

Además, la llamada a container.AddSubview aumentará el recuento de referencias en la instancia MyView no administrada. Cuando esto sucede, el tiempo de ejecución de Xamarin.iOS crea una instancia GCHandle para mantener activo el objeto MyView en código administrado, porque no hay ninguna garantía de que otros objetos administrados mantengan una referencia al mismo. Desde una perspectiva de código administrado, el objeto MyView se reclamaría tras la llamada a AddSubview si no fuera por el GCHandle.

El objeto MyView no administrado tendrá un GCHandle que apunta al objeto administrado, conocido como vínculo fuerte. El objeto administrado contendrá una referencia a la instancia Container. A su vez, la instancia Container tendrá una referencia administrada al objeto MyView.

En casos donde un objeto contenido mantiene un vínculo a su contenedor, hay varias opciones disponibles para solucionar la referencia circular:

  • Interrumpir manualmente el ciclo estableciendo el vínculo al contenedor en null.
  • Quitar manualmente el objeto contenido del contenedor.
  • Llamar a Dispose en los objetos.
  • Evitar la referencia circular manteniendo una referencia débil al contenedor. Para más información sobre las referencias débiles.

Empleo de referencias débiles

Una manera de evitar un ciclo es usar una referencia débil del elemento secundario al primario. Por ejemplo, el código anterior podría escribirse así:

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));

Aquí, el objeto contenido no mantiene activo el elemento primario. Pero el elemento primario mantiene activo el elemento secundario durante la llamada realizada a container.AddSubView.

Esto también ocurre en las API de iOS que usan el modelo de delegado o de origen de datos, donde una clase del mismo nivel contiene la implementación; por ejemplo, al establecer la propiedad Delegate propiedad oDataSource en la UITableView clase .

En el caso de las clases que se crean exclusivamente para la implementación de un protocolo, por ejemplo, IUITableViewDataSource, en lugar de crear una subclase, puede implementar la interfaz de la clase, invalidar el método y asignar la propiedad DataSource a this.

Atributo Weak

Xamarin.iOS 11.10 ha presentado el atributo [Weak]. Al igual que WeakReference <T>, [Weak] puede usarse para interrumpir referencias circulares seguras, pero con aún menos código.

Considere el código siguiente, que usa WeakReference <T>:

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 ();
        }
    }
}

El código equivalente con [Weak] es mucho más conciso:

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

A continuación hay otro ejemplo del uso de [Weak] en el contexto del modelo de delegación:

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 ();
    }
}

Eliminación de objetos con referencias seguras

Si existe una referencia fuerte y la dependencia es difícil de quitar, haga que un método Dispose borre el puntero primario.

Para los contenedores, invalide el método Dispose para quitar los objetos contenidos, como se muestra en el ejemplo de código siguiente:

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

Para un objeto secundario que mantiene una referencia fuerte a su elemento primario, borre la referencia al elemento primario en la implementación de Dispose:

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

Para más información sobre la liberación de las referencias fuertes, vea Liberar los recursos de IDisposable. Aquí también hay más información sobre la recolección de elementos no utilizados.

Más información

Para obtener más información, consulte Reglas para evitar los ciclos de retención en cocoa con amor, ¿se trata de un error en MonoTouch GC en StackOverflow y por qué no se pueden eliminar objetos administrados monoTouch con refcount > 1? en StackOverflow.

Optimizar las vistas de tabla

Los usuarios esperan un desplazamiento sin problemas y unos tiempos de carga rápidos para las instancias de UITableView. Pero el rendimiento del desplazamiento puede verse afectado si las celdas contienen jerarquías de vista profundamente anidadas o si las celdas contienen diseños complejos. Pero hay técnicas que se pueden usar para evitar un mal rendimiento de UITableView:

  • Reutilizar las celdas. Para más información, vea Reutilizar celdas.
  • Reducir el número de subvistas.
  • Almacenar en caché el contenido de celda recuperado de un servicio web.
  • Almacenar en caché el alto de todas las filas si no son idénticas.
  • Hacer que la celda, y otras vistas, sean opacas.
  • Evitar la escala de imágenes y los degradados.

Colectivamente, estas técnicas facilitan que las instancias de UITableView se desplacen sin problemas.

Reutilizar las celdas

Al mostrar cientos de filas en una UITableView, sería un desperdicio de memoria crear cientos de objetos UITableViewCell cuando solo un pequeño número de ellos se muestra en pantalla a la vez. En su lugar, se pueden cargar en la memoria solo las celdas visibles en la pantalla y cargar el contenido en estas celdas reutilizadas. Esto evita la creación de instancias de cientos de objetos adicionales, con lo que se ahorra tiempo y memoria.

Por tanto, cuando una celda desaparece de la pantalla, su vista puede colocarse en una cola para su reutilización, como se muestra en el ejemplo de código siguiente:

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;
    }
}

Cuando el usuario se desplaza, la UITableView llama a la invalidación de GetCell para solicitar nuevas vistas para mostrar. Esta invalidación llama al método DequeueReusableCell y, si hay una celda disponible para su reutilización, se devuelve.

Para más información, vea Reutilizar celdas en Rellenar una tabla con datos.

Usar vistas opacas

Asegúrese de que las vistas que no tienen transparencia definida tienen establecida su propiedad Opaque. Esto garantizará que el sistema de dibujo represente las vistas de forma óptima. Esto es especialmente importante cuando se incrusta una vista en una UIScrollView o cuando forma parte de una animación compleja. De lo contrario, el sistema de dibujo compondrá las vistas con otro contenido, lo que puede afectar considerablemente al rendimiento.

Evitar FAT XIB

Aunque en gran medida los XIB se han sustituido por guiones gráficos, hay algunas circunstancias donde todavía se pueden usar XIB. Cuando se carga un XIB en memoria, todo su contenido se carga en la memoria, incluidas todas las imágenes. Si el XIB contiene una vista que no se está usando inmediatamente, la memoria se desperdicia. Por tanto, cuando use XIB, asegúrese de que hay solo un XIB por cada controlador de vista y, si es posible, separe la jerarquía de vistas del controlador de vista en XIB independientes.

Optimizar los recursos de imagen

Las imágenes son uno de los recursos con más consumo usados por las aplicaciones y se suelen capturar en altas resoluciones. Por tanto, al mostrar una imagen del paquete de la aplicación en una UIImageView, asegúrese de que la imagen y UIImageView tienen el mismo tamaño. La escala de imágenes en tiempo de ejecución puede ser una operación costosa, especialmente si la UIImageView está incrustada en una UIScrollView.

Para más información, vea Optimizar los recursos de imagen en la guía Rendimiento multiplataforma.

Realizar pruebas en dispositivos

Comience a implementar y probar una aplicación en un dispositivo físico tan pronto como sea posible. Los simuladores no igualan a la perfección los comportamientos y las limitaciones de los dispositivos, por lo que es importante realizar las pruebas en un escenario de dispositivos reales tan pronto como sea posible.

En concreto, el simulador no simula de ningún modo las restricciones de memoria o CPU de un dispositivo físico.

Sincronizar animaciones con la actualización de la pantalla

Los juegos tienden a tener bucles estrechos para ejecutar la lógica del juego y actualizar la pantalla. Las velocidades de fotogramas típicas oscilan entre 30 y 60 fotogramas por segundo. Algunos desarrolladores creen que deben actualizar la pantalla tantas veces como sea posible por segundo, combinando la simulación del juego con las actualizaciones de la pantalla, y es posible que se vean tentados a superar los 60 fotogramas por segundo.

Pero el servidor de pantalla realiza las actualizaciones de pantalla a un límite máximo de 60 veces por segundo. Por tanto, intentar actualizar la pantalla más rápidamente que este límite puede provocar que la pantalla se entrecorte y se produzcan micro-parpadeos. Es mejor estructurar el código para que las actualizaciones de pantalla se sincronicen con la actualización en pantalla. Esto puede lograrse mediante el uso de la clase CoreAnimation.CADisplayLink, que es un temporizador adecuado para la visualización y juegos que se ejecuta a 60 fotogramas por segundo.

Evitar la transparencia de la animación principal

Evitar la transparencia de la animación de principal mejora el rendimiento de la composición de mapas de bits. En general, evite capas transparentes y bordes difuminados si es posible.

Evitar la generación de código

La generación de código dinámicamente con System.Reflection.Emit o Dynamic Language Runtime debe evitarse porque el kernel de iOS impide la ejecución de código dinámico.

Resumen

En este artículo se han descrito y explicado técnicas para aumentar el rendimiento de las aplicaciones compiladas con Xamarin.iOS. En conjunto, estas técnicas pueden reducir considerablemente la cantidad de trabajo que está realizando una CPU y la cantidad de memoria consumida por una aplicación.