今回の記事はメモリリークに対して弱い参照を活用した事例について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 を表すクラスが必要だ。
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
///
/// Stores the animation state for a character
///
public class StoredAnimationState
{
///
/// The name of the animatoin
///
public string name;
///
/// Is the animation enabled
///
public bool enabled;
///
/// The current speed of the animation
///
public float speed;
///
/// The current progress through the animation
///
public float time;
///
/// The current weight of the animation
///
public float weight;
///
/// The current blend mode of the animation
///
public AnimationBlendMode blendMode;
///
/// The current layer of the animation
///
public int layer;
///
/// The current wrap mode of the animation
///
public WrapMode wrapMode;
///
/// The list of current mixing transforms for the animation
///
public List<string> mixingTransforms = new List<string>();
public override int GetHashCode()
{
return name.GetHashCode() ^ weight.GetHashCode();
}
public override bool Equals (object obj)
{
if(!(obj is StoredAnimationState))
return false;
var other = (StoredAnimationState) obj;
return name == other.name && enabled == other.enabled &&
Math.Abs(speed - other.speed) < Single.Epsilon && Math.Abs(weight - other.weight) < Single.Epsilon &&
blendMode == other.blendMode &&
layer == other.layer &&
wrapMode == other.wrapMode &&
mixingTransforms.Count == other.mixingTransforms.Count;
}
public StoredAnimationState()
{
}
//Configure from an animation
public StoredAnimationState(AnimationState state)
{
name = state.name;
enabled = state.enabled;
speed = state.speed;
time = state.time;
weight = state.weight;
blendMode = state.blendMode;
layer = state.layer;
wrapMode = state.wrapMode;
mixingTransforms = state.Get<mixing>().transforms;
}
///
/// Configure an existing animation state from the information in this transfer class
///
///
public void Set(Animation animation)
{
var anim = animation[name];
//Do our mixing transform match?
//First check if we have the same number, if they match then check
//the names
if(anim.Get<mixing>().transforms.Count != mixingTransforms.Count || !anim.Get<mixing>().transforms.All(mixingTransforms.Contains))
{
//Remove all of the existing mixings
foreach(var m in anim.Get<mixing>().transforms)
{
anim.RemoveMixingTransform(animation.transform.Find(m));
}
//Update the list
anim.Get<mixing>().transforms = mixingTransforms;
//Apply the new mixings
foreach(var m in mixingTransforms)
{
anim.AddMixingTransform(animation.transform.Find(m), true);
}
}
//Apply other variables
anim.enabled = enabled;
anim.weight = weight;
anim.speed = speed;
if(Mathf.Abs(anim.time - time) > 1)
anim.time = time;
anim.blendMode = blendMode;
anim.layer = layer;
anim.wrapMode = wrapMode;
}
///
/// Class to transfer a list of animation states
///
public class AnimationTransfer
{
///
/// The animation state to be transferred
///
public List<storedanimation> StoredAnimations;
///
/// The representation of an animation state
///
public class StoredAnimation
{
///
/// The name of the animation
///
public string name;
///
/// The data for the stored state of the animation
///
public StoredAnimationState data;
}
}
}
これで相当なコード量だ。基本的にこれでアニメーション ステートを表す、シリアライズ向きのクラスを作成出来ました。アニメーションへの適用を戻す方法も組み込まれてます。
それではその他のプレイヤーにどのようにして送信出来るか見ていくことにしよう:
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から借用させてもらう:
//次のクラスの提供者は:
//Jeff Richter - http://www.wintellect.com/CS/blogs/jeffreyr/archive/2009/12/22/receiving-notifications-garbage-collections-occur.aspx
public static class GCNotification {
private static Action<int32> s_gcDone = null; // The event’s field
public static event Action<int32> GCDone {
add {
// もし登録されたデリゲートが前になかった場合、通知を開始
if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
s_gcDone += value;
}
remove { s_gcDone -= value; }
}
private sealed class GenObject {
private Int32 m_generation;
public GenObject(Int32 generation) { m_generation = generation; }
~GenObject() { // Finalize メソッド
// もしオブジェクトが期待するジェネレーション(またはそれ以上)であれば、
// ガーベージコレクション(GC)が完了したばかりであることをデリゲートに通知
if (GC.GetGeneration(this) >= m_generation) {
//スレッドは安全に s_gcDone デリゲートを抜けた - 中断されない
Action<int32> temp = Interlocked.CompareExchange(ref s_gcDone, null, null);
//イベントを発動
if (temp != null) temp(m_generation);
}
// 少なくともひとつのデリゲートが登録されてる場合、
// AppDomain がロードしない場合、および
// プロセスがシャットダウンしない場合に、通知の発行を継続
if ((s_gcDone != null) &&
!AppDomain.CurrentDomain.IsFinalizingForUnload() &&
!Environment.HasShutdownStarted) {
// ジェネレーション 0 の場合、新規オブジェクトを作成。ジェネレーション 2 の場合、 オブジェクトを復活し、
// ガーベージコレクション(GC)に 次にジェネレーション 2 がガーベージコレクションするときにFinalize を再びコールさせる
if (m_generation == 0) new GenObject(0);
else GC.ReRegisterForFinalize(this);
} else { /* オブジェクトがなくなる */ }
}
}
}
このクラスはいくつかの参照されていないオブジェクトを作成し、ガーベージコレクションにより finalize されるのを待ち、そしてそれが発生したときに GCDone イベントを発行する。このイベントを、弱い参照が依然として有効であるかどうかチェックするためのキューとして使用出来て、有効でないものについては紐付けされたオブジェクトを削除する...
完成した WeakTable<T> および static 拡張された Get<T> メソッドは次のようになる:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Threading;
///
/// Weak entry for the table
///
class WeakEntry
{
///
/// The weak reference to the collectable object
///
public WeakReference weakReference;
///
/// The associated object with the reference
///
public object associate;
}
///
/// A weak table for a particular type of associated class
///
public class WeakTable<T> : IDisposable where T : class, new()
{
//When finished we can dispose of the notification
public void Dispose()
{
GCNotification.GCDone -= Collected;
}
///
/// The references held by the table
///
Dictionary<int, List<WeakEntry>> references = new Dictionary<int, List<WeakEntry>>();
///
/// Gets an associated object give an index of another object
///
/// <param name='index'>
/// The object to use as an index
/// </param>
public T this[object index]
{
get
{
//Get the hash code of the indexed object
var hash = index.GetHashCode();
List<WeakEntry> entries;
//Try to get a reference to it
if(!references.TryGetValue(hash, out entries))
{
//If we failed then create a new entry
references[hash] = entries = new List<WeakEntry>();
}
//Try to get an associated object of the right type for this
//indexer/make sure it is still alive
var item = entries.FirstOrDefault(e=>e.weakReference.IsAlive && e.weakReference.Target == index);
//Check if we got one
if(item == null)
{
//If we didn't then create a new one
entries.Add(item = new WeakEntry { weakReference = new WeakReference(index), associate = new T() });
}
//Return the associated object
return (T)item.associate;
}
}
///
/// Get an associate given an indexing object
///
/// <param name='index'>
/// The object to find the associate for
/// </param>
/// <typeparam name='T2'>
/// The type of associate to find
/// </typeparam>
public T2 Get<T2>(object index) where T2 : T, new()
{
//Get the hash code of the indexing object
var hash = index.GetHashCode();
List<WeakEntry> entries;
//See if we have a reference already
if(!references.TryGetValue(hash, out entries))
{
//If not the create the reference list
references[hash] = entries = new List<WeakEntry>();
}
//See if we have an object of the correct type and that the
//reference is still alive
var item = entries.FirstOrDefault(e=>e.weakReference.IsAlive && e.weakReference.Target == index && e.associate is T2);
if(item == null)
{
//If not create one
entries.Add(item = new WeakEntry { weakReference = new WeakReference(index), associate = new T2() });
}
//Return the associate
return (T2)item.associate;
}
public WeakTable()
{
//Setup garbage collection notification
GCNotification.GCDone += Collected;
}
///
/// Called when the garbage has been collected
///
/// <param name='generation'>
/// The generation that was collected
/// </param>
void Collected(int generation)
{
//Remove the references which are no longer alive
//Scan each reference list
foreach(var p in references)
{
//Scan each item in the references and remove
//items that are missing
removeEntries.Clear();
foreach(var r in p.Value.Where(r=>!r.weakReference.IsAlive))
removeEntries.Add(r);
foreach(var entry in removeEntries)
{
if(entry.associate is IDisposable)
(entry.associate as IDisposable).Dispose();
p.Value.Remove(entry);
}
}
}
List<WeakEntry> removeEntries = new List<WeakEntry>();
}
///
/// Extension class to support getting weak tables easily
///
public static class Extension
{
static Dictionary<Type, WeakTable<object>> extensions = new Dictionary<Type, WeakTable<object>>();
///
/// Get an associate for a particular object
///
/// <param name='reference'>
/// The object whose associate should be found
/// </param>
/// <param name='create'>
/// Whether the associate should be created (defaults true)
/// </param>
/// <typeparam name='T'>
/// The type of associate
/// </typeparam>
public static T Get<T>(this object reference, bool create = true) where T : class, new()
{
WeakTable<object> references;
//Try to get a weaktable for the reference object
if(!extensions.TryGetValue(reference.GetType(), out references))
{
//Verify that we should be creating it if missing
if(!create)
return null;
//Create a new table
extensions[reference.GetType()] = references = new WeakTable<object>();
}
//Get the associate from the table
return (T)references.Get<T>(reference);
}
}
//The following class is from:
//Jeff Richter - http://www.wintellect.com/CS/blogs/jeffreyr/archive/2009/12/22/receiving-notifications-garbage-collections-occur.aspx
public static class GCNotification {
private static Action<Int32> s_gcDone = null; // The event’s field
public static event Action<Int32> GCDone {
add {
// If there were no registered delegates before, start reporting notifications now
if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
s_gcDone += value;
}
remove { s_gcDone -= value; }
}
private sealed class GenObject {
private Int32 m_generation;
public GenObject(Int32 generation) { m_generation = generation; }
~GenObject() { // This is the Finalize method
// If this object is in the generation we want (or higher),
// notify the delegates that a GC just completed
if (GC.GetGeneration(this) >= m_generation) {
//Thread safe get of the s_gcDone delegate - will not be interrupted
Action<Int32> temp = Interlocked.CompareExchange(ref s_gcDone, null, null);
//Fire the event
if (temp != null) temp(m_generation);
}
// Keep reporting notifications if there is at least one delegate
// registered, the AppDomain isn't unloading, and the process
// isn’t shutting down
if ((s_gcDone != null) &&
!AppDomain.CurrentDomain.IsFinalizingForUnload() &&
!Environment.HasShutdownStarted) {
// For Gen 0, create a new object; for Gen 2, resurrect the
// object & let the GC call Finalize again the next time Gen 2 is GC'd
if (m_generation == 0) new GenObject(0);
else GC.ReRegisterForFinalize(this);
} else { /* Let the objects go away */ }
}
}
}
最初にディクショナリ定義をスイッチして参照の型を使用する必要があり、そしてその後は参照に関する WeakTable を先に取得できるようになる。
紐付けしたクラスの使用が完了したとき、 IDisposable.Dispose を使用して、もう使用しないことを明示する。
結論
この記事はずいぶん技術的に高度な記事としなってしまったが、少しでも意味が伝わったことを願うばかりだ。これで自身のクラスを、自身が制御しない生存期間のあるもの (すなわちAnimation Stateなど) に対して紐付ける方法が分かったはずであり、強力なテクニックとして身についたはずだ。楽しんでもらえてたら何よりだ!
-------------
んー、翻訳してみて何なんだが、ちょっと活用場面がなかなか思いつかないなー。良い活用方法がおもいついたらまた更新してみるわー。すまぬーー