【问题标题】:Xamarin iOS memory leaks everywhereXamarin iOS 内存泄漏无处不在
【发布时间】:2014-10-21 09:00:45
【问题描述】:

过去 8 个月来,我们一直在使用 Xamarin iOS,并开发了一个具有许多屏幕、功能和嵌套控件的非平凡企业应用程序。我们已经完成了我们自己的 MVVM 架构、跨平台 BLL 和 DAL 作为“推荐”。我们在 Android 之间共享代码,甚至我们的 BLL/DAL 也用于我们的网络产品。

一切都很好,除了现在在项目的发布阶段,我们发现在基于 Xamarin iOS 的应用程序中到处都是不可修复的内存泄漏。我们已遵循所有“指南”来解决此问题,但现实情况是 C# GC 和 Obj-C ARC 似乎是不兼容的垃圾收集机制,目前它们在单点触控平台中相互叠加。

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

Xamarin 的回答是对此问题毫无兴趣的推迟,并且不切实际地期望“开发人员应该避免这些情况”。仔细考虑这一点表明这是承认 垃圾收集在 Xamarin 中基本上被破坏了

我现在意识到,在传统的 c# .NET 意义上,您并没有真正在 Xamarin iOS 中获得“垃圾收集”。您需要采用“垃圾维护”模式才能真正让 GC 移动并完成其工作,即使那样它也永远不会完美 - 非确定性。

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

问题是,我们的经验是用 Xamarin 编写的重要企业级应用程序的例外还是规则?

更新

查看 DisposeEx 方法和解决方案的答案。

【问题讨论】:

  • 您对“垃圾收集基本上在 Xamarin 中被破坏”的参考在哪里?就个人而言,我认为你是例外而不是规则。我没有像你提到的任何记忆问题。我遇到的唯一与生产相关的问题是链接 SDK。
  • 如果它是一个游戏应用程序,我不会这么惊讶,但对于“企业”应用程序,我很怀疑。让我们看看您的问题和解决方法的一些示例。
  • Herman,我完全同意你的看法,垃圾收集 IS 在 Xamarin.iOS/MonoTouch 上损坏。一年前我也有同样的经历,甚至建立了一个像你在这里发布的那样的“层次结构清理器”。我的应用程序是一个消费者应用程序,几乎有 ± 35 个控制器,我花了 3 个多月的时间才把内存弄出来。 Xamarin 工作人员中的某个人告诉我他们正在研究它,但又过了一年,我看不到任何进展,甚至样本都充满了这些反模式 lamdas。

标签: c# ios xamarin.ios garbage-collection xamarin


【解决方案1】:

我已经发布了一个用 Xamarin 编写的重要应用程序。许多其他人也有。

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

button.Click += (sender, e) => { ... } 是一种反模式,因为您没有对 lambda 的引用,并且您永远无法从 Click 事件中删除事件处理程序。同样,在托管和非托管对象之间创建引用时,您必须小心了解自己在做什么。

至于“我们已经完成了自己的 MVVM 架构”,有一些备受瞩目的 MVVM 库(MvvmCrossReactiveUIMVVM Light Toolkit),所有这些库都非常重视引用/泄漏问题。

【讨论】:

  • 还有 MvvmLight 现在支持 Xamarin。所以实际上是 3!
  • 基本上,每当在做一些比玩具应用程序更严肃的事情时,你都必须研究如何保持对对象的引用。特别是Bitmaps 是内存猪,需要小心处理。我建议每个人都研究一下 ReactiveUI 的东西。如果您对此不感兴趣,您必须查看弱引用,即使在 EventHandlers 上,它们也会让生活变得如此轻松。弱引用构成了很多 MvvmCross,甚至 ReactiveUI 也依赖它。要么这样做,要么确保释放你在生命中某个时刻创建的每个对象。
  • @anthony:本机和托管对象之间的独立引用循环永远不会在 xamarin 中收集。这本质上是问题所在。如果 Xamarin 应该像 C# .NET 一样工作,它应该收集这些周期。让开发人员有责任明确打破这些循环不仅是不合理的,而且承认 GC 在 Xamarin 中不能像它一样在任何声称它是一项功能的合理托管环境中都可以工作。
  • @anthony 您的button.Click 示例仅在发布者(按钮)比订阅者寿命长的情况下才是反模式。但我同意,很容易获得难以破解的循环引用。
  • 我们也遇到了 OP 所说的内存问题,除了我们没有重新实现 MVVM 框架外,情况大致相同。在阅读了大量有关 Xamarin 如何与本机代码交互的内容之后,似乎简单的 .NET 模式不适用于 iOS - 自 iOS 3 以来我一直是一名 iOS 开发人员。首先,没有 GC!我强烈建议任何有记忆问题的人阅读这篇文章krumelur.me/2015/04/27/xamarin-ios-the-garbage-collector-and-me 不判断错误所在,希望像上一篇一样的帖子/博客有所帮助。
【解决方案2】:

我使用以下扩展方法来解决这些内存泄漏问题。想想 Ender 的游戏最终战斗场景,DisposeEx 方法就像激光一样,它将所有视图及其连接的对象分离,并以不应该使您的应用崩溃的方式递归地处理它们。

当您不再需要该视图控制器时,只需在 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;
                // 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();
    }

【讨论】:

  • @ThomasdeRoo:删除它(我自己的自定义扩展方法)
  • view.Subviews.Update(RemoveFromSuperviewAndDispose); 更新不存在。
  • 能否提供安卓版本?
  • 我现在得看安德的游戏了。
  • @CaseyHancock:更改为 ForEach(我有名为 Update 的 ext 方法)。同样的事情。
【解决方案3】:

完全同意 OP 的观点,即“垃圾收集在 Xamarin 中基本上被破坏了”。

这里有一个示例说明为什么您必须始终按照建议使用 DisposeEx() 方法。

以下代码泄漏内存:

  1. 创建一个继承 UITableViewController 的类

    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. 使用 Instruments,您将看到大约 274 个永久对象,其中 252 KB 从未被收集。

  4. 解决此问题的唯一方法是将 DisposeEx 或类似功能添加到 Dispose() 函数并手动调用 Dispose 以确保 disposing == true。

总结:创建一个 UITableViewController 派生类然后释放/清空总是会导致堆增长。

【讨论】:

  • 你是怎么发现剩下的 252kb 没有被处理掉的?
【解决方案4】:

iOS 和 Xamarin 的关系有点麻烦。 iOS 使用引用计数来管理和处置其内存。当添加和删除引用时,对象的引用计数会增加和减少。当引用计数变为 0 时,对象被删除并释放内存。 Objective C 和 Swift 中的自动引用计数对此有所帮助,但仍然很难做到 100% 正确,并且在使用原生 iOS 语言进行开发时,悬空指针和内存泄漏可能会很痛苦。

在 Xamarin for iOS 中编码时,我们必须牢记引用计数,因为我们将使用 iOS 本机内存对象。为了与 iOS 操作系统进行通信,Xamarin 创建了所谓的 Peers,它为我们管理引用计数。有两种类型的对等体——框架对等体和用户对等体。框架对等体是围绕众所周知的 iOS 对象的托管包装器。 Framework Peers 是无状态的,因此不持有对底层 iOS 对象的强引用,并且可以在需要时由垃圾收集器清理——并且不会导致内存泄漏。

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

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

我们可以创建一个新的 MyViewController,将它添加到视图树中,然后将 UIViewController 转换为 MyViewController。可能没有对此 MyViewController 的引用,因此 Xamarin 需要“根”此对象以使其在底层 UIViewController 处于活动状态时保持活动状态,否则我们将丢失状态信息。

问题在于,如果我们有两个相互引用的用户对等体,那么这会创建一个无法自动中断的引用循环——这种情况经常发生!

考虑这种情况:-

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

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

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

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

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

如何诊断这些内存泄漏

诊断这些内存问题的一个好方法是在从 iOS 包装类派生的类的终结器中添加一些调试代码,例如 UIViewControllers。 (尽管只将它放在您的调试版本中,而不是在发布版本中,因为它相当慢。

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 的内存管理在 iOS 中并未中断,但您必须注意这些特定于在 iOS 上运行的“陷阱”。

Thomas Bandt 有一个很棒的页面 Xamarin.iOS Memory Pitfalls 对此进行了更详细的介绍,并提供了一些非常有用的提示和技巧。

【讨论】:

  • 我在一个使用 sprite 工具包的 iOS 应用程序中出现严重的内存泄漏,修复泄漏的方法是取消订阅 ViewDidDisappear 中的事件处理程序。实现需要很长时间,因为 TouchUpInside 事件等有很多 lambda,但是当我完成时,我没有泄漏
【解决方案5】:

我注意到在您的 DisposeEx 方法中,您在杀死该集合的可见单元格之前处置了集合视图源和表视图源。我注意到在调试时可见单元格属性被设置为一个空数组,因此,当您开始处理可见单元格时,它们不再“存在”,因此它变成了一个零元素数组。

我注意到的另一件事是,如果您不从其超级视图中删除参数视图,您将遇到不一致的异常,我特别注意到设置集合视图的布局。

除此之外,我必须在我们这边实现类似的东西。

【讨论】:

  • 那个代码是旧的。我已经发布了应该解决您提出的大部分问题的答案。
猜你喜欢
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-09
  • 2016-10-16
  • 2014-06-06
  • 2011-04-29
相关资源
最近更新 更多