web-dev-qa-db-de.com

Xamarin iOS-Speicher leckt überall

Wir verwenden Xamarin iOS seit 8 Monaten und haben eine nicht triviale Unternehmens-App mit vielen Bildschirmen, Funktionen und verschachtelten Steuerelementen entwickelt. Wir haben unseren eigenen MVVM Arch erstellt, plattformübergreifend BLL & DAL als "empfohlen". Wir teilen Code zwischen Android und sogar unser BLL/DAL wird in unserem Webprodukt verwendet.

Alles ist gut, außer jetzt, in der Release-Phase des Projekts, entdecken wir überall in der Xamarin iOS-basierten App irreparable Speicherlecks. Wir haben alle "Richtlinien" befolgt, um dies zu beheben, aber die Realität ist, dass C # GC und Obj-C ARC scheinbar inkompatible Speicherbereinigungsmechanismen sind, so wie sie sich derzeit in Monotouch-Plattformen überlagern.

Die Realität, die wir gefunden haben, ist, dass es zwischen nativen und verwalteten Objekten harte Zyklen gibt WERDEN auftreten und HÄUFIG für jede nicht-triviale App. Dies ist extrem einfach, wenn Sie beispielsweise Lambdas oder Gestenerkenner verwenden. Wenn Sie die Komplexität von MVVM erhöhen, ist dies fast eine Garantie. Wenn Sie nur eine dieser Situationen verpassen, werden niemals ganze Diagramme von Objekten gesammelt. Diese Grafiken locken andere Objekte an und wachsen wie Krebs, was schließlich zu einer raschen und gnadenlosen Ausrottung durch iOS führt.

Xamarins Antwort ist eine uninteressierte Zurückstellung des Problems und eine unrealistische Erwartung, dass "Entwickler diese Situationen vermeiden sollten". Eine sorgfältige Betrachtung zeigt dies als ein Eingeständnis, dass Die Garbage Collection ist in Xamarin im Wesentlichen defekt.

Die Erkenntnis für mich ist jetzt, dass Sie in Xamarin iOS nicht wirklich "Garbage Collection" im traditionellen c # .NET-Sinne erhalten. Sie müssen "Garbage Maintanence" -Muster anwenden, um den GC in Bewegung zu setzen und seine Arbeit zu erledigen, und selbst dann wird er niemals perfekt sein - NICHT DETERMINISTISCH.

Mein Unternehmen hat ein Vermögen investiert, um zu verhindern, dass unsere App abstürzt und/oder der Speicher knapp wird. Grundsätzlich mussten wir jedes verdammte Ding explizit und rekursiv in Sicht bringen und Garbage-Maintanence-Muster in die App implementieren, um die Abstürze zu stoppen und ein brauchbares Produkt zu haben, das wir verkaufen können. Unsere Kunden sind unterstützend und tolerant, aber wir wissen, dass dies nicht ewig so bleiben kann. Wir hoffen, dass Xamarin ein engagiertes Team hat, das an diesem Problem arbeitet und es ein für alle Mal festnagelt. Sieht leider nicht so aus.

Die Frage ist, ist unsere Erfahrung die Ausnahme oder die Regel für nicht-triviale Unternehmens-Apps, die in Xamarin geschrieben wurden?

AKTUALISIEREN

Siehe Antwort für DisposeEx-Methode und -Lösung.

49

Ich habe die folgenden Erweiterungsmethoden verwendet, um diese Speicherleckprobleme zu lösen. Denken Sie an die letzte Kampfszene von Ender, die DisposeEx-Methode ist wie dieser Laser. Sie trennt alle Ansichten und ihre verbundenen Objekte und ordnet sie rekursiv und so an, dass Ihre App nicht abstürzt.

Rufen Sie einfach DisposeEx () in der Hauptansicht von UIViewController auf, wenn Sie diesen View Controller nicht mehr benötigen. Implementieren Sie ISpecialDisposable.SpecialDispose, das anstelle von IDisposable.Dispose aufgerufen wird, wenn einige verschachtelte UIView-Objekte spezielle Dinge zu entsorgen haben oder Sie nicht möchten, dass sie entsorgt werden.

NOTE : Dies setzt voraus, dass keine UIImage-Instanzen in Ihrer App geteilt werden. Wenn dies der Fall ist, ändern Sie DisposeEx, um es intelligent zu entsorgen.

    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

Ich habe eine nicht triviale App geliefert, die mit Xamarin geschrieben wurde. Viele andere auch.

"Garbage Collection" ist keine Zauberei. Wenn Sie einen Verweis erstellen, der an den Stamm Ihres Objektdiagramms angehängt ist, und diesen niemals trennen, wird er nicht erfasst. Dies gilt nicht nur für Xamarin, sondern auch für C # unter .NET, Java usw.

button.Click += (sender, e) => { ... } ist ein Anti-Pattern, da Sie keinen Bezug zum Lambda haben und den Event-Handler niemals aus dem Event Click entfernen können. Ebenso müssen Sie darauf achten, dass Sie verstehen, was Sie tun, wenn Sie Verweise zwischen verwalteten und nicht verwalteten Objekten erstellen.

Für "Wir haben unseren eigenen MVVM-Arch erstellt" gibt es hochkarätige MVVM-Bibliotheken ( MvvmCross , ReactiveUI und MVVM Light Toolkit ). , die alle Referenz-/Leckprobleme sehr ernst nehmen.

25
anthony

Konnte nicht mehr mit dem OP übereinstimmen, dass "Garbage Collection in Xamarin im Wesentlichen defekt ist".

Das folgende Beispiel zeigt, warum Sie immer die vorgeschlagene DisposeEx () -Methode verwenden müssen.

Der folgende Code verliert Speicher:

  1. Erstellen Sie eine Klasse, die UITableViewController erbt

    public class Test3Controller : UITableViewController
    {
        public Test3Controller () : base (UITableViewStyle.Grouped)
        {
        }
    }
    
  2. Rufen Sie den folgenden Code von irgendwoher auf

    var controller = new Test3Controller ();
    
    controller.Dispose ();
    
    controller = null;
    
    GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
    
  3. Wenn Sie Instrumente verwenden, werden Sie feststellen, dass es ~ 274 beständige Objekte gibt, von denen 252 KB niemals gesammelt wurden.

  4. Die einzige Möglichkeit, dies zu beheben, besteht darin, der Funktion Dispose () DisposeEx oder eine ähnliche Funktion hinzuzufügen und Dispose manuell aufzurufen, um sicherzustellen, dass disposing == true ist.

Zusammenfassung: Wenn Sie eine von UITableViewController abgeleitete Klasse erstellen und anschließend entsorgen/nullen, wird der Heap immer größer.

13
Derek Massey

iOS und Xamarin haben eine etwas schwierige Beziehung. iOS verwendet Referenzzählungen, um seinen Speicher zu verwalten und zu entsorgen. Der Referenzzähler eines Objekts wird erhöht und verringert, wenn Referenzen hinzugefügt und entfernt werden. Wenn der Referenzzähler auf 0 geht, wird das Objekt gelöscht und der Speicher freigegeben. Das automatische Zählen von Referenzen in Objective C und Swift= hilft dabei, aber es ist immer noch schwierig, 100% richtig zu machen, und baumelnde Zeiger und Speicherlecks können bei der Entwicklung mit nativen iOS-Sprachen zu Problemen führen.

Bei der Codierung in Xamarin für iOS müssen wir die Anzahl der Verweise berücksichtigen, da wir mit nativen iOS-Speicherobjekten arbeiten werden. Um mit dem iOS-Betriebssystem zu kommunizieren, erstellt Xamarin sogenannte Peers, die die Referenzzahlen für uns verwalten. Es gibt zwei Arten von Peers: Framework-Peers und Benutzer-Peers. Framework-Peers sind verwaltete Wrapper für bekannte iOS-Objekte. Framework-Peers sind statusfrei und enthalten daher keine starken Verweise auf die zugrunde liegenden iOS-Objekte. Sie können bei Bedarf von den Garbage Collectors bereinigt werden - und verursachen keine Speicherverluste.

Benutzer-Peers sind benutzerdefinierte verwaltete Objekte, die von Framework-Peers abgeleitet sind. Benutzer-Peers enthalten state und werden daher vom Xamarin-Framework selbst dann am Leben erhalten, wenn Ihr Code keine Verweise darauf enthält - z.

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

Wir können einen neuen MyViewController erstellen, ihn zum Ansichtsbaum hinzufügen und dann einen UIViewController in einen MyViewController umwandeln. Möglicherweise gibt es keine Verweise auf diesen MyViewController. Daher muss Xamarin dieses Objekt "root", um es am Leben zu erhalten, während der zugrunde liegende UIViewController am Leben ist. Andernfalls verlieren wir die Statusinformationen.

Das Problem ist, dass, wenn wir zwei Benutzer-Peers haben, die sich gegenseitig referenzieren, dies einen Referenzzyklus erzeugt, der nicht automatisch unterbrochen werden kann - und diese Situation kommt häufig vor!

Betrachten Sie diesen Fall: -

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

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

Xamarin erstellt zwei Benutzer-Peers, die sich gegenseitig referenzieren - einen für MyViewController und einen für MyButton (da wir einen Ereignishandler haben). Auf diese Weise wird ein Referenzzyklus erstellt, der vom Garbage Collector nicht gelöscht wird. Damit dies geklärt wird, müssen wir den Ereignishandler abbestellen . Dies geschieht normalerweise im ViewDidDisappear-Handler, z.

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

Melden Sie sich immer von Ihren iOS-Ereignishandlern ab.

So diagnostizieren Sie diese Speicherlecks

Eine gute Möglichkeit, diese Speicherprobleme zu diagnostizieren, besteht darin, den Finalisern der Klassen, die von iOS-Wrapperklassen abgeleitet sind, Code beim Debuggen hinzuzufügen, z. B. UIViewControllers. (Obwohl dies nur in Debug-Builds und nicht in Release-Builds enthalten ist, weil es relativ langsam ist.

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
}

Daher ist die Speicherverwaltung von Xamarin in iOS nicht fehlerhaft, aber Sie müssen sich dieser Fallstricke bewusst sein, die speziell für iOS gelten.

Es gibt eine ausgezeichnete Seite von Thomas Bandt mit dem Namen Xamarin.iOS Memory Pitfalls , die detaillierter darauf eingeht und auch einige sehr nützliche Hinweise und Tipps bietet.

9
JasonB

Ich habe festgestellt, dass Sie in Ihrer DisposeEx-Methode die Quelle für die Sammlungsansicht und die Tabellenansicht löschen, bevor Sie die sichtbaren Zellen dieser Sammlung entfernen. Beim Debuggen ist mir aufgefallen, dass die Eigenschaft visible cells auf ein leeres Array gesetzt wird. Wenn Sie also damit beginnen, sichtbare Zellen zu entsorgen, sind sie nicht mehr "vorhanden", daher wird sie zu einem Array von Null-Elementen.

Eine andere Sache, die mir aufgefallen ist, ist, dass es zu Inkonsistenzausnahmen kommt, wenn Sie die Parameteransicht nicht aus der Superansicht entfernen. Dies ist mir besonders beim Festlegen des Layouts der Sammlungsansicht aufgefallen.

Ansonsten musste ich auf unserer Seite etwas Ähnliches umsetzen.

5
dervish