2013年2月7日木曜日

Unityでの「メモリリークと弱い参照での対策」

今回の記事はメモリリークに対して弱い参照を活用した事例についてUnity Gemsからの翻訳を紹介したい:


November 17, 2012


はじめに


次のような場合はこの記事を読むべきだ:

  • ネットワーク越しにAnimationStates を送信したい (transformのミキシングの場合も含めて)
  • メモリリークすることなくオブジェクトに新規のプロパティを追加したい
  • ガーベージコレクションおよび弱い参照について学習したい

イントロダクション

Unityの長年の問題はバックエンドが C++ となっていて、コードのインタフェースが .NET となっていることだ。Unityも頑張って素晴らしいモノを作ってきたけど、いかんせんどうしても必要な値が取得出来ない場合がある。古典的な例はtransformのミキシングで、キャラクターの一部分にだけアニメーションを追加して再生する場合だ。追加、削除することは出来るが、あらかじめ何であるか把握する方法が必要になる。多くの場合では一つのコードにtransformのミキシングを追加する部分を記述して、別の関数の中でRPCコールを通して何であるかを把握したり、また別のスクリプトで削除することも出来る。

この課題を解決にはかなり骨の折れる作業となって、多くのクラスや変数を、格納したり、情報の管理ご必要となるし、さらにはオブジェクトが削除されたかはどうやって把握すれば良いのだろうか?メモリはどのように解放するか?オブジェクトをいつまでもロックしてメモリリークとならないか?場合によってはかなり高い確率でリークしてしまうだろう。
この記事ではあるクラスを他のあらゆるオブジェクトと紐付けて、そのクラスがオブジェクトそのものが生存しているときのみクラスも生存させる方法を確立する。使用するのはとても簡単だ!:
public class Extra
{
    public int anyVariablesYouLike;
}

...

anyObject.Get().anyVariablesYouLike = 1;

初めての紐付け- アニメーション ミキシング

では早速 AnimationState をミキシングしたいデータと紐付けるための基本的な方法をみていく。簡単なヘルパークラスを作成して一つのオブジェクトを他のオブジェクトに紐付けるコードを実現しよう。
public static class Extensions
{
    // 拡張機能を格納するクラス
    static Dictionary<object, Dictionary<Type, object>> _extensions = new Dictionary<object, Dictionary<Type, object>>();
 
    //紐付けされた拡張クラスを修正
    public static T Get<T>(this object o) where T : class, new()
    {
         //このオブジェクトがすでにあるかチェック
         if(!_extensions.ContainsKey(o))
         {
             //ない場合は追加
             _extensions[o] = new Dictionary<Type, object>();
         }
         //この紐付けがすでにあるかチェック
         if(!_extensions[o].ContainsKey(typeof(T)))
         {
             //ない場合は作成する
             _extensions[o][typeof(T)] = new T();
         }
         //紐付けは戻す
         return _extensions[o][typeof(T)];
 
    }
}
早速、このクラスでanyObject.Get<AnyClass>() を使用すればそのクラスのインスタンスを取得出来るようになる。ディクショナリでは O(1)  の参照であるため処理が速いはずだ。このためtransform をアニメーション ステートと紐付けしたいため、それを行うクラスを作成してみたい。RPC経由で送信したときに他のコンピュータ上で見つけることが出来るようにtransform の名前を使用する。
public class Mixing
{
    public List<string> transforms = new List<string>();
}
次に、Get<T> ヘルパー 関数を使用して何かをクラスに追加するとしよう。
Transform spine2;

public void SwingSword()
{
    var state = animation["swingSword"];
    //実際は歩行中の場合のみ使用する
    //だろうが簡便のため、そのテスト内容
    //は割愛する
    state.AddMixingTransform(spine2);
    //このアニメーション ステートに対する Mixing インスタンスを取得し、
    //新しい transform の名前を追加する
    state.Get().transforms.Add(spine2.name);
    state.enabled = true;
    state.weight = 1;
}
早速これにより Mixing クラスをこの特定のアニメーションと紐付け出来た。次にRPC経由で別のプレイヤーに AnimationStates を送信する方法を見ていく。

RPC 送信に適合した AnimationState を表すクラスが必要だ。

RPC のための StoredAnimationState クラス - クリックして展開



これで相当なコード量だ。基本的にこれでアニメーション ステートを表す、シリアライズ向きのクラスを作成出来ました。アニメーションへの適用を戻す方法も組み込まれてます。

それではその他のプレイヤーにどのようにして送信出来るか見ていくことにしよう:
public void SendAnimations()
{
      //transfer 構造を作成
      var transfer = new StoredAnimationState.AnimationTransfer();
      //格納されたアニメーションの一覧を作成
      transfer.StoredAnimations =
         //現在のリストの全てのステート
         animation.Cast<AnimationState>()
            //有効化されたときのみ
            .Where(state=>state.enabled)
            //格納されたアニメーションを作成
            .Select(state=>new StoredAnimationState.AnimationTransfer.StoredAnimation
                   {
                        //アニメーション名
                        name = state.name,
                        //アニメーションデータ
                        data = new StoredAnimationState(state)
                   })
            .ToList();
       //データをシリアライズ
       var m = new MemoryStream();
       var b = new BinaryFormatter();
       b.Serialize(transfer, m);
       //送信
       networkView.RPC("ReceiveAnimations", RPCMode.Others, m.ToArray());

}

[RPC]
void ReceiveAnimations(byte[] data)
{
     //transfer 構造を受信
     var m = new MemoryStream(data);
     var b = new BinaryFormatter();
     var transfer = (StoredAnimationState.AnimationTransfer)b.Deserialize(m);
     //全てのアニメーションを無効化
     //(必要な場合は再度有効化される)
     foreach(var state in animation.Cast<animationstate>())
        state.enabled = false;
     //transferから全てのアニメーションをセットアップ
     //送信
     foreach(var state in transfer.StoredAnimations)
         state.data.Set(animation);
}
これだとメモリリークしまくり!!!

うん、今更でゴメンだけど _extensions ディクショナリから強制的に取り除くことを行わないかぎり、このコードはみるみるうちにメモリを無駄使いする。ゲームオブジェクトを破棄するとき、AnimationState をディクショナリでのキーとして保持してしまっているため、メモリは解放されない。頻繁に発生するとメモリ消費は莫大だ。

一縷の希望


理由なく酷い目にあわせたわけでないので、そこは安心して欲しい。この問題には解決方法がきちんとある。あとは技術的に高度なので大変、という点が残る。

やるべきことは、AnimationState(あるいはキーが何であっても) が破棄された時にMixing (またら他の拡張クラス)を解放することだ。当然に、現在は掴んでいるため破棄できない。

弱い参照


最初にやるべきことはAnimationStateへの強い参照でなく、弱い参照をもつことだ。.NET には WeakReference 型がビルトインされているため、管理することが出来る。これで同じオブジェクトへの二つの弱い参照は同じハッシュコードをもつことがない。このためディクショナリのキーとして使用することができる。これで大丈夫、.NET の全てのオブジェクトには GetHashCode コールがあり、本当のキーが何であり、それを代わりに使用してくれる。ところで int 型なので注意をば。

すべてをジェネリックにすることでオブジェクトの紐付けをする WeakTable<T> クラスという発想が出てくる。これについては、またちょっと後に検証するので現時点ではガーベージコレクションされた時点の元のキーの紐付けを削除する必要がある。

ガーベージコレクションがいつ実行されたかを調べるのは結構大変だ。しかし、実際に調べるにはいくつかの参照されていない、回収されたときに見ることが出来るオブジェクトを作成することが出来る。finalization メソッドを使用することでこれは実現され、 Jeff Richterから借用させてもらう:

ガーベージコレクション 通知クラス - クリックして展開


このクラスはいくつかの参照されていないオブジェクトを作成し、ガーベージコレクションにより finalize されるのを待ち、そしてそれが発生したときに GCDone イベントを発行する。このイベントを、弱い参照が依然として有効であるかどうかチェックするためのキューとして使用出来て、有効でないものについては紐付けされたオブジェクトを削除する...

完成した WeakTable<T> および static 拡張された Get<T> メソッドは次のようになる:


完成した WeakTable - クリックして展開


最初にディクショナリ定義をスイッチして参照の型を使用する必要があり、そしてその後は参照に関する WeakTable を先に取得できるようになる。

紐付けしたクラスの使用が完了したとき、 IDisposable.Dispose を使用して、もう使用しないことを明示する。


結論

この記事はずいぶん技術的に高度な記事としなってしまったが、少しでも意味が伝わったことを願うばかりだ。これで自身のクラスを、自身が制御しない生存期間のあるもの (すなわちAnimation Stateなど) に対して紐付ける方法が分かったはずであり、強力なテクニックとして身についたはずだ。楽しんでもらえてたら何よりだ!
-------------

んー、翻訳してみて何なんだが、ちょっと活用場面がなかなか思いつかないなー。良い活用方法がおもいついたらまた更新してみるわー。すまぬーー

0 件のコメント:

コメントを投稿

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿