はじめに
UnityでAssetBundleを用いて、別プロジェクトで作成したScriptableObjectを読み込みUIに画像を表示する処理を行っていたときのこと。
Editor上ではまったく問題ないのに、Build(実行ビルド)でだけ一枚目の画像だけが表示されないという奇妙な現象に直面しました。
複数の画像を連続で表示するにもかかわらず、なぜか最初の一枚だけが失敗するのです。
結論から言うと、これは参照カウントとガベージコレクション(GC)の罠でした。
問題の再現コード
[Serializable]
public class SpSo : ScriptableObject
{
public Sprite icon;
public bool isCheck; //テスト用
}
public class ReadTest
{
static List<SpSo> SpSoData = new();
async UniTask ReadTest(string bundlePath)
{
List<SpSo> SpSoData2 = new(); // ローカル変数
var bundleL = AssetBundle.LoadFromFileAsync(bundlePath);
await bundleL;
var bundle = bundleL.assetBundle;
foreach (string asset in bundle.GetAllAssetNames())
{
if (asset.EndsWith("SpSo.asset"))
{
var request = bundle.LoadAssetAsync<SpSo>(asset);
await request;
var spData = request.asset as SpSo;
if (!spData.isCheck)
SpSoData.Add(spData);
else
SpSoData2.Add(spData); // 一旦ローカル変数に入れる
break;
}
}
// あとでまとめて移す
foreach (var spData in SpSoData2)
{
if(...) //何かしらの条件チェック
SpSoData.Add(spData);
}
}
}
このコードでは、ScriptableObject SpSo をABから非同期で読み込み、その中の Sprite icon をUIに表示しようとしています。
Editorでは完璧に動きますが、Buildでは最初にSpSoData2に入ったSpriteを使った画像だけ表示されないという現象が発生しました。
コード解析
Editor上では、Unityがプロジェクト内の全アセットを常にロードしており、SpriteやTextureもProject内に存在しているため、意図せず参照が維持されている状態になっています。
しかし、Buildでは全てのリソースは明示的にロードされ、かつ誰かが保持しなければ即座に解放対象になる可能性があります。
最初のScriptableObjectがローカルリスト SpSoData2 に格納される
await によって一時的に中断され、戻ったタイミングでGCが走る
この時、SpSoData2 以外にそのデータを参照しているものが存在しない
GCがそのオブジェクトを解放してしまい、中のSpriteも texture == null になった…のようです。
なぜ「最初の一枚だけ」失敗するのかよくわからないですが…
対策
//...略す
foreach (string asset in bundle.GetAllAssetNames())
SpSoData.Add(spData); // 早めに永続リストへ入れて、即時保持する
//...略す
//保持後からチェック処理
SpSoData.RemoveAll(d => d.isCheck && !(...));//何かしらの条件チェック
おわりに
「AssetBundleからロードしたら、必ずどこかに参照を保持する」という習慣をつけておくことで、こうした事故を未然に防ぐことができます。