Подтвердить что ты не робот

Утечка памяти Xamarin iOS повсюду

Мы использовали Xamarin iOS в течение последних 8 месяцев и разработали нетривиальное корпоративное приложение со многими экранами, функциями, вложенными элементами управления. Мы сделали свою собственную арку MVVM, кросс-платформу BLL и DAL как "рекомендуется". Мы используем код между Android и даже наш BLL/DAL используется в нашем веб-продукте.

Все хорошо, только сейчас в стадии релиза проекта мы обнаруживаем непоправимые утечки памяти повсюду в приложении Xamarin iOS. Мы выполнили все "рекомендации", чтобы решить эту проблему, но реальность такова, что С# GC и Obj-C ARC кажутся несовместимыми механизмами сбора мусора в том виде, как они накладываются друг на друга в платформе monotouch.

Реальность, которую мы обнаружили, заключается в том, что возникают жесткие циклы между нативными объектами и управляемыми объектами WILL и ЧАСТО) для любого нетривиального приложения. Чрезвычайно просто, чтобы это произошло везде, где вы используете, например, lambdas или распознаватели жестов. Добавьте сложность MVVM и это почти гарантия. Мисс только одна из этих ситуаций и целые графики объектов никогда не будут собраны. Эти графики будут замаскировать другие объекты и расти как рак, что в конечном итоге приведет к быстрому и беспощадному истреблению со стороны iOS.

Ответ на Xamarin - это незаинтересованная отсрочка проблемы и нереалистичное ожидание того, что "разработчики должны избегать этих ситуаций". Тщательное рассмотрение этого показывает это как признание того, что Сбор мусора по существу разрушен в Xamarin.

Реализация для меня теперь заключается в том, что вы действительно не получаете "сбор мусора" в Xamarin iOS в традиционном смысле С#.NET. Вам нужно использовать шаблоны "утилизацию мусора", чтобы заставить GC двигаться и выполнять свою работу, и даже тогда это никогда не будет идеальным - НЕ ДЕТЕРМИНИСТИЧЕСКИМ.

Моя компания инвестировала удачу, пытаясь остановить наше приложение от сбоев и/или нехватки памяти. Мы в основном должны были явно и рекурсивно распоряжаться каждой чертовой штукой в ​​поле зрения и внедрять утилизацию мусора в приложение, просто чтобы остановить аварии и иметь жизнеспособный продукт, который мы можем продать. Наши клиенты поддерживают и терпимы, но мы знаем, что это не может быть вечно. Мы надеемся, что у Xamarin есть специальная команда, работающая над этой проблемой и забившая ее раз и навсегда. Не похоже, к сожалению.

Вопрос в том, является ли наш опыт исключением или правилом для нетривиальных приложений корпоративного класса, написанных на Xamarin?

ОБНОВЛЕНИЕ

См. ответ для метода DisposeEx и решения.

4b9b3361

Ответ 1

Я использовал приведенные ниже методы расширения для решения этих проблем с утечкой памяти. Подумайте об окончательной битве в Ender Game, метод DisposeEx похож на этот лазер, и он разобьет все взгляды и связанные объекты и решает их рекурсивно и таким образом, чтобы не разбивать ваше приложение.

Просто вызовите DisposeEx() в главном представлении UIViewController, когда вам больше не нужен этот контроллер представления. Если какой-либо вложенный 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();
    }

Ответ 2

Я отправил нетривиальное приложение, написанное с помощью Xamarin. Многие другие также имеют.

"Сбор мусора" - это не волшебство. Если вы создадите ссылку, прикрепленную к корню вашего графа объектов, и никогда не отсоединяете ее, она не будет собрана. Это относится не только к Xamarin, но и к С# на .NET, Java и т.д.

button.Click += (sender, e) => { ... } является анти-шаблоном, потому что у вас нет ссылки на лямбда, и вы никогда не сможете удалить обработчик событий из события Click. Точно так же вы должны быть осторожны, чтобы понять, что делаете, создавая ссылки между управляемыми и неуправляемыми объектами.

Что касается "Мы сделали свою собственную MVVM-арку", существуют высокопрофильные библиотеки MVVM (MvvmCross, ReactiveUI и MVVM Light Toolkit), все из которых очень серьезно относятся к проблеме ссылок/утечек.

Ответ 3

Невозможно согласиться с OP, что "сбор мусора по существу разрушен в Xamarin".

Здесь пример показывает, почему вы всегда должны использовать метод DisposeEx(), как было предложено.

Следующий код утечки памяти:

  • Создайте класс, наследующий UITableViewController

    public class Test3Controller : UITableViewController
    {
        public Test3Controller () : base (UITableViewStyle.Grouped)
        {
        }
    }
    
  • Вызвать следующий код откуда-нибудь

    var controller = new Test3Controller ();
    
    controller.Dispose ();
    
    controller = null;
    
    GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
    
  • Используя Инструменты, вы увидите, что существует ~ 274 постоянных объектов с 252 КБ, которые никогда не собирались.

  • Единственный способ исправить это - добавить DisposeEx или аналогичную функциональность функции Dispose() и вызвать Dispose вручную, чтобы обеспечить удаление == true.

Сводка: создание производного класса UITableViewController, а затем удаление/обнуление всегда будет приводить к росту кучи.

Ответ 4

Я заметил, что в вашем методе DisposeEx вы выбрали источник представления коллекции и источник представления таблицы, прежде чем убивать видимые ячейки этой коллекции. Я заметил, что при отладке свойство видимых ячеек устанавливается на пустой массив, поэтому, когда вы начинаете использовать видимые ячейки, они больше не "существуют", поэтому он становится массивом нулевых элементов.

Еще одна вещь, которую я заметил, это то, что вы столкнетесь с исключениями несогласованности, если вы не удалите представление параметра из своего супер-представления, я заметил, особенно с настройкой макета коллекции.

Кроме того, мне пришлось реализовать что-то подобное на нашей стороне.

Ответ 5

iOS и Xamarin имеют слегка обеспокоенные отношения. iOS использует подсчет ссылок для управления и распоряжения своей памятью. Счетчик ссылок объекта увеличивается и уменьшается, когда ссылки добавляются и удаляются. Когда счетчик ссылок переходит в 0, объект удаляется и память освобождается. Автоматический подсчет ссылок в Objective C и Swift помогает с этим, но его все еще сложно получить 100% прав и оборванных указателей и утечек памяти, может быть болью при разработке с использованием родных языков iOS.

При кодировании в Xamarin для iOS мы должны учитывать количество ссылок, поскольку мы будем работать с объектами памяти iOS. Чтобы общаться с операционной системой iOS, Xamarin создает так называемые Peers, которые управляют подсчетами ссылок для нас. Существует два типа Peers - Framework Peers и User Peers. Framework Peers - это управляемые обертки вокруг известных объектов iOS. Framework Peers являются апатридами и поэтому не содержат сильных ссылок на основные объекты iOS и могут быть очищены сборщиками мусора, когда это необходимо, и не вызывают утечки памяти.

Пользовательские сверстники являются настраиваемыми управляемыми объектами, которые получены из Framework Peers. Пользователи Peers содержат состояние и поэтому поддерживаются каркасом 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 создает два User Peers, которые ссылаются друг на друга - один для 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
}

Итак, управление памятью Xamarins не нарушено в iOS, но вы должны знать об этих "gotchas", специфичных для работы на iOS.

Существует отличная страница Томаса Бэндта, названная Xamarin.iOS Memory Pitfalls, которая идет на это более подробно, а также предоставляет некоторые очень полезные советы и подсказки.