La memory de iOS de Xamarin se filtra en todas partes

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

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

La realidad que hemos encontrado es que ocurrirán ciclos duros entre los objects nativos y los objects administrados y FRECUENTEMENTE para cualquier aplicación no trivial. Es extremadamente fácil que esto ocurra en cualquier lugar donde uses lambdas o reconocedores de gestos, por ejemplo. Agregue la complejidad de MVVM y es casi una garantía. Extraiga solo una de estas situaciones y nunca se recostackrán charts integers de objects. Estos charts atraerán a otros objects y crecerán como un cáncer, lo que eventualmente resultará en un exterminio pronunciado y despiadado por iOS.

La respuesta de Xamarin es un aplazamiento desinteresado del tema y una expectativa poco realist de que "los desarrolladores deben evitar estas situaciones". La consideración cuidadosa de esto revela esto como una admisión de que la recolección de basura se rompe esencialmente en Xamarin .

La conclusión para mí ahora es que realmente no obtienes "recolección de basura" en Xamarin iOS en el sentido tradicional de c # .NET. Es necesario emplear patrones de "mantenimiento de basura" para que la GC se mueva y haga su trabajo, e incluso entonces nunca será perfecto – NO DETERMINISTA.

Mi compañía ha invertido una fortuna tratando de evitar que nuestra aplicación se cuelgue o se agote la memory. Básicamente hemos tenido que eliminar de manera explícita y recursiva todo lo que tenemos a la vista e implementar patrones de mantenimiento de basura en la aplicación, solo para detener los lockings 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 meta de una vez por todas. No se ve así, por desgracia.

La pregunta es: ¿es nuestra experiencia la exception o la regla para aplicaciones empresariales no triviales escritas en Xamarin?

ACTUALIZAR

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

Solutions Collecting From Web of "La memory de iOS de Xamarin se filtra en todas partes"

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

"Recolección de basura" no es mágico. Si crea una reference que está unida a la raíz del gráfico de object y nunca la separa, no se recostackrá. Eso no es solo cierto para Xamarin, sino para C # en .NET, Java, etc.

button.Click += (sender, e) => { ... } es un anti-patrón, porque no tiene una reference a la lambda y nunca puede eliminar el controller de events del evento Click . De manera similar, debe tener cuidado de comprender lo que está haciendo cuando crea references entre objects administrados y no administrados.

En cuanto a "Hemos creado nuestro propio arco MVVM", existen bibliotecas MVVM de alto perfil ( MvvmCross , ReactiveUI y MVVM Light Toolkit ), todas las cuales toman muy en serio los problemas de reference / filtración.

Utilicé los siguientes methods de extensión para resolver estos problemas de pérdida de memory. Piense en la escena de batalla final de Ender's Game, el método DisposeEx es como ese láser y desasocia todas las vistas y sus objects conectados y las dispone de forma recursiva y de una manera que no bloquee su aplicación.

Simplemente llame a DisposeEx () en la vista principal de UIViewController cuando ya no necesite ese controller de vista. Si alguna UIView anidada tiene cosas especiales para eliminar, o no la desea, implementar ISpecialDisposable.SpecialDispose que se llama en lugar de IDisposable.Dispose.

NOTA : esto supone que no se comparten instancias de UIImage en su aplicación. Si lo son, modifique DisposeEx para disponer de forma inteligente.

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

No podría estar más de acuerdo con el OP 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 memory:

  1. Cree una class henetworkingada UITableViewController

     public class Test3Controller : UITableViewController { public Test3Controller () : base (UITableViewStyle.Grouped) { } } 
  2. Llame al siguiente código desde algún lugar

     var controller = new Test3Controller (); controller.Dispose (); controller = null; GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced); 
  3. Al usar Instruments, verá que hay ~ 274 objects persistentes con 252 KB nunca recolectados.

  4. La única forma de solucionar esto es agregar DisposeEx o una funcionalidad similar a la function Dispose () y llamar a Dispose manualmente para garantizar que == true esté disponible.

Resumen: Crear una class derivada UITableViewController y luego deshacer / anular siempre causará que el montón crezca.

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

Otra cosa que noté es que se encontrará con excepciones de inconsistencia si no elimina la vista de parameters de su súper vista, me he dado count especialmente al configurar el layout de la vista de la colección.

Aparte de eso, tuve que implementar algo similar de nuestro lado.

iOS y Xamarin tienen una relación levemente conflictiva. iOS usa recuentos de reference para administrar y eliminar su memory. El recuento de references de un object se incrementa y decrementa cuando se agregan y eliminan references. Cuando el recuento de reference va a 0, el object se elimina y la memory se libera. El recuento automático de references en Objective C y Swift ayuda con esto, pero aún es difícil get el 100% correcto, y los pointers colgantes y las pérdidas de memory pueden ser molestos al desarrollar utilizando lenguajes nativos de iOS.

Al codificar en Xamarin para iOS, tenemos que tener en count los recuentos de reference ya que trabajaremos con objects de memory nativa de iOS. Para comunicarse con el sistema operativo iOS, Xamarin crea lo que se conoce como Pares que gestionan los recuentos de reference para nosotros. Hay dos types de pares: pares de estructura y pares de usuario. Framework Los pares son contenedores gestionados alnetworkingedor de objects iOS conocidos. Los Framework Peers son apátridas y, por lo tanto, no tienen references sólidas a los objects iOS subyacentes y pueden ser limpiados por los recolectores de basura cuando sea necesario, y no causan pérdidas de memory.

Los pares de usuario son objects gestionados personalizados que se derivan de los pares de estructura. Los pares de usuarios contienen estado y, por lo tanto, se mantienen vivos en el marco de Xamarin incluso si su código no tiene references a ellos, por ejemplo

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

Podemos crear un MyViewController nuevo, agregarlo al tree de vista, y luego lanzar un UIViewController a MyViewController. Puede que no haya references a este MyViewController, por lo que Xamarin necesita 'rootear' este object para mantenerlo vivo mientras el UIViewController subyacente está vivo, de lo contrario perderemos la información de estado.

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

Considere 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 hacen reference entre sí: uno para MyViewController y otro para MyButton (porque tenemos un manejador de events). Entonces, esto creará un ciclo de reference que el recolector de basura no aclarará. Para que esto se aclare, debemos cancelar la suscripción al controller de events, y esto generalmente se realiza en el manejador ViewDidDisappear, por ejemplo

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

Siempre cancele la suscripción a sus controlleres de events iOS.

Cómo diagnosticar estas pérdidas de memory

Una buena manera de diagnosticar estos problemas de memory es agregar un código en debugging a los finalizadores de las classs derivadas de las classs de contenedor iOS, como UIViewControllers . (Aunque solo coloque esto en sus comstackciones de debugging y no en comstackciones 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 memory de Xamarin no se rompe en iOS, pero debes tener en count estas 'gotchas' que son específicas de ejecutarse en iOS.

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