La mémoire de Xamarin iOS fuit partout.

nous utilisons Xamarin iOS depuis 8 mois et avons développé une application d'entreprise non triviale avec de nombreux écrans, fonctionnalités, contrôles imbriqués. Nous avons fait notre propre voûte MVVM, plate-forme cross BLL & DAL comme "recommandé". Nous partageons le code entre Android et même notre BLL / DAL est utilisé sur notre produit web.

tout va bien sauf maintenant dans la phase de sortie du projet nous découvrons des fuites de mémoire irréparables partout dans L'application basée sur Xamarin iOS. Nous avons suivi toutes les "lignes directrices" pour résoudre ceci, mais la réalité est que C# GC et Obj-C ARC semblent être des mécanismes incompatibles de collecte des ordures dans la façon dont ils se superposent dans la plate-forme monotouch.

la réalité que nous avons trouvée est que les cycles durs entre les objets natifs et les objets gérés se produisent et FREQUENTLY pour toute non-trivial app. Il est extrêmement facile pour cela d'arriver n'importe où vous utilisez lambdas ou les reconnaisseurs de geste par exemple. Ajouter à la complexité de MVVM et il est presque une garantie. Ne manquez qu'une de ces situations et des graphiques entiers d'objets ne seront jamais recueillis. Ces graphes attireront d'autres objets et se développeront comme un cancer, entraînant finalement une extermination rapide et impitoyable par iOS.

la réponse de Xamarin est un ajournement désintéressé de la question et une attente irréaliste que "les devs devraient éviter ces situations". Un examen attentif révèle que c'est un aveu que la Collecte des Ordures est essentiellement brisé dans Xamarin.

la prise de conscience pour moi maintenant est que vous ne recevez pas vraiment "la collecte des ordures" en Xamarin iOS dans le sens c# .NET traditionnel. Vous avez besoin d'employer des modèles "d'entretien des déchets" qui font bouger le GC et font son travail, et même alors, il ne sera jamais parfait - non déterministe.

mon entreprise a investi une fortune en essayant d'empêcher notre application de s'effondrer et/ou de manquer de mémoire. Nous avons dû disposer de façon explicite et récursive chaque fichue chose en vue et mettre en œuvre des patrons d'entretien des ordures dans l'application, juste pour arrêter les accidents et avoir un produit viable que nous pouvons vendre. Nos clients nous soutiennent et sont tolérants, mais nous savons que cela ne peut pas durer éternellement. Nous espérons que Xamarin a une équipe dédiée qui travaille sur cette question et de se faire clouer une fois pour toutes. N'a pas l'air comme ça, malheureusement.

Question Est, notre expérience est-elle l'exception ou la règle pour les applications non-trivial classe d'entreprise écrit en Xamarin?

UPDATE

voir la réponse pour la méthode et la solution de Disposseex.

44
demandé sur Herman Schoenfeld 2014-08-27 20:54:19

5 réponses

j'ai envoyé une application non-triviale écrite avec Xamarin. Beaucoup d'autres ont ainsi.

"la collecte des ordures" n'est pas magique. Si vous créez une référence qui est attachée à la racine de votre graphe d'objet et ne jamais la détacher, elle ne sera pas collectée. Ce n'est pas seulement vrai de Xamarin, mais aussi de C# on .NET, Java, etc.

button.Click += (sender, e) => { ... } est un anti-modèle, car vous n'avez pas une référence pour le lambda et vous ne pouvez pas supprimer le gestionnaire d'événements à partir de la Click événement. De même, vous devez faire attention à bien comprendre ce que vous faites lorsque vous créez des références entre des objets gérés et non gérés.

quant à "nous avons fait notre propre voûte MVVM", il y a des bibliothèques MVVM de haut niveau (MvvmCross, ReactiveUI et MVVM Light Toolkit), qui prennent tous très au sérieux les problèmes de référence et de fuite.

20
répondu anthony 2014-08-27 20:13:59

j'ai utilisé les méthodes d'extension ci-dessous pour résoudre ces problèmes de fuite de mémoire. Pensez à la scène de bataille finale du jeu D'Ender, la méthode de DisposeEx est comme ce laser et il dissocie toutes les vues et leurs objets connectés et les dispose de façon récursive et d'une manière qui ne devrait pas planter votre application.

il suffit d'appeler DisposeEx() sur la vue principale D'UIViewController lorsque vous n'avez plus besoin de ce contrôleur de vue. Si un certain UIView emboîté a des choses spéciales à disposer, ou vous ne voulez pas qu'Il dispose, mettre en œuvre ISpecialDisposable.SpecialDispose qui est appelé à la place D'IDisposable.Disposer.

NOTE: cela suppose qu'aucune instance UIImage n'est partagée dans votre application. Si c'est le cas, modifiez DisposeEx pour disposer intelligemment.

    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();
    }
19
répondu Herman Schoenfeld 2014-11-22 02:53:20

on ne peut pas être plus d'accord avec L'OP que "la collecte des ordures est essentiellement cassée en Xamarin".

voici un exemple qui montre pourquoi vous devez toujours utiliser une méthode DisposeEx() comme suggéré.

Le code suivant fuites de mémoire:

  1. Créer une classe qui hérite de la UITableViewController

    public class Test3Controller : UITableViewController
    {
        public Test3Controller () : base (UITableViewStyle.Grouped)
        {
        }
    }
    
  2. Appeler le code suivant à partir de quelque part

    var controller = new Test3Controller ();
    
    controller.Dispose ();
    
    controller = null;
    
    GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
    
  3. en utilisant des Instruments que vous verrez qu'il y a environ 274 objets persistants avec 252 KB jamais collectés.

  4. seule façon de corriger ceci est d'ajouter des fonctionnalités de Disposseex ou similaire à la fonction Dispose() et à l'appel Dispose manuellement pour s'assurer que disposing == true.

résumé: la création d'une classe dérivée de ViewController utilisable et ensuite l'élimination/annulation provoquera toujours la croissance du tas.

13
répondu Derek Massey 2017-09-18 14:30:54

iOS et Xamarin ont une relation un peu trouble. iOS utilise des comptes de référence pour gérer et disposer de sa mémoire. Le compteur de référence d'un objet est incrémenté et décrémenté lorsque les références sont ajoutés et supprimés. Lorsque le compteur de référence passe à 0, l'objet est supprimé et la mémoire libérée. Comptage de référence automatique dans L'Objectif C et aide rapide avec cela, mais il est encore difficile d'obtenir 100% droite et des indicateurs pendants et des fuites de mémoire peuvent être une douleur lors du développement utilisation des langues indigènes iOS.

lors du codage en Xamarin pour iOS, nous devons garder à l'esprit les comptes de référence car nous allons travailler avec des objets de mémoire natifs iOS. Afin de communiquer avec le système d'exploitation iOS, Xamarin crée ce que L'on appelle des pairs qui gèrent les comptes de référence pour nous. Il existe deux types de pairs – les pairs cadres et les pairs utilisateurs. Les pairs cadres sont des enveloppeurs gérés autour d'objets iOS bien connus. Les pairs cadres sont apatrides et n'ont donc pas de les références aux objets iOS sous-jacents et peuvent être nettoyées par les éboueurs au besoin – et ne causent pas de fuites de mémoire.

les pairs utilisateurs sont des objets gérés sur mesure qui sont dérivés des pairs du Framework. Les pairs utilisateurs contiennent l'état et sont donc maintenus en vie par le cadre Xamarin même si votre code ne contient aucune référence à eux-par exemple

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

nous pouvons créer un nouveau MyViewController, l'ajouter à l'arbre de vue, puis lancer un UIViewController à un MyViewController. Il se peut qu'il n'y ait aucune référence à ce MyViewController, donc Xamarin doit "root" cet objet pour le garder vivant pendant que le UIViewController sous-jacent est vivant, sinon nous perdrons les informations de l'état.

le problème est que si nous avons deux pairs utilisateurs qui font référence l'un à l'autre, alors cela crée un cycle de référence qui ne peut pas être automatiquement cassé – et cette situation arrive souvent!

tenir compte de cette affaire:-

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

    void DoSomething (object sender, EventArgs e) { ... }
}

Xamarine crée deux pairs utilisateurs qui se référencent l'un l'autre – un pour MyViewController et un autre pour MyButton (parce que nous avons un gestionnaire d'événements). Ainsi, cela créera un cycle de référence qui ne sera pas nettoyé par le collecteur d'ordures. Pour avoir cette autorisation, nous désinscrire le gestionnaire d'événements, et cela se fait généralement dans le gestionnaire ViewDidDisappear-e.g.

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

désabonnez-vous toujours à vos gestionnaires d'événements iOS.

Comment diagnostiquer ces fuites de mémoire

un bon moyen de diagnostiquer ces problèmes de mémoire est d'ajouter du code dans debug aux finaliseurs des classes dérivées des classes iOS wrapper – comme UIViewControllers. (Bien que seulement mettez cela dans vos constructions de débogage et pas dans les constructions de version parce que c'est assez lent.

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
}

ainsi, la gestion de la mémoire de Xamarin n'est pas brisée dans iOS, mais vous devez être conscient de ces ‘gotchas’ qui sont spécifiques à l'exécution sur iOS.

il y a un excellente page par Thomas Bandt appelé Xamarin.iOS Memory Pitfalls qui va cela plus en détail et fournit également des conseils et astuces.

6
répondu JasonB 2017-04-25 18:23:56

j'ai remarqué dans votre méthode DisposeEx que vous disposez de la source de la vue collection et de la source de la vue table avant de tuer les cellules visibles de cette collection. J'ai remarqué lors du débogage que la propriété Visible cells est définie à un tableau vide donc, quand vous commencez à disposer des cellules visibles, elles n'existent plus et deviennent un tableau d'éléments zéro.

une autre chose que j'ai remarqué est que vous allez exécuter des exceptions d'incohérence si vous ne supprimez pas la vue de paramètre de sa vue super, j'ai remarqué surtout avec le réglage de la disposition de la vue collection.

à part ça, j'ai dû mettre en place quelque chose de similaire de notre côté.

5
répondu dervish 2014-11-20 19:55:10