個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Xamarin に関する事を中心に書いています。 Microsoft MVP for Development Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

Xamarin.iOS でメモリーリークをコントロールするために知っておきたいこと

はじめに


こんにちは、@hiro128_777です。


この記事は「Xamarin その1 Advent Calendar 2018」の21日目になります。


Xamarin.iOS は基本的には、Objective-C の薄い Wrapper であることは間違いありませんが、オブジェクトが、マネージドの世界とネイティブの世界でそれぞれに存在し、互いに関係性を持ちながら、インスタンスが生成・破棄されているので、GC の挙動を正しく理解しておかないとメモリリークをコントロールするのが難しくなる弱点があります。今回はそこについてのお話です。


この記事の内容については Xamarin.iOSソースコードhttps://github.com/xamarin/xamarin-macios/blob/master/runtime/runtime.mに記載されています。


これは、Xamarin.iOSGC の挙動について調べるために、Xamarin.iOSソースコード を読んでいるときに偶然発見しました。これを読むと Xamarin.iOSGC の挙動のクセがわかると思います。


ですが、こんな超重要な事が、そんなところに書いてあってもなかなか皆さんが目にする機会はないと思い、今回日本語に翻訳し説明を追加しました。
※わかりやすくするために色々補完して翻訳していますので、原文に忠実な翻訳ではありません。


参照カウンタについての注意点

Wrapper types と User types


Wrapper types は、UIViewUIButton のような Objective-C の組み込み型をラップしたもので、マネージドの世界では、ネイティブオブジェクトのインスタンスへのハンドルだけを持っています。


一方、User types は、UIViewUIButton のような Wrapper types を継承し派生した型で、Objective-C に対応する型が無いものを指し、ネイティブオブジェクトのインスタンスへのハンドルの他に、マネージドな世界だけで管理されている、フィールド、プロパティやメソッドを持っています。


Xamarin.iOS における GC の挙動を考える上で、この2つの型の区別はとても重要となります。


Wrapper types の寿命について


これはとても簡単です。


Wrapper types のインスタンスの寿命は、ネイティブオブジェクトの寿命とはリンクしてません。


Wrapper types のインスタンスは、マネージドな世界にネイティブオブジェクトへのハンドル以外には何も保持していませんので、GCの判断によっていつでも自由に解放できます。もし、後の段階で再びそのオブジェクトが必要になった場合には Wrapper types のインスタンスが再度作成されるだけです。


User types の寿命について


こちらは簡単ではありません。


User types のオブジェクトは マネージドな世界にユーザーが定義した状態を含む可能性があるためGCの判断によって自由に解放することはできません。
よって、User types のオブジェクトを必要な間はずっと生かし続けることを保証する必要があります(マネージオブジェクトは強力な GCHandle によって保持されます)。


この場合の問題は「どうやって不要になったタイミングを判断するのか」ということになります。
これには、歴史的に2つのケースが存在します。

ケース1


ユーザーが Dispose を呼び出すと、マネージオブジェクトの参照が解放され、ネイティブオブジェクトとマネージオブジェクトの間のリンクが切断され、GC がそのオブジェクトを解放できるようになります。(そのマネージオブジェクトを保持している他のオブジェクトが何もなければ)


ケース2


参照カウンタが1に達すると、マネージオブジェクトからの参照が唯一の参照であり、ネイティブコードは当該オブジェクトを再び使用しないと安全に仮定できます。よって、リンクを解除し、ネイティブオブジェクトを解放して、GC がマネージオブジェクトを解放できるようにします。


ケース1で、ユーザーが Dispose を呼び出した後、ネイティブコードが何らかの理由でそのオブジェクトを使用しようとすると、問題が発生します。 Xamarin.iOS は対応するマネージオブジェクトが存在しないことを検出し、それを(再)作成しようとします。ですが、これは失敗し、プロセスを終了させる例外がスローされます(この時、スタックにはマネージドフレーム/例外ハンドラが存在しない可能性があります)。


解決策としては、Disposeを呼び出すときに、次のことを行う必要があります。

  • マネージオブジェクトの参照を解放する。
  • ネイティブオブジェクトの参照カウンタが0に達するまで、ネイティブオブジェクトと管理オブジェクトの間のリンクを切断しない。


これにより、ネイティブオブジェクトが存続している限り、マネージオブジェクトを引き続き参照することができます。

User types の挙動についての注意点

User types のオブジェクトは、次のいずれかの条件が発生したときにネイティブオブジェクトへの参照を解放するようになっています。

  • Dispose がオブジェクトに対して(手動で)呼び出されたとき。
  • マネージオブジェクトの Handle プロパティが変更されたとき。
  • GC がオブジェクトを解放したとき。これは、参照カウンタが1でその参照がマネージオブジェクトによる参照である場合にのみ発生します。

(それ以外の時は、マネージオブジェクトに強力な GCHandle があるため、GC の解放処理はブロックされます。)


User types のオブジェクトは、次のいずれかの条件が発生すると、ネイティブ <-> マネージオブジェクトのディクショナリーからオブジェクトが削除されます。

  • ネイティブオブジェクトが dealloc されたとき(参照カウンタは0になります)。
  • マネージオブジェクトの Handle プロパティが変更されたとき(変更前の Handle 値とマネージオブジェクトの間のリンクがディクショナリーから削除されます)。

 

User types のオブジェクトは、ネイティブの世界の2つの情報を追跡し続けています。

  • マネージオブジェクトへの GCHandle。
  • マネージオブジェクトのインスタンスが存在するかどうか。


User types のオブジェクトはすでに GCHandle を保存する Objective-C インスタンス変数を持っているので、別のインスタンス変数を作成しません。
User types のオブジェクトはマネージオブジェクトへの参照(MANAGED_REF_BIT)があるかどうかを GCHandle の1ビットで保存します。



まとめ


Wrapper types は単に、ラッパーですので難しいことは何もありません。
問題は、User types です。こちらは、マネージドな世界とネイティブの世界にそれぞれ状態を持つことになるので、GC の挙動が複雑になっています。


オブジェクトの解放がうまく行われない時には、今回の内容を確認してたいただくとヒントになると思います。実際私もこれを理解してからは、解放できなくて困ったり、強引な Dispose で Exception に悩まされることが無くなりました。


以上です。