以下の続きの記事になります。
おさらい
-
UnityEngine.Objectでは等値演算子==(及び非等値演算子!=)がオーバーロードされている - オーバーロードされた等値演算子では
Object.CompareBaseObjectsメソッドに判定処理を委譲している
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);
public static bool operator !=(Object x, Object y) => !Object.CompareBaseObjects(x, y);
-
Object.CompareBaseObjectsメソッド内では純粋なnullチェックやInstanceIDの等値判定に加えて、渡されたオブジェクトが「破棄」されていないか(Unityフレームワーク上で生存しているか)をチェックするObject.IsNativeObjectAliveメソッドが実行されている -
Destroy実行前後で、検証対象メソッドの戻り値は以下のように変化した(
UnityEngine.Objectに生えている静的な非公開メソッド)
| IsNativeObjectAlive | DoesObjectWithInstanceIDExist | GetCachedPtr | |
|---|---|---|---|
| 破棄前 | true | true | インスタンス固有の値 |
| 破棄後 | false | false | 0 |
-
等値演算子
==とis演算子は挙動が異なる -
[UnityEngine.Objectを継承したクラスのインスタンス] is nullのように書いた場合、is演算子はそのインスタンスに本当にnullが代入されているかを判断するため、既に破棄済みのインスタンスでも実際にnullが代入されていなければfalseを返す
ガベージコレクション
Unityがスクリプト用の言語として標準的にサポートしているこのC#は、メモリ管理をガベージコレクション(GC)に任せるという仕様を持つ言語です。
GCの存在によって、私たちプログラマはメモリ管理に煩わされることが少なくなっています。
一方で、プログラマ(C#のユーザ)が任意のタイミングでメモリを解放することは仕様上できなくなっています。
参考
岩永さんという方が運営されているブログ「++C++;」の以下の記事は、C#のメモリ管理の基本的な概念を知る上でとても参考になります。
メモリリーク
これまで検証した内容やC#のメモリ管理の仕様を踏まえて考えると、どのような場合にメモリリークを起こすか見当がつきます。
ポイントとしては、オブジェクトの破棄を行うDestroyが、C#のユーザ側がメモリ解放を自由にできないという仕様上、メモリ解放などは行わずにいわば破棄フラグを立てるような挙動を取るということです。
以下のようなテストコードを考えます。
using UnityEngine;
public class SampleTest : MonoBehaviour
{
private readonly SampleMonoBehaviour[] sampleMonoBehaviours = new SampleMonoBehaviour[10000];
private void Start()
{
for (int i = 0; i < sampleMonoBehaviours.Length; i++)
{
sampleMonoBehaviours[i] = new GameObject($"Game Object {i}").AddComponent<SampleMonoBehaviour>();
Destroy(sampleMonoBehaviours[i]);
}
}
private void Update()
{
foreach (var sample in sampleMonoBehaviours)
{
Debug.Log(sample.GetName());
}
}
}
これは、破棄されたインスタンスの参照を保持して継続的にアクセスするというコードです。
使用したSampleMonoBehaviourの実装は特に関係はありませんが、以下に示しておきます。
using UnityEngine;
public class SampleMonoBehaviour : MonoBehaviour
{
private GameObject go;
private void Awake()
{
// 破棄された後にgameObjectプロパティにアクセスするとMissingReferenceExceptionが発生するが
// 破棄されるより前に参照をコピーしておけば例外は発生しない
go = gameObject;
}
public string GetName()
{
return go.name;
}
}
このコードを実行すると、SampleMonoBehaviourが破棄された後も参照が保持されて、GetNameメソッドが呼び出され続けているということが確認できます。
このように破棄後も誰かが参照を保持し続けているとGCの収集対象にならずにインスタンスが生存し続けて、実質的なメモリリークを起こしてしまいます。
これを回避するには、nullチェックを行った後は実際にnullを代入するなどの対策が必要になります。
