Xamarin iOS内存无处不在

我们在过去的8个月里一直在使用Xamarin iOS,并开发了一个具有许多屏幕,function,嵌套控件的非平凡的企业应用程序。 我们已经做了我们自己的MVVM拱门,跨平台BLL&DAL“推荐”。 我们在Android之间共享代码,甚至在我们的networking产品上使用BLL / DAL。

除了现在在项目的发布阶段,我们发现在Xamarin基于iOS的应用程序中无处不在的内存泄漏。 我们已经遵循了所有的“准则”来解决这个问题,但现实情况是,C#GC和Obj-C ARC似乎是不兼容的垃圾收集机制,它们以目前的方式互相叠加在单点触摸平台上。

我们发现的现实是,对于任何非平凡的应用程序,本机对象和被pipe理对象之间的硬周期将会发生,并且频繁出现。 例如,在使用lambdaexpression式或手势识别器的任何地方发生这种情况都非常容易。 加上MVVM的复杂性,这几乎是一个保证。 错过其中的一种情况,整个对象的graphics将永远不会被收集。 这些图表会引诱其他物体并像癌症一样长大,最终导致iOS的迅速和无情的消灭。

Xamarin的回答是对这个问题毫无兴趣的推迟,以及“开发者应该避免这些情况”的不切实际的期望。 仔细考虑这一点,可以看出这是垃圾收集在Xamarin中基本上被破坏了

现在我认识到,在传统的c#.NET意义上,你并不真正在Xamarin iOS中获得“垃圾收集”。 你需要雇用“垃圾维护”模式,实际上是让GC移动并完成工作,即使这样,它也永远不会是完美的 – 不确定性。

我的公司投入了大量资金,试图阻止我们的应用程序崩溃和/或内存不足。 我们基本上必须明确地recursion处理每一个该死的东西,并在应用程序中实施垃圾维护模式,只是为了停止崩溃,并有一个可行的产品,我们可以出售。 我们的客户是支持和宽容的,但我们知道这不能永远持续下去。 我们希望Xamarin有一个专门的团队在这个问题上工作,并一劳永逸。 不幸的是,看起来不像。

问题是,我们的经验是用Xamarin编写的非平凡企业级应用程序的例外规则吗?

UPDATE

请参阅DisposeEx方法和解决scheme的答案。

我已经发布了一个与Xamarin写的非平凡的应用程序。 许多其他人也有。

“垃圾收集”并不神奇。 如果您创build一个附加到对象图的根的引用,并且永远不会分离它,它将不会被收集。 这不仅仅是Xamarin,而是.NET,Java等上的C#。

button.Click += (sender, e) => { ... }是一种反模式,因为您没有对lambda的引用,所以您不能从Click事件中删除事件处理程序。 同样的,当你在托pipe和非托pipe对象之间创build引用时,你必须小心地理解你在做什么。

对于“我们已经完成了自己的MVVM拱门”,有很多高调的MVVM库( MvvmCross , ReactiveUI和MVVM Light Toolkit ),所有这些都非常重视引用/泄漏问题。

我使用下面的扩展方法来解决这些内存泄漏问题。 想想Ender的游戏最后的战斗场景,DisposeEx方法就像那个激光,它将所有的视图和它们的连接对象分开,recursion地处理,并且不应该使你的应用程序崩溃。

当你不再需要视图控制器时,只需在UIViewController的主视图上调用DisposeEx()。 如果一些嵌套的UIView有特殊的东西来处理,或者你不想要它的处置,实现ISpecialDisposable.SpecialDispose被调用,而不是IDisposable.Dispose。

注意 :这里假定没有UIImage实例在您的应用程序中共享。 如果是,请修改DisposeEx以智能处理。

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

不能同意更多OP“垃圾收集在Xamarin基本上被打破”。

这里有一个例子说明了为什么你必须总是按照build议使用DisposeEx()方法。

以下代码泄漏内存:

  1. 创build一个类inheritanceUITableViewController

     public class Test3Controller : UITableViewController { public Test3Controller () : base (UITableViewStyle.Grouped) { } } 
  2. 从某处调用以下代码

     var controller = new Test3Controller (); controller.Dispose (); controller = null; GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced); 
  3. 使用仪器,你会看到有~274个永远不会被收集的永久性对象。

  4. 解决这个问题的唯一方法是将DisposeEx或类似的function添加到Dispose()函数中,并手动调用Dispose来确保处置== true。

简介:创build一个UITableViewController派生类,然后处置/ nulling将始终导致堆增长。

我注意到在DisposeEx方法中,在处理该集合的可见单元格之前,需要处置集合视图源和表视图源。 我注意到,当debugging可见的单元格属性设置为一个空数组,因此,当你开始处理可见单元格,他们不再“存在”,因此它成为一个零元素的数组。

我注意到的另一件事是,如果你不从其超级视图中删除参数视图,你会碰到不一致的exception,我特别注意到设置集合视图的布局。

除此之外,我不得不在我们身上实施类似的东西。

iOS和Xamarin有一个稍微困扰的关系。 iOS使用引用计数来pipe理和处理其内存。 当添加和删除引用时,对象的引用计数会增加或减less。 当引用计数变为0时,对象被删除并释放内存。 Objective C和Swift中的自动引用计数function对此有帮助,但使用本机iOS语言进行开发时,仍然很难获得100%的正确性,悬挂的指针和内存泄漏可能会很痛苦。

在Xamarin for iOS编码时,我们必须记住引用计数,因为我们将使用iOS本机内存对象。 为了与iOS操作系统进行通信,Xamarin创build了所谓的Peers,它们为我们pipe理引用计数。 有两种types的对等 – 框架对等和用户对等。 Framework Peers是众所周知的iOS对象的托pipe包装器。 框架对等是无状态的,因此不会强制引用底层的iOS对象,并且在需要时可以被垃圾收集器清理 – 而且不会造成内存泄漏。

用户对等是从框架对等派生的自定义托pipe对象。 用户对等体包含状态,因此即使您的代码没有对它们的引用,Xamarin框架也会保持活动 – 例如

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

我们可以创build一个新的MyViewController,将其添加到视图树中,然后将一个UIViewController投射到MyViewController。 可能没有对这个MyViewController的引用,所以Xamarin需要“根”这个对象来保持它活着,而底层的UIViewController是活着的,否则我们将失去状态信息。

问题是,如果我们有两个相互引用的用户对等,那么这将创build一个不能自动打破的引用循环 – 而且这种情况经常发生!

考虑这种情况:

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

Xamarin创build了两个相互引用的用户对等 – 一个用于MyViewController,另一个用于MyButton(因为我们有一个事件处理程序)。 所以,这将创build一个不会被垃圾收集器清除的引用循环。 为了解决这个问题,我们必须取消订阅事件处理程序,这通常在ViewDidDisappear处理程序中完成 – 例如

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

始终取消订阅您的iOS事件处理程序。

如何诊断这些内存泄漏

诊断这些内存问题的一个好方法就是在debugging时将一些代码添加到从iOS包装类(例如UIViewControllers派生的类的Finalisers中。 (虽然只是把它放在你的debugging版本中,而不是在发布版本中,因为它相当慢。

 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 } 

所以,Xamarin的内存pipe理在iOS中并没有被打破,但是你必须意识到这些在iOS上运行的特定问题。

托马斯·class特(Thomas Bandt)有一个非常棒的页面,叫做Xamarin.iOS记忆陷阱 ,更详细地介绍了这个陷阱 ,并提供了一些非常有用的提示和技巧。