DestroyしてUnloadUnusedAssetsしてもアセットがアンロードされない場合がある話

Unity Advent Calendar 21日目の記事です。


書くネタがなかったので、地味ですが`Resources.UnloadUnusedAssets`に関する話をひとつ書きます。
`GameObject`を`Destroy`して`Resources.UnloadUnusedAssets`を実行しても、その`GameObject`で使われていたアセットが解放されない場合があるという話です。

なお検証はUnity 5.3.0f4で行っています。

## Resources.UnloadUnusedAssets

一応`Resources.UnloadUnusedAssets`について簡単に説明します。リファレンスは[こちら](http://docs.unity3d.com/ja/current/ScriptReference/Resources.UnloadUnusedAssets.html)。

次のプログラムでは`Resources.Load`でテクスチャをロードしたあと、その後それが不要になった後で`Resources.UnloadUnusedAssets`を呼び出しています。

```csharp:Test1.cs
using UnityEngine;
using System;
using System.Collections;

public class Test1 : MonoBehaviour
{
    bool doNext;

    IEnumerator Start ()
    {
        // テクスチャをロード
        var texture = Resources.Load<Texture> ("sample");
        Debug.Log (texture.name);

        // (1)
        yield return StartCoroutine (Wait ());

        /* ...textureを使ったなんらかの処理... */

        // テクスチャへの参照を捨ててGC実行
        texture = null;
        GC.Collect ();

        // (2)
        yield return StartCoroutine (Wait ());

        // Resources.UnloadUnusedAssetsを実行
        yield return Resources.UnloadUnusedAssets ();

        // (3)
        yield return StartCoroutine (Wait ());
	}

    void OnGUI ()
    {
        if (GUILayout.Button ("Do Next")) {
            doNext = true;
        }
    }

    IEnumerator Wait ()
    {
        while (!doNext) {
            yield return null;
        }
        doNext = false;
    }
}
```

(1)(2)(3)のそれぞれの時点でのテクスチャロード状況をUnity Profilerから確認してみます。

(1)
![スクリーンショット 2015-12-20 16.06.09.png](https://qiita-image-store.s3.amazonaws.com/0/77929/91d0e4e5-4351-37ec-2654-fd3501dbb805.png)

(2)
![スクリーンショット 2015-12-20 16.06.24.png](https://qiita-image-store.s3.amazonaws.com/0/77929/0edb82be-58d5-6272-e7ce-86c4f3b4f683.png)

(3)
![スクリーンショット 2015-12-20 16.06.45.png](https://qiita-image-store.s3.amazonaws.com/0/77929/3c3ef4d0-a031-8662-1bc5-9e5db6c88ea3.png)

(1)の時点でテクスチャのRef Countが1になっていたのが(2)の時点では0になってますが、それでもメモリ上にはテクスチャが残っています。`Resources.UnloadUnusedAssets`呼び出し後の(3)の時点でテクスチャがリストから消え、メモリ上からアンロードされたことがわかります。

*なおEditor上ではResources.UnloadUnusedAssetsを実行してもアンロードされない場合があります。検証の際はビルドして行ってください。*

## DestroyしてUnloadUnusedAssetsしても解放されないケース

ここから本題です。

先のコードを一部変更し、テクスチャを`Resources.Load`でロードするのではなく、テクスチャを持ったプレハブからゲームオブジェクトを生成するようにしました。なお`Start`以外のメソッドについては先のサンプルと同じです。

```csharp:Test2.cs(一部抜粋)
    IEnumerator Start ()
    {
        // テクスチャへの参照を持ったassetContainerを生成
        // assetContainer.textureフィールドにはインスペクタから適当なテクスチャが設定されている
        var assetContainer = Instantiate (Resources.Load<AssetContainer> ("samplePrefab"));
        Debug.Log (assetContainer.texture.name);

        // (1)
        yield return StartCoroutine (Wait ());

        // assetContainerを破棄
        Destroy (assetContainer.gameObject);
        yield return null;

        // (2)
        yield return StartCoroutine (Wait ());

        // Resources.UnloadUnusedAssetsを実行
        yield return Resources.UnloadUnusedAssets ();

        // (3)
        yield return StartCoroutine (Wait ());
    }
```

```csharp:AssetContainer.cs
public class AssetContainer : MonoBehaviour
{
    public Texture texture;
}
```

`assetContainer`が`texture`フィールドを持っていて、そこからテクスチャが参照されています。`Destroy(assetContainer)`してから`Resources.UnloadUnusedAssets`を実行することでそのテクスチャがアンロードされることを意図した形になってます。

このプログラムを実行し、(3)の時点でのアセットのロード状況を確認します。

![スクリーンショット 2015-12-20 14.54.28.png](https://qiita-image-store.s3.amazonaws.com/0/77929/1bc2513f-9e0a-5c17-e7ec-7f0721f49717.png)

`Resources.UnloadUnusedAssets`呼び出した後の(3)の時点でも、テクスチャがアンロードされずに残ってしまっています。そのためこのまま`Start`メソッド内で処理を続けるとメモリリークということになります。これが今回説明する問題のケースです。

## 原因

原因について説明します。

`Resources.UnloadUnusedAssets`の実装を見ることはできないので推測ですが、`Destroy(assetContainer.gameObject)`を実行しても*Monoレイヤー上では`assetContainer`変数が`null`になっていない*ことが原因と思われます。

説明のために先のプログラムの(3)の後に次のコードを追加して実行してみます。

```csharp
Debug.Log (assetContainer == null); //=> true
Debug.Log (assetContainer.texture.name); //=> sample
```

`assetContainer == null`は`true`を返しているので、その後の`assetContainer.texture.name`へのアクセスは通常であれば`NullReferenceException`が発生するのですが、実行してみると発生しません。

これは`MonoBehaviour`や`Texture`のベースオブジェクトである`UnityEngine.Object`が、==演算子のオーバーロードをしており、Monoレイヤー上では`null`でないものが==を使うと`null`とされてしまうことが原因です。

これについて詳しくは下記のページなどで説明されてます。（一番上は公式のブログポストです）

http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
http://qiita.com/satanabe1@github/items/e896303859be5d42c188
http://tsubakit1.hateblo.jp/entry/2015/05/28/014440

推測ですが、`Resources.UnloadUnusedAssets`はMonoレイヤー上で参照されなくなったアセットをアンロードするような実装になっていて、そのため`Destroy`したがMonoレイヤー上で参照が残っている場合に、そのアセットがアンロードされずに残ってしまうのではないかと思われます。

## 対処

Monoレイヤー上でもゲームオブジェクトを参照する変数の値を`null`にしてやり、テクスチャへの参照が残っていない状態にすれば解決します。
先のプログラムの`Destroy(assetContainer.gameObject)`の箇所を次のように書き換えて実行してみます。

```csharp
        // assetContainerを破棄し、さらにnull代入
        Destroy (assetContainer.gameObject);
        assetContainer = null; // <- この行を追加
        yield return null;
```

これを実行した結果が下の画像です。

![スクリーンショット 2015-12-20 14.57.48.png](https://qiita-image-store.s3.amazonaws.com/0/77929/0efef838-cf26-ba5f-006b-5eb04d6add7a.png)

テクスチャがアンロードされていることがわかります。

また他に、`AssetContainer`の`OnDestroy`で`texture`フィールドに`null`を代入するなどの対処法も考えられます。

なお、今回はコルーチンの中で`Resources.UnloadUnusedAssets`を呼び出しましたが、コルーチンが終了すれば`assetContainer`を参照しているところはなくなるのでその後`Resources.UnloadUnsedAssets`を呼び出せば問題なくアンロードされます。要はどこからも（本当に）参照されてなければそれでよいということです。

## まとめ

- `GameObject`を`Destroy`した後で`Resources.UnloadUnusedAssets`を実行してもアセットがアンロードされない場合がある
- その`GameObject`を参照していた変数に`null`を代入したり、`OnDestroy`でアセットを参照するフィールドに`null`を代入するとアンロードされるようになる