Xamarin iOS内存到处泄漏

本文关键字:泄漏 内存 iOS Xamarin | 更新日期: 2023-09-27 18:19:34

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

一切都很好,除了现在在项目的发布阶段,我们发现基于Xamarin iOS的应用程序中到处都是无法修复的内存泄漏。我们已经遵循了所有的"指导方针"来解决这个问题,但事实是,C#GC和Obj-C ARC似乎是不兼容的垃圾收集机制,因为它们在单点触控平台中以目前的方式相互覆盖。

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

Xamarin的答案是对这个问题的不感兴趣的延期,以及"开发者应该避免这些情况"的不切实际的期望。仔细考虑这一点,就会发现这是一种承认垃圾收集在Xamarin中基本上被破坏了。

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

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

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

更新

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

Xamarin iOS内存到处泄漏

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

"垃圾回收"不是魔法。如果创建了一个附加到对象图根的引用,并且从未将其分离,则不会收集该引用。这不仅适用于Xamarin,也适用于.NET、Java等平台上的C#。

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

至于"我们已经完成了我们自己的MVVM arch",有一些高调的MVVM库(MvvmCross、ReactiveUI和MVVM Light Toolkit),所有这些库都非常重视引用/泄漏问题。

我使用了以下扩展方法来解决这些内存泄漏问题。想想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();
    }

非常同意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以确保Dispose==true。

摘要:创建一个UITableViewController派生类,然后处理/置零总是会导致堆增长。

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

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

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

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

我们可以创建一个新的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) { ... }
}

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

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

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

如何诊断这些内存泄漏

诊断这些内存问题的一个好方法是在调试中向从iOS包装类派生的类(如UIViewControllers)的Finaliser添加一些代码。(尽管只把它放在调试构建中,而不是发布构建中,因为它相当慢

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上运行的"gotchas"。

Thomas Bandt有一个名为Xamarin.iOS Memory Pitfalls的优秀页面,它详细介绍了这一点,并提供了一些非常有用的提示和技巧。

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

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

除此之外,我不得不在我们这边实施类似的措施。