visual tutorial studio developer apps c# ios xamarin.ios garbage-collection xamarin

c# - studio - xamarin tutorial



La memoria iOS de Xamarin se filtra por todos lados (5)

Hemos estado usando Xamarin iOS durante los últimos 8 meses y hemos desarrollado una aplicación empresarial no trivial con muchas pantallas, funciones y controles anidados. Hemos hecho nuestro propio arco MVVM, plataforma cruzada BLL y DAL como "recomendado". Compartimos el código entre Android e incluso nuestro BLL / DAL se usa en nuestro producto web.

Todo está bien, excepto que ahora, en la fase de lanzamiento del proyecto, descubrimos pérdidas de memoria irreparables en todas partes en la aplicación Xamarin iOS. Hemos seguido todas las "pautas" para resolver esto, pero la realidad es que C # GC y Obj-C ARC parecen ser mecanismos incompatibles de recolección de basura en la forma actual en que se superponen entre sí en la plataforma monotouch.

La realidad que hemos encontrado es que los ciclos duros entre objetos nativos y objetos administrados ocurrirán y con FRECUENCIA para cualquier aplicación no trivial. Es extremadamente fácil que esto suceda en cualquier lugar donde uses lambdas o reconocedores de gestos, por ejemplo. Agregue la complejidad de MVVM y es casi una garantía. Perdí solo una de estas situaciones y gráficos completos de objetos nunca serán recopilados. Estos gráficos atraerán a otros objetos y crecerán como un cáncer, lo que eventualmente resultará en un exterminio inmediato y despiadado por iOS.

La respuesta de Xamarin es un aplazamiento desinteresado del problema y una expectativa poco realista de que "los desarrolladores deben evitar estas situaciones". La cuidadosa consideración de esto revela esto como una admisión de que la recolección de basura está esencialmente rota en Xamarin .

La realización para mí ahora es que realmente no se obtiene "recolección de basura" en Xamarin iOS en el sentido tradicional de c # .NET. Necesita emplear patrones de "mantenimiento de basura" para que la GC se mueva y haga su trabajo, y aun así nunca será perfecta, NO DETERMINÍSTICA.

Mi empresa ha invertido una fortuna tratando de evitar que nuestra aplicación se cuelgue y / o se quede sin memoria. Básicamente, hemos tenido que eliminar de forma explícita y recursiva todo lo que está a la vista e implementar patrones de mantenimiento de basura en la aplicación, solo para detener los bloqueos y tener un producto viable que podamos vender. Nuestros clientes son comprensivos y tolerantes, pero sabemos que esto no puede durar para siempre. Esperamos que Xamarin tenga un equipo dedicado que trabaje en este tema y lo clave de una vez por todas. No se ve, desafortunadamente.

La pregunta es, ¿nuestra experiencia es la excepción o la regla para las aplicaciones no triviales de clase empresarial escritas en Xamarin?

ACTUALIZAR

Vea la respuesta para el método y solución DisposeEx.


He enviado una aplicación no trivial escrita con Xamarin. Muchos otros también lo han hecho.

La "recolección de basura" no es mágica. Si crea una referencia que está unida a la raíz de su gráfico de objetos y nunca la desprende, no se recopilará. Eso no solo es cierto para Xamarin, sino también para C # en .NET, Java, etc.

button.Click += (sender, e) => { ... } es un antipatrón porque no tiene una referencia al lambda y nunca puede eliminar el controlador de eventos del evento Click . Del mismo modo, debe tener cuidado de comprender lo que hace cuando crea referencias entre objetos administrados y no administrados.

En cuanto a "Hemos hecho nuestro propio arco MVVM", hay bibliotecas MVVM de alto perfil ( MvvmCross , ReactiveUI y MVVM Light Toolkit ), todas las cuales toman muy en serio los problemas de referencia / fuga.


No podría estar más de acuerdo con el OP en que "la recolección de basura está esencialmente rota en Xamarin".

Aquí hay un ejemplo que muestra por qué siempre debe usar un método DisposeEx () como se sugiere.

El siguiente código pierde memoria:

  1. Crea una clase heredera UITableViewController

    public class Test3Controller : UITableViewController { public Test3Controller () : base (UITableViewStyle.Grouped) { } }

  2. Llame al siguiente código desde algún lado

    var controller = new Test3Controller (); controller.Dispose (); controller = null; GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);

  3. Al utilizar Instrumentos verás que hay ~ 274 objetos persistentes con 252 KB nunca recogidos.

  4. La única manera de solucionar esto es agregar DisposeEx o una funcionalidad similar a la función Dispose () y llamar a Dispose manualmente para garantizar que se elimine == verdadero.

Resumen: la creación de una clase derivada de UITableViewController y la eliminación / nulidad siempre harán que crezca el almacenamiento dinámico.


Noté en su método DisposeEx que elimina el origen de vista de colección y la fuente de vista de tabla antes de matar las celdas visibles de esa colección. Al depurar me di cuenta de que la propiedad de las celdas visibles se establece en una matriz vacía, por lo tanto, cuando comienzas a disponer las celdas visibles, ya no "existen", por lo que se convierte en una matriz de cero elementos.

Otra cosa que noté es que se encontrarán con excepciones de inconsistencia si no elimina la vista de parámetros de su súper vista, lo he notado especialmente al establecer el diseño de la vista de colección.

Aparte de eso, he tenido que implementar algo similar de nuestro lado.


Usé los métodos de extensión a continuación para resolver estos problemas de pérdida de memoria. Piensa en la escena de batalla final del juego de Ender, el método DisposeEx es como ese láser y desasocia todas las vistas y sus objetos conectados y los desecha recursivamente y de una manera que no debería bloquear tu aplicación.

Simplemente llame a DisposeEx () en la vista principal de UIViewController cuando ya no necesite ese controlador de vista. Si algún UIView anidado tiene elementos especiales para eliminar, o si no desea que se eliminen, implemente ISpecialDisposable.SpecialDispose que se llama en lugar de IDisposable.Dispose.

NOTA : esto supone que no hay instancias de UIImage compartidas en su aplicación. Si lo son, modifique DisposeEx para disponer inteligentemente.

public static void DisposeEx(this UIView view) { const bool enableLogging = false; try { if (view.IsDisposedOrNull()) return; var viewDescription = string.Empty; if (enableLogging) { viewDescription = view.Description; SystemLog.Debug("Destroying " + viewDescription); } var disposeView = true; var disconnectFromSuperView = true; var disposeSubviews = true; var removeGestureRecognizers = false; // WARNING: enable at your own risk, may causes crashes var removeConstraints = true; var removeLayerAnimations = true; var associatedViewsToDispose = new List<UIView>(); var otherDisposables = new List<IDisposable>(); if (view is UIActivityIndicatorView) { var aiv = (UIActivityIndicatorView)view; if (aiv.IsAnimating) { aiv.StopAnimating(); } } else if (view is UITableView) { var tableView = (UITableView)view; if (tableView.DataSource != null) { otherDisposables.Add(tableView.DataSource); } if (tableView.BackgroundView != null) { associatedViewsToDispose.Add(tableView.BackgroundView); } tableView.Source = null; tableView.Delegate = null; tableView.DataSource = null; tableView.WeakDelegate = null; tableView.WeakDataSource = null; associatedViewsToDispose.AddRange(tableView.VisibleCells ?? new UITableViewCell[0]); } else if (view is UITableViewCell) { var tableViewCell = (UITableViewCell)view; disposeView = false; disconnectFromSuperView = false; if (tableViewCell.ImageView != null) { associatedViewsToDispose.Add(tableViewCell.ImageView); } } else if (view is UICollectionView) { var collectionView = (UICollectionView)view; disposeView = false; if (collectionView.DataSource != null) { otherDisposables.Add(collectionView.DataSource); } if (!collectionView.BackgroundView.IsDisposedOrNull()) { associatedViewsToDispose.Add(collectionView.BackgroundView); } //associatedViewsToDispose.AddRange(collectionView.VisibleCells ?? new UICollectionViewCell[0]); collectionView.Source = null; collectionView.Delegate = null; collectionView.DataSource = null; collectionView.WeakDelegate = null; collectionView.WeakDataSource = null; } else if (view is UICollectionViewCell) { var collectionViewCell = (UICollectionViewCell)view; disposeView = false; disconnectFromSuperView = false; if (collectionViewCell.BackgroundView != null) { associatedViewsToDispose.Add(collectionViewCell.BackgroundView); } } else if (view is UIWebView) { var webView = (UIWebView)view; if (webView.IsLoading) webView.StopLoading(); webView.LoadHtmlString(string.Empty, null); // clear display webView.Delegate = null; webView.WeakDelegate = null; } else if (view is UIImageView) { var imageView = (UIImageView)view; if (imageView.Image != null) { otherDisposables.Add(imageView.Image); imageView.Image = null; } } else if (view is UIScrollView) { var scrollView = (UIScrollView)view; scrollView.UnsetZoomableContentView(); } var gestures = view.GestureRecognizers; if (removeGestureRecognizers && gestures != null) { foreach(var gr in gestures) { view.RemoveGestureRecognizer(gr); gr.Dispose(); } } if (removeLayerAnimations && view.Layer != null) { view.Layer.RemoveAllAnimations(); } if (disconnectFromSuperView && view.Superview != null) { view.RemoveFromSuperview(); } var constraints = view.Constraints; if (constraints != null && constraints.Any() && constraints.All(c => c.Handle != IntPtr.Zero)) { view.RemoveConstraints(constraints); foreach(var constraint in constraints) { constraint.Dispose(); } } foreach(var otherDisposable in otherDisposables) { otherDisposable.Dispose(); } foreach(var otherView in associatedViewsToDispose) { otherView.DisposeEx(); } var subViews = view.Subviews; if (disposeSubviews && subViews != null) { subViews.ForEach(DisposeEx); } if (view is ISpecialDisposable) { ((ISpecialDisposable)view).SpecialDispose(); } else if (disposeView) { if (view.Handle != IntPtr.Zero) view.Dispose(); } if (enableLogging) { SystemLog.Debug("Destroyed {0}", viewDescription); } } catch (Exception error) { SystemLog.Exception(error); } } public static void RemoveAndDisposeChildSubViews(this UIView view) { if (view == null) return; if (view.Handle == IntPtr.Zero) return; if (view.Subviews == null) return; view.Subviews.Update(RemoveFromSuperviewAndDispose); } public static void RemoveFromSuperviewAndDispose(this UIView view) { view.RemoveFromSuperview(); view.DisposeEx(); } public static bool IsDisposedOrNull(this UIView view) { if (view == null) return true; if (view.Handle == IntPtr.Zero) return true;; return false; } public interface ISpecialDisposable { void SpecialDispose(); }


iOS y Xamarin tienen una relación ligeramente problemática. iOS usa recuentos de referencia para administrar y eliminar su memoria. El recuento de referencias de un objeto se incrementa y disminuye cuando se agregan y eliminan referencias. Cuando el recuento de referencias va a 0, el objeto se elimina y la memoria se libera. El recuento automático de referencias en Objective C y Swift ayudan con esto, pero todavía es difícil obtener el 100% de la información correcta y los punteros colgantes y las pérdidas de memoria pueden ser un problema al desarrollar con los lenguajes nativos de iOS.

Al codificar en Xamarin para iOS, tenemos que tener en cuenta los recuentos de referencia ya que trabajaremos con los objetos de memoria nativos de iOS. Para comunicarse con el sistema operativo iOS, Xamarin crea lo que se conoce como Peers, que administra los recuentos de referencia para nosotros. Hay dos tipos de Pares: Pares de Marco y Pares de Usuario. Los Pares de Marco son envolturas administradas alrededor de objetos iOS conocidos. Los Pares de Marco son apátridas y, por lo tanto, no tienen referencias fuertes a los objetos subyacentes de iOS y pueden ser limpiados por los recolectores de basura cuando sea necesario, y no causan pérdidas de memoria.

Los Peers de Usuario son objetos gestionados personalizados que se derivan de Framework Peers. Los pares de usuario contienen estado y, por lo tanto, los mantiene el marco Xamarin, incluso si su código no tiene referencias sobre ellos, por ejemplo:

public class MyViewController : UIViewController { public string Id { get; set; } }

Podemos crear un nuevo MyViewController, agregarlo al árbol de visualización y luego enviar un UIViewController a un MyViewController. Puede que no haya referencias a este MyViewController, por lo que Xamarin necesita ''rootear'' este objeto para mantenerlo vivo mientras el UIViewController subyacente esté activo, de lo contrario perderemos la información de estado.

El problema es que si tenemos dos Pares de Usuario que se referencian entre sí, esto crea un ciclo de referencia que no se puede romper automáticamente, ¡y esta situación ocurre a menudo!

Considera este caso:

public class MyViewController : UIViewController { public override void ViewDidAppear(bool animated) { base.ViewDidAppear (animated); MyButton.TouchUpInside =+ DoSomething; } void DoSomething (object sender, EventArgs e) { ... } }

Xamarin crea dos pares de usuarios que se referencian entre sí: uno para MyViewController y otro para MyButton (porque tenemos un controlador de eventos). Por lo tanto, esto creará un ciclo de referencia que no será aclarado por el recolector de basura. Para aclarar esto, debemos anular la suscripción del controlador de eventos, y esto generalmente se realiza en el controlador ViewDidDisappear, por ejemplo:

public override void ViewDidDisappear(bool animated) { ProcessButton.TouchUpInside -= DoSomething; base.ViewDidDisappear (animated); }

Siempre anule la suscripción a los controladores de eventos de iOS.

Cómo diagnosticar estas pérdidas de memoria

Una buena forma de diagnosticar estos problemas de memoria es agregar algún código en la depuración a los Finalizadores de las clases derivadas de las clases contenedoras iOS, como UIViewControllers . (Aunque solo ponga esto en sus compilaciones de depuración y no en compilaciones de lanzamiento porque es razonablemente lento.

public partial class MyViewController : UIViewController { #if DEBUG static int _counter; #endif protected MyViewController (IntPtr handle) : base (handle) { #if DEBUG Interlocked.Increment (ref _counter); Debug.WriteLine ("MyViewController Instances {0}.", _counter); #endif } #if DEBUG ~MyViewController() { Debug.WriteLine ("ViewController deleted, {0} instances left.", Interlocked.Decrement(ref _counter)); } #endif }

Entonces, la administración de la memoria de Xamarin no está rota en iOS, pero debes ser consciente de estas ''trampas'' que son específicas para correr en iOS.

Hay una excelente página de Thomas Bandt llamada Xamarin.iOS Memory Pitfalls que entra en esto con más detalle y también proporciona algunos consejos y sugerencias muy útiles.