Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

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

Resources.UnloadUnusedAssets

一応Resources.UnloadUnusedAssetsについて簡単に説明します。リファレンスはこちら

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

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

(2)
スクリーンショット 2015-12-20 16.06.24.png

(3)
スクリーンショット 2015-12-20 16.06.45.png

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

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

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

ここから本題です。

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

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 ());
    }
AssetContainer.cs
public class AssetContainer : MonoBehaviour
{
    public Texture texture;
}

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

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

スクリーンショット 2015-12-20 14.54.28.png

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

原因

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

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

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

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

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

これはMonoBehaviourTextureのベースオブジェクトである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)の箇所を次のように書き換えて実行してみます。

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

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

スクリーンショット 2015-12-20 14.57.48.png

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

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

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

まとめ

  • GameObjectDestroyした後でResources.UnloadUnusedAssetsを実行してもアセットがアンロードされない場合がある
  • そのGameObjectを参照していた変数にnullを代入したり、OnDestroyでアセットを参照するフィールドにnullを代入するとアンロードされるようになる
klab
モバイルオンラインゲーム、その他スマートフォン関連サービス、及びサーバーインフラ開発・運用
http://www.klab.com/jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした