Xamarin
Xamarin.iOS

Xamarin.iOSでWeakAttributeを使って循環参照を回避する

Xamarin.iOS11.10からWeakAttributeが追加されました。

これによって以前まではちょっとめんどくさいことをしなければいけなかった循環参照回避のためのフィールドの弱参照が凄い楽に書けるようになりました。

循環参照とは?

循環参照はオブジェクト同士がお互いに参照を持っている状態のことで、Objective-CやSwiftなどの場合オブジェクトが使われなくなったあとも適切に開放されず、メモリリークが起きます。

参考: メモリー管理とパフォーマンスの最適化 | Xamarin : XLsoft エクセルソフト

C#では基本循環参照によるメモリリークは起きないのですが、Xamarin.iOSでObjective-CのAPIを呼び出すことで発生する場合があります。

    public partial class ViewController : UIViewController
    {
        protected ViewController(IntPtr handle) : base(handle)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            var strongView = new StrongView(this);
            // ここでObjective-CのAPI(UIView.AddSubview)に
            // ViewControllerの参照を持ったStrongViewを渡したことで
            // 循環参照発生
            this.View.AddSubview(strongView);
        }

        partial void tapped(NSObject sender)
        {
            var viewController = UIStoryboard.FromName("Main", null).InstantiateInitialViewController();
            NavigationController.PushViewController(viewController, true);
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            Console.WriteLine($"{nameof(ViewController)} is disposed.");
        }
    }

    class StrongView: UIView
    {
        private ViewController viewController;

        public StrongView(ViewController viewController)
        {
            this.viewController = viewController;
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            Console.WriteLine($"{nameof(StrongView)} is disposed.");
        }
    }

上記の例のようなコードを書くと本来参照が開放されるときに呼ばれるDisposeメソッドが(ViewController・StrongViewともに)いつまでも呼ばれず、メモリを圧迫します。

今までの解決策(WeakReferenceクラス)

上のコードではStrongViewViewControllerの参照を直接持っているのが問題でした。

なのでWeakReferenceクラスを間に挟むことによって強参照を避けます。

    class WeakReferenceView : UIView
    {
        private WeakReference<ViewController> weakViewController;

        public WeakReferenceView(ViewController viewController)
        {
            this.weakViewController = new WeakReference<ViewController>(viewController);
        }
    }

上記のようにすることで弱参照となり、ViewControllerが使われなくなったときにちゃんとDisposeメソッドが呼ばれオブジェクトは破棄されるようになります。

ただし、weakViewControllerからViewControllerを取り出すには以下のようにするしかなく、(C#7.0で少し簡潔になりましたが)手間になります。

if(!weakViewController.TryGetTarget(out var strongThis))
{
    // ViewControllerの参照が開放されていた場合
    return;
}

// ViewControllerの参照が開放されていない場合

新しい解決策(WeakAttribute)

そこで今回新しく追加されたWeakAttributeの出番です。

WeakAttributeを使うと以下のように書けます。

    class WeakView : UIView
    {
        [Weak] 
        private ViewController viewController;

        public WeakView(ViewController viewController)
        {
            this.viewController = viewController;
        }
    }

とても簡潔になりました!

WeakAttributeをつけることでコードを汚さず、今まで通りの書き方で弱参照にすることができます。

もちろん、WeakAttributeを付けたフィールドは参照先が消えるとnullになるので扱いには気をつけたほうがいいです。

まとめ

Xamarin.iOS11.10から追加されたWeakAttributeを使うことでめんどくさかったフィールドの弱参照が簡潔に書けるようになりました。

ただし、もう1つのよくある循環参照であるクロージャでthisをキャプチャすることによる循環参照は今の所従来通りのWeakRreferenceクラスを用いたやり方しかありません。
こちらのほうが頻繁に起きる(しかも分かりづらい)ので今後に期待したいところです。