Problem
UIGestureRecognizer
とその継承クラスはなんらかのビューにジェスチャを付けたいときに使います。たとえば UIView
がタップされたことを検知したいなら次のようになるでしょう。
public override void ViewDidLoad()
{
var tapGesture = new UITapGestureRecognizer(this.HandleTapped);
view.AddGestureRecognizer(tapGesture);
}
private void HandleTapped()
{
Console.WriteLine("Tapped!");
}
おめでとうございます。これであなたは解放されない ViewController
を手に入れました。
サンプルはこちらです。https://github.com/ailen0ada/XamarinAdvent18Sample
色の名前をタップすると全面に色を表示する画面に遷移し,タップすると透明度が変化するようになっています。行をタップしたときに GC.Collect
を走らせているので,きちんと解放されればファイナライザが呼ばれてデバッグコンソールに出力が行われるはずです。解放されれば。
TL;DR
- 強参照
- ちゃんと参照を外すか,
ObjCRuntime.Selector
を使ったアプローチに変える - そのうちもう少し簡単に解放できるようになる
Cause
Action
を渡すコンストラクタの内部で,別のクラスが作られてにここで渡した Action
が抱え込まれるからです。おおよそ次のようなコードに展開されます。
[DesignatedInitializer]
public UIGestureRecognizer(Action action)
: this(Selector.GetHandle("target"),
(UIGestureRecognizer.Token)
new UIGestureRecognizer.ParameterlessDispatch(action))
{
}
[Register("__UIGestureRecognizerParameterlessToken")]
public class ParameterlessDispatch : UIGestureRecognizer.Token
{
private Action action; // ここで抱え込まれて誰も解放しない
internal ParameterlessDispatch(Action action)
{
this.action = action;
}
[Export("target")]
[Preserve(Conditional = true)]
public void Activated()
{
this.action();
}
}
ネイティブ側の UIGestureRecognizer
が要求するのはセレクタと,そのセレクタを受け取るターゲットのポインタです。いろいろXamarin側でコンストラクタを定義していますが最終的にはそこに行き着きます。詳しくは拙著 Xamarin Myth Busters をどうぞ。
Solution
A; TapGesture
をビューから剥がす
プッシュ遷移の際には,必ず DidMoveToParentViewController
を通ることになっているので,これを使います。
public override void DidMoveToParentViewController(UIViewController parent)
{
base.DidMoveToParentViewController(parent);
if (parent == null) // 戻る遷移の場合は parent が null
{
foreach (var recognizer in this.View.GestureRecognizers)
{
this.View.RemoveGestureRecognizer(recognizer);
}
}
}
これで ViewController
については解放されます。
B; コンストラクタでセレクタを渡す
次のように書き換えて,ネイティブ呼び出しに近い形にしてしまいましょう。
public override void ViewDidLoad()
{
var tapGesture = new UITapGestureRecognizer(this, new ObjCRuntime.Selector(nameof(HandleTapped)));
view.AddGestureRecognizer(tapGesture);
}
[Foundation.Action(nameof(HandleTapped))]
private void HandleTapped()
{
Console.WriteLine("Tapped!");
}
これでターゲットである UIViewController
のポインタと,セレクタが渡されるだけになったので参照が弱くなりました。
C; Dispose()
する
Xamarin.iOS 12.2.1.12 の時点ではまだ実装が入っていませんが,きちんと UIGestureRecognizer.Dispose()
を呼べば抱えているものをすべて解放する実装がそのうち入ります。
https://github.com/xamarin/xamarin-macios/issues/4190
マージされた箇所を見ると,どこにアタッチされたかを保持しておいて全部解除していくという動きをしています。Aパターンとやることは同じで方向が逆ということですね。
Conclusion
- 参照について意識しましょう。
- Rxを使うなら
Subscribe
したらちゃんとDispose
しましょう。 - Xamarin Profilerが使える環境なら使いましょう。(ライセンスはあってもアプリの構造によっては使い物にならないのじゃよ)