Xamarin iOS memory leaks everywhere Xamarin iOS memory leaks everywhere ios ios

Xamarin iOS memory leaks everywhere


I have shipped a non-trivial app written with Xamarin. Many others have as well.

"Garbage collection" isn't magic. If you create a reference that is attached to the root of your object graph and never detach it, it will not be collected. That's not only true of Xamarin, but of C# on .NET, Java, etc.

button.Click += (sender, e) => { ... } is an anti-pattern, because you don't have a reference to the lambda and you can never remove the event handler from the Click event. Similarly, you have to be careful that you understand what you're doing when you create references between managed and unmanaged objects.

As for "We've done our own MVVM arch", there are high profile MVVM libraries (MvvmCross, ReactiveUI, and MVVM Light Toolkit), all of which take reference/leak issues very seriously.


I used the below extension methods to solve these memory leak issues. Think of Ender's Game final battle scene, the DisposeEx method is like that laser and it disassociates all views and their connected objects and disposes them recursively and in a way that shouldn't crash your app.

Just call DisposeEx() on UIViewController's main view when you no longer need that view controller. If some nested UIView has special things to dispose, or you dont want it disposed, implement ISpecialDisposable.SpecialDispose which is called in place of IDisposable.Dispose.

NOTE: this assumes no UIImage instances are shared in your app. If they are, modify DisposeEx to intelligently dispose.

    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;                // Comment out extension method                //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();    }


Couldn't be agree more with the OP that "Garbage Collection is essentially broken in Xamarin".

Here's an example shows why you have to always use a DisposeEx() method as suggested.

The following code leaks memory:

  1. Create a class the inherits UITableViewController

    public class Test3Controller : UITableViewController{    public Test3Controller () : base (UITableViewStyle.Grouped)    {    }}
  2. Call the following code from somewhere

    var controller = new Test3Controller ();controller.Dispose ();controller = null;GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
  3. Using Instruments you will see that there are ~ 274 persistent objects with 252 KB never collected.

  4. Only way to fix this is add DisposeEx or similar functionality to the Dispose() function and call Dispose manually to ensure disposing == true.

Summary: Creating a UITableViewController derived class and then disposing/nulling will always cause the heap to grow.