web-dev-qa-db-ja.com

Xamarin iOSのメモリリークはどこでも

過去8か月間Xamarin iOSを使用しており、多くの画面、機能、ネストされたコントロールを備えた重要なエンタープライズアプリを開発しました。 「推奨」として、独自のMVVM Arch、クロスプラットフォームBLLおよびDALを作成しました。 Androidの間でコードを共有し、BLL/DALでさえWeb製品で使用されます。

Xamarin iOSベースのアプリのどこでも修復不可能なメモリリークを発見したプロジェクトのリリースフェーズを除き、すべてが良好です。これを解決するためにすべての「ガイドライン」に従いましたが、実際には、C#GCとObj-C ARCは、モノタッチプラットフォームで互いにオーバーレイする現在の方法では、互換性のないガベージコレクションメカニズムのようです。

私たちが見つけた現実は、ネイティブオブジェクトと管理オブジェクトの間のハードサイクルです 意志 発生し、 頻繁に 自明でないアプリの場合。これは、たとえばラムダやジェスチャレコグナイザーを使用する場所であればどこでも簡単に実行できます。 MVVMの複雑さを加えると、ほぼ確実になります。これらの状況の1つだけを見逃すと、オブジェクトのグラフ全体が収集されることはありません。これらのグラフは他のオブジェクトを引き付け、ガンのように成長し、最終的にはiOSによる迅速かつ容赦のない駆除をもたらします。

Xamarinの答えは、問題を無関心に延期し、「開発者はこれらの状況を回避する必要がある」という非現実的な期待です。これを慎重に検討すると、これが次のことを認めていることがわかります。 ガベージコレクションはXamarinで本質的に壊れています

私が今気づいているのは、従来のc#.NETの意味では、Xamarin iOSで実際に「ガベージコレクション」を取得しないということです。 「ガベージメンテナンス」パターンを使用する必要があり、実際にGCを動かしてその仕事をさせますが、それでも完璧ではありません-非決定性.

私の会社は、アプリのクラッシュやメモリ不足を防ぐために大金を投資しました。基本的には、クラッシュを止めて販売可能な実行可能な製品を得るために、基本的にすべてのいまいましいものを明示的かつ再帰的に処理し、アプリにゴミのメンテナンスパターンを実装する必要がありました。お客様は協力的で寛容ですが、これが永遠に続くことはありません。 Xamarinがこの問題に取り組んでいる専任のチームを持ち、それがきっぱりと解決されることを願っています。残念ながら、そのようには見えません。

質問は、私たちの経験は、Xamarinで書かれた重要なエンタープライズクラスのアプリの例外またはルールですか?

更新

DisposeExメソッドとソリューションの回答を参照してください。

49

これらのメモリリークの問題を解決するために、以下の拡張メソッドを使用しました。 EnderのGameの最終バトルシーンを考えると、DisposeExメソッドはそのレーザーに似ており、すべてのビューとそれらに接続されたオブジェクトの関連付けを解除し、アプリをクラッシュさせないように再帰的に破棄します。

そのView Controllerが不要になったら、UIViewControllerのメインビューでDisposeEx()を呼び出すだけです。ネストされたUIViewに破棄する特別なものがある場合、または破棄したくない場合は、IDisposable.Disposeの代わりに呼び出されるISpecialDisposable.SpecialDisposeを実装します。

[〜#〜] note [〜#〜]:これは、アプリで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();
    }
21

Xamarinで書かれた重要なアプリを出荷しました。他にも多くの人がいます。

「ゴミ収集」は魔法ではありません。オブジェクトグラフのルートにアタッチされる参照を作成し、それをデタッチしない場合、それは収集されません。これは、Xamarinだけでなく、.NET、JavaなどのC#にも当てはまります。

button.Click += (sender, e) => { ... }はアンチパターンです。ラムダへの参照がなく、Clickイベントからイベントハンドラーを削除できないためです。同様に、管理対象オブジェクトと管理対象外オブジェクトの間に参照を作成するときに、何をしているのかを理解するように注意する必要があります。

「独自のMVVM Archを実行しました」については、高プロファイルのMVVMライブラリがあります( MvvmCrossReactiveUI 、および MVVM Light Toolkit ) 、すべてが参照/リークの問題を非常に深刻に受け止めています。

25
anthony

「ガベージコレクションは基本的にXamarinで破損している」というOPには同意できませんでした。

以下に、推奨されるように常に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を使用すると、252 KBの永続オブジェクトが274個まで収集されないことがわかります。

  4. これを修正する唯一の方法は、DisposeExまたは同様の機能をDispose()関数に追加し、Disposeを手動で呼び出して== trueを破棄することです。

概要:UITableViewController派生クラスを作成してから破棄/ヌル化すると、常にヒープが増加します。

13
Derek Massey

iOSとXamarinの関係はわずかに問題を抱えています。 iOSは、参照カウントを使用してメモリを管理および破棄します。参照が追加および削除されると、オブジェクトの参照カウントが増加および減少します。参照カウントが0になると、オブジェクトが削除され、メモリが解放されます。 Objective Cでの自動参照カウントとSwiftこれは役立ちますが、ネイティブiOS言語を使用して開発する場合、100%正しいポインタやメモリリークが発生するのは依然として困難です。

Xamarin for iOSでコーディングする場合、iOSネイティブメモリオブジェクトを使用するため、参照カウントを念頭に置く必要があります。 iOSオペレーティングシステムと通信するために、Xamarinは参照カウントを管理するピアと呼ばれるものを作成します。ピアには、フレームワークピアとユーザーピアの2種類があります。フレームワークピアは、有名なiOSオブジェクトのマネージラッパーです。フレームワークピアはステートレスであるため、基盤となるiOSオブジェクトへの強力な参照を保持せず、必要に応じてガベージコレクターによってクリーンアップできます。メモリリークは発生しません。

ユーザーピアは、フレームワークピアから派生したカスタム管理オブジェクトです。ユーザーピアには状態が含まれているため、コードに参照がなくてもXamarinフレームワークによって保持されます。

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

新しいMyViewControllerを作成し、ビューツリーに追加してから、UIViewControllerをMyViewControllerにキャストできます。このMyViewControllerへの参照がない可能性があるため、Xamarinはこのオブジェクトを「ルート化」して、基になるUIViewControllerが生きている間、これを維持します。そうしないと、状態情報が失われます。

問題は、相互に参照する2つのユーザーピアがある場合、自動的に壊れない参照サイクルが作成されることです。この状況は頻繁に発生します。

この場合を考慮してください:-

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

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

Xamarinは、相互に参照する2つのユーザーピアを作成します。1つはMyViewController用で、もう1つはMyButton用です(イベントハンドラーがあるため)。したがって、これにより、ガベージコレクターによってクリアされない参照サイクルが作成されます。これをクリアするには、mustイベントハンドラーのサブスクライブを解除します。これは通常、ViewDidDisappearハンドラーで行われます。

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

常にiOSイベントハンドラーの登録を解除します。

これらのメモリリークを診断する方法

これらのメモリの問題を診断する良い方法は、UIViewControllersなどのiOSラッパークラスから派生したクラスのファイナライザーにデバッグコードを追加することです。 (ただし、これはリリースビルドではなく、デバッグビルドにのみ配置してください。かなり遅いためです。

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 と呼ばれる優れたページがあります。このページでは、これについてさらに詳しく説明し、非常に役立つヒントを提供しています。

9
JasonB

DisposeExメソッドで、コレクションの表示可能なセルを削除する前に、コレクションビューのソースとテーブルビューのソースを破棄することに気付きました。表示セルのプロパティが空の配列に設定されることをデバッグするときに気づいたので、表示セルを破棄し始めると、「存在」しなくなり、ゼロ要素の配列になります。

もう1つ気づいたのは、スーパービューからパラメータービューを削除しないと不整合の例外が発生することです。特にコレクションビューのレイアウトを設定すると気付きました。

それ以外は、私たちの側で似たようなものを実装しなければなりませんでした。

5
dervish