web-dev-qa-db-fra.com

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 et contrôles imbriqués. Nous avons fait notre propre MVVM Arch, multiplateforme BLL & DAL comme "recommandé". Nous partageons du code entre Android et même notre BLL/DAL est utilisé sur notre produit Web.

Tout va bien sauf que 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 iOS Xamarin. Nous avons suivi toutes les "lignes directrices" pour résoudre ce problème, mais la réalité est que C # GC et Obj-C ARC semblent être des mécanismes de collecte de déchets incompatibles dans la façon dont ils se superposent dans la plate-forme monotouch.

La réalité que nous avons constatée est que les cycles difficiles entre les objets natifs et les objets gérés VOLONTÉ se produire et SOUVENT pour toute application non triviale. Il est extrêmement facile que cela se produise partout où vous utilisez des lambdas ou des reconnaisseurs de gestes par exemple. Ajoutez la complexité de MVVM et c'est presque une garantie. Ne manquez qu'une seule de ces situations et des graphiques entiers d'objets ne seront jamais collectés. Ces graphiques attireront d'autres objets et se développeront comme un cancer, entraînant éventuellement une extermination rapide et sans merci par iOS.

La réponse de Xamarin est un report non intéressé du problème et une attente irréaliste que "les développeurs devraient éviter ces situations". Un examen attentif de cela révèle que cela admet que La récupération de place est essentiellement interrompue dans Xamarin.

La réalisation pour moi maintenant est que vous n'obtenez pas vraiment de "garbage collection" dans Xamarin iOS au sens traditionnel de c # .NET. Vous devez utiliser des modèles de "maintenance des ordures" pour que le GC bouge et fasse son travail, et même alors il ne sera jamais parfait - NON DÉTERMINISTIQUE.

Mon entreprise a investi une fortune en essayant d'empêcher notre application de planter et/ou de manquer de mémoire. Nous avons essentiellement dû éliminer explicitement et récursivement chaque fichue chose en vue et implémenter des modèles de maintenance des ordures dans l'application, juste pour arrêter les plantages et avoir un produit viable que nous pouvons vendre. Nos clients sont solidaires et tolérants, mais nous savons que cela ne peut pas durer éternellement. Nous espérons que Xamarin aura une équipe dédiée qui travaillera sur ce problème et le clouera une fois pour toutes. Ça ne ressemble pas à ça, malheureusement.

La question est, notre expérience est-elle l'exception ou la règle pour les applications de classe entreprise non triviales écrites en Xamarin?

MISE À JOUR

Voir la réponse pour la méthode et la solution DisposeEx.

49
Herman Schoenfeld

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 d'Ender's Game, la méthode DisposeEx est comme ce laser et dissocie toutes les vues et leurs objets connectés et les élimine récursivement et d'une manière qui ne devrait pas planter votre application.

Appelez simplement DisposeEx () sur la vue principale de UIViewController lorsque vous n'avez plus besoin de ce contrôleur de vue. Si certains UIView imbriqués ont des choses spéciales à supprimer, ou si vous ne voulez pas qu'ils soient supprimés, implémentez ISpecialDisposable.SpecialDispose qui est appelé à la place de IDisposable.Dispose.

NOTE : cela suppose qu'aucune instance UIImage n'est partagée dans votre application. S'ils le sont, modifiez DisposeEx pour les éliminer 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.ForEach(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();
    }
21
Herman Schoenfeld

J'ai expédié une application non triviale écrite avec Xamarin. Beaucoup d'autres aussi.

La "collecte des ordures" n'est pas magique. Si vous créez une référence attachée à la racine de votre graphique d'objet et que vous ne la détachez jamais, elle ne sera pas collectée. Cela n'est pas seulement vrai pour Xamarin, mais pour C # sur .NET, Java, etc.

button.Click += (sender, e) => { ... } est un anti-modèle, car vous n'avez pas de référence au lambda et vous ne pouvez jamais supprimer le gestionnaire d'événements de l'événement Click. De même, vous devez veiller à 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 MVVM Arch", il existe des bibliothèques MVVM de haut niveau ( MvvmCross , ReactiveUI , et MVVM Light Toolkit ) , qui prennent tous les problèmes de référence/fuite très au sérieux.

25
anthony

Je ne pourrais pas être plus d'accord avec l'OP que "la récupération de place est essentiellement cassée dans Xamarin".

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

Le code suivant fuit la mémoire:

  1. Créer une classe dont hérite UITableViewController

    public class Test3Controller : UITableViewController
    {
        public Test3Controller () : base (UITableViewStyle.Grouped)
        {
        }
    }
    
  2. Appelez le code suivant quelque part

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

  4. Le seul moyen de résoudre ce problème consiste à ajouter DisposeEx ou une fonctionnalité similaire à la fonction Dispose () et à appeler Dispose manuellement pour garantir l'élimination de == true.

Résumé: La création d'une classe dérivée UITableViewController, puis la suppression/l'annulation entraînera toujours la croissance du tas.

13
Derek Massey

iOS et Xamarin ont une relation légèrement troublée. iOS utilise des décomptes de références pour gérer et éliminer sa mémoire. Le nombre de références d'un objet est incrémenté et décrémenté lorsque des références sont ajoutées et supprimées. Lorsque le nombre de références passe à 0, l'objet est supprimé et la mémoire libérée. Comptage automatique des références dans Objective C et Swift aide à cela, mais il est toujours difficile d'avoir 100% raison et les pointeurs pendants et les fuites de mémoire peuvent être pénibles lors du développement en utilisant des langues iOS natives.

Lors du codage dans Xamarin pour iOS, nous devons garder à l'esprit le nombre de références car nous travaillerons avec des objets de mémoire native iOS. Afin de communiquer avec le système d'exploitation iOS, Xamarin crée ce que l'on appelle des pairs qui gèrent pour nous le nombre de références. Il existe deux types de pairs: les pairs du framework et les pairs utilisateurs. Les pairs du framework sont des wrappers gérés autour d'objets iOS bien connus. Les homologues du framework sont sans état et ne contiennent donc aucune référence forte aux objets iOS sous-jacents et peuvent être nettoyés par les garbage collector si nécessaire - et ne provoquent pas de fuites de mémoire.

Les homologues utilisateurs sont des objets gérés personnalisés dérivés des homologues du framework. Les pairs utilisateurs contiennent un état et sont donc maintenus en vie par le framework Xamarin même si votre code n'y fait pas référence - par exemple.

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

Nous pouvons créer un nouveau MyViewController, l'ajouter à l'arborescence des vues, puis convertir un UIViewController en un MyViewController. Il ne peut y avoir aucune référence à ce MyViewController, donc Xamarin doit "rooter" cet objet pour le garder en vie tandis que le UIViewController sous-jacent est vivant, sinon nous perdrons les informations d'état.

Le problème est que si nous avons deux pairs utilisateurs qui se référencent, cela crée un cycle de référence qui ne peut pas être automatiquement interrompu - et cette situation se produit souvent!

Considérez ce cas: -

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

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

Xamarin crée deux homologues utilisateurs qui se référencent mutuellement - un pour MyViewController et un autre pour MyButton (car nous avons un gestionnaire d'événements). Ainsi, cela créera un cycle de référence qui ne sera pas effacé par le garbage collector. Pour que cela soit clarifié, nous devons désinscrire le gestionnaire d'événements, et cela se fait généralement dans le gestionnaire ViewDidDisappear - par exemple.

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

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

Comment diagnostiquer ces fuites de mémoire

Un bon moyen de diagnostiquer ces problèmes de mémoire consiste à ajouter du code de débogage aux finaliseurs des classes dérivées des classes wrapper iOS, telles que UIViewControllers. (Bien que ne le mettiez que dans vos versions de débogage et non dans les versions de version car il est raisonnablement 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 interrompue dans iOS, mais vous devez être conscient de ces "pièges" qui sont spécifiques à l'exécution sur iOS.

Il y a une excellente page de Thomas Bandt appelée Xamarin.iOS Memory Pitfalls qui va plus en détail et fournit également quelques conseils et astuces très utiles.

9
JasonB

J'ai remarqué dans votre méthode DisposeEx que vous supprimez la source de vue de collection et la source de vue de table avant de tuer les cellules visibles de cette collection. J'ai remarqué lors du débogage que la propriété des cellules visibles est définie sur un tableau vide. Par conséquent, lorsque vous commencez à supprimer les cellules visibles, elles "n'existent" plus, ce qui en fait un tableau de zéro éléments.

Une autre chose que j'ai remarquée est que vous rencontrerez des exceptions d'incohérence si vous ne supprimez pas la vue des paramètres de sa super vue, j'ai remarqué en particulier en définissant la disposition de la vue de la collection.

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

5
dervish