ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

AssetBundleを読み込む手段 メモリの使用量について

Addressable Asset Systemをそろそろ理解したいのだが、その前にAssetBundleのベストプラクティスを考える前提知識を整える必要がある。

AssetBundleにはいくつか利用方法(読み込み方法)があるようだが、それぞれ特徴があるようなので確かめてみる。

AssetBundleについての基本知識

qiita.com

Assetを読み込む方法

LoadFromFile

AssetBundleファイルのパスを指定して読み込む。

AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundlePath);

LoadFromStream

AssetBundleファイルのパスを指定して、Streamで読み込む。

AssetBundle assetBundle;
using (FileStream fileStream = new FileStream(assetBundlePath, FileMode.Open, FileAccess.Read))
{
    assetBundle = AssetBundle.LoadFromStream(fileStream);
}

暗号化されている場合、

FileStream fileStream = new FileStream(assetBundlePath);
// なんらかの復号Stream
DecryptStream decryptStream = new DecryptStream(fileStream, password, salt);
AssetBundle assetBundle = AssetBundle.LoadFromStream(decryptStream);

LoadFromStreamの復号について

暗号化アルゴリズムによって中のメモリ確保が変わる。 一般的な暗号化Stream(CryptStreamなど)を用いた場合は、上記の処理でAssetBundleファイルそのままをアンマネージドメモリへ読み込む。と思われる。(未検証) CanSeekがtrueな暗号化Streamを用いた場合は、上記処理ではメタ情報のみがMonoメモリに読み込まれる。 また、LoadAssetする時点で復号に用いたStreamが破棄されている場合、Loadに失敗する。 詳細な実装はC++領域なため公開されていないが、この動作から、LoadAssetの段階でも、目的のAssetの部分までSeekし、目的のAssetだけを復号して読み込んでいると推測できる。

LoadFromMemory

Monoメモリ上のバイト列からAssetBundleを生成する。

byte[] bytes = File.ReadAllBytes(assetBundlePath);
AssetBundle assetBundle = AssetBundle.LoadFromMemory(bytes);

のように使用する。 AssetBundleが暗号化されていれば復号処理を挟む。

// ファイルから直接バイナリとして全て読み込む。
byte[] encryptedBytes = File.ReadAllBytes(assetBundlePath);
byte[] decryptedBytes = Decryptor.Decrypt(encryptedBytes)
AssetBundle assetBundle = AssetBundle.LoadFromMemory(decryptedBytes);

検証内容

準備

適当な音楽ファイルを20個用意した。全てmp3で総容量は98.6MB、平均して1ファイル5MBだった。 これをLZ4圧縮で1つのAssetBundleにビルドした。(平文AssetBundle) さらに、その上でCanSeekな暗号化Streamで暗号化したものを用意した。(暗号文AssetBundle) 具体的には、こちらの記事をそのまま流用させてもらった。 tsubakit1.hateblo.jp 平文AssetBundle、暗号文AssetBundle共にサイズは85.2MBだった。

検証

LoadFromFile, LoadFromStream, LoadFromMemoryを用いた3つの検証を行った。 それぞれ、AssetBundleを読み込む段階と、その後LoadAssetでAudioClipを1つ読み込む段階でメモリの使用状況を確認し、UsedMemoryの変化を記録した。 LoadFromStreamのみ暗号文AssetBundleを使用し、他は平文AssetBundleを使用している。 LoadFromMemoryはReadAllBytesが事前に必要なため、そこも確認している。

結果

LoadFromFile LoadFromStream LoadFromMemory
ReadAllBytes Mono: 7MB -> 88MB
Unity: 110MB -> 110MB
AssetBundle Mono: 7MB -> 7MB
Unity: 110MB -> 110MB
Mono: 7MB -> 7MB
Unity: 110MB -> 110MB
Mono: 88MB -> 88MB
Unity: 110MB -> 191MB
AudioClip Mono: 7MB -> 7MB
Unity: 110MB -> 116MB
Mono: 7MB -> 7MB
Unity: 110MB -> 116MB
Mono: 88MB -> 88MB
Unity: 191MB -> 197MB

ここから、以下の特徴が推測できる。

LoadFromFile

  • これで取得するのはAssetBundleのメタ情報のみ。
    • ここからAssetBundle.LoadAsset<T>で中のAssetを取得する際も、目的以外のAssetは無視され、不要なメモリは確保せず、メモリに優しい。
  • 暗号化されたAssetBundleファイルに使用できない。

LoadFromStream(CanSeekな暗号化を行った暗号文AssetBundle)

  • これで取得するのはAssetBundleのメタ情報のみ。
    • ここからAssetBundle.LoadAsset<T>で中のAssetを取得する際も、目的以外のAssetは無視され、不要なメモリは確保せず、メモリに優しい。
  • 暗号化されたAssetBundleファイルに使用できるが、そのための暗号化アルゴリズムに制約がある。

LoadFromMemory

  • AssetBundleファイルを一括でMonoメモリに読み込む必要があり、その上でUnityメモリにコピーする必要もある。
    • メモリ効率が非常に悪い。AssetBundle内の1つのAssetを使用したい場合でもAssetBundle全体を読み込む必要がある上に、適切に処理しても一時的にAssetBundle1つ分のメモリがMonoメモリとUnityメモリに同時に存在する状態を避けられない。
  • 暗号化されたAssetBundleファイルに使用できる。

結論

平文のAssetBundleを使用する場合はLoadFromFileが適切。 暗号化されたAssetBundleは基本的にはLoadFromMemoryを使用するが、CanSeekな暗号化アルゴリズムを使用すればLoadFromStreamでメモリに優しいAsset読み込みが可能になる。