Xamarin iOS内存随处可见

Her*_*eld 49 c# garbage-collection xamarin.ios ios xamarin

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

一切都很好,除了现在在项目的发布阶段,我们在基于Xamarin iOS的应用程序中发现无法修复的内存泄漏.我们已经遵循了所有"指南"来解决这个问题,但实际情况是C#GC和Obj-C ARC似乎是不兼容的垃圾收集机制,它们以当前的方式在monotouch平台中相互叠加.

我们发现现实情况是,原生对象和管理对象之间的硬周期WILL发生,FREQUENTLY对于任何不平凡的应用程序.在您使用lambdas或手势识别器的任何地方都可以轻松实现这一点.加上MVVM的复杂性,这几乎是一种保证.只想念其中一种情况,并且永远不会收集整个对象图.这些图表将引诱其他物体进入并像癌症一样成长,最终导致iOS的迅速和无情的消灭.

Xamarin的答案是对这个问题的不感兴趣的推迟以及"开发者应该避免这些情况"的不切实际的期望.仔细考虑这一点就可以看出这是对垃圾收集在Xamarin中基本被打破的承认.

现在对我的认识是,在传统的c#.NET意义上,你并没有真正在Xamarin iOS中获得"垃圾收集".您需要使用"垃圾维护"模式实际上让GC移动并完成其工作,即使这样,它也永远不会是完美的 - 非决定性的.

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

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

UPDATE

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

ant*_*ony 25

我发布了一个用Xamarin编写的非平凡应用程序.其他许多人也有.

"垃圾收集"并不神奇.如果创建附加到对象图的根的引用并且从不分离它,则不会收集它.这不仅适用于Xamarin,而且适用于.NET,Java等的C#.

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

至于"我们已经完成了我们自己的MVVM拱门",有高调的MVVM库(MvvmCross,ReactiveUIMVVM Light Toolkit),所有这些都非常重视参考/泄漏问题.

  • @anthony:在xamarin中永远不会收集本机和托管对象之间的独立引用循环.这基本上就是问题所在.如果Xamarin应该像C#.NET一样工作,它应该收集这些周期.将责任放在开发人员身上以明确地打破这些周期不仅是不合理的,而是承认GC在Xamarin中不起作用,因为它应该适用于任何合理的托管环境,并将其声称为功能. (9认同)
  • 基本上,每当处理比玩具应用程序更严重的事情时,您必须研究如何保持对对象的引用.特别是`Bitmap'是内存生猪,需要小心处理.我建议大家查看ReactiveUI的内容.如果你不参与其中,你将**必须研究弱参考,即使在`EventHandler'上,它们也会让生活变得更加容易.弱引用组成了很多MvvmCross,甚至ReactiveUI都依赖它.要么这样做,要么确保在生命中的某个时刻释放您创建的每个对象. (2认同)
  • @anthony如果发布者(Button)超过订阅者,那么你的`button.Click`示例只是一种反模式.但我同意,很容易得到循环引用,这很难打破. (2认同)
  • 正如OP所说,我们也遇到了内存问题,除了我们没有重新实现MVVM框架之外,我们也是如此.在做了很多关于Xamarin如何与Native代码交互的阅读之后,看起来简单的.NET模式不适用于iOS - 从iOS 3开始我一直是iOS开发者.首先,没有GC!我强烈建议任何有记忆问题的人阅读这篇文章http://krumelur.me/2015/04/27/xamarin-ios-the-garbage-collector-and-me/不要判断错误在哪里,希望帖子/博客就像前一个帮助一样. (2认同)

Her*_*eld 21

我使用下面的扩展方法来解决这些内存泄漏问题.想想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;
                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();
    }
Run Code Online (Sandbox Code Playgroud)

  • `view.Subviews.Update(RemoveFromSuperviewAndDispose);`更新不存在. (2认同)

小智 13

不能同意OP"垃圾收集在Xamarin中基本上被打破".

这是一个示例,说明为什么必须始终按照建议使用DisposeEx()方法.

以下代码泄漏内存:

  1. 创建一个继承UITableViewController的类

    public class Test3Controller : UITableViewController
    {
        public Test3Controller () : base (UITableViewStyle.Grouped)
        {
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 从某个地方调用以下代码

    var controller = new Test3Controller ();
    
    controller.Dispose ();
    
    controller = null;
    
    GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
    
    Run Code Online (Sandbox Code Playgroud)
  3. 使用Instruments,您将看到有大约274个持久对象,从未收集过252 KB.

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

简介:创建UITableViewController派生类然后处理/ nulling将始终导致堆增长.


Jas*_*onB 9

iOS和Xamarin的关系略显不稳定.iOS使用引用计数来管理和处理其内存.添加和删​​除引用时,对象的引用计数会递增和递减.当引用计数变为0时,将删除该对象并释放内存.Objective C和Swift中的自动引用计数对此有所帮助,但在使用本机iOS语言进行开发时,仍然很难获得100%正确并且悬空指针和内存泄漏可能会很痛苦.

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

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

public class MyViewController : UIViewController
{
    public string Id { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

问题是,如果我们有两个相互引用的用户对等,那么这会创建一个无法自动破坏的参考周期 - 这种情况经常发生!

考虑这种情况: -

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

    void DoSomething (object sender, EventArgs e) { ... }
}
Run Code Online (Sandbox Code Playgroud)

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

public override void ViewDidDisappear(bool animated)
{
    ProcessButton.TouchUpInside -= DoSomething;
    base.ViewDidDisappear (animated);
}
Run Code Online (Sandbox Code Playgroud)

始终取消订阅您的iOS活动处理程序.

如何诊断这些内存泄漏

诊断这些内存问题的一个好方法是将调试中的一些代码添加到从iOS包装类派生的类的Finaliser中 - 例如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
}
Run Code Online (Sandbox Code Playgroud)

因此,Xamarin的内存管理在iOS中没有被破坏,但你必须要注意这些特定于在iOS上运行的"陷阱".

Thomas Bandt有一个很好的页面,名为Xamarin.iOS Memory Trafalls,更详细地介绍了这一点,并提供了一些非常有用的提示和技巧.

  • 我在使用 sprite 工具包的 iOS 应用程序中出现了严重的内存泄漏,要修复泄漏是取消订阅 ViewDidDisappear 中的事件处理程序。实现需要很长时间,因为 TouchUpInside 事件等有很多 lambda,但是当我完成时我没有泄漏 (2认同)

小智 5

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

我注意到的另一件事是,如果你没有从超级视图中删除参数视图,你会遇到不一致的异常,我特别注意到设置了集合视图的布局.

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

  • 那段代码很旧.我发布了应该解决你提出的大部分问题的答案. (2认同)