Search Unity

アセットバンドルの使用方法を改善してメモリ消費を抑えよう

, 4月 9, 2020

あなたのアプリケーションが、アセットをコンテンツデリバリネットワーク(CDN)からストリーミングしているか、全てのアセットを 1 つの大きなバイナリにまとめているかに関わらず、あなたはきっと「アセットバンドル」という言葉をお聞きになったことがあるでしょう。 アセットバンドルは、(1 つまたは複数の)シリアライズされたアセット(テクスチャ、メッシュ、オーディオクリップ、シェーダーなど)を含んだ、ランタイムで読み込めるファイルです。

アセットバンドルは、直接あるいは Unity の Addressable Asset System(Addressables)などのシステム経由で使用することができます。Addressables システムは、プロジェクト内でアセットを管理するためのよりサポートされた使いやすい方法を提供するパッケージで、アセットバンドルを抽象化によって補足するものです。Addressables は開発者によるアセットバンドルの直接操作を最小限に抑えますが、アセットバンドルの使用がメモリ消費にどのように影響するかを理解しておくことは有用です。Addressables システムの概要は、こちらのブログ記事 Unite Copenhagen 2019 のこちらのセッションをご覧ください。

新しいプロジェクトを開始される開発者の皆様は、アセットバンドルを直接操作する代わりに Addressables を使用されることをぜひご検討ください。アセットバンドルの扱い方がすでに確立された状態でプロジェクトを進行中の皆様は、本記事内で触れている、アセットバンドルがランタイムメモリに与える影響についての情報が、最高の結果を得るための一助となるでしょう。

メモリキャッシュによるメモリ消費

Unity は、LZMA アセットバンドルを(現在は非推奨化されている)WWW クラスあるいは UnityWebRequestAssetBundle(UWR)を使用してダウンロードする際、メモリキャッシュとディスクキャッシュの 2 つのキャッシュを使用してアセットバンドルのフェッチング・再圧縮・バージョニングを最適化します。

メモリキャッシュ内に読み込まれたアセットバンドルは大量のメモリを消費します。特定のアセットバンドルのコンテンツに素早く頻繁にアクセスしたい場合を除いては、メモリキャッシュを使用すると消費メモリ量が大きくなり過ぎる傾向にあるため、代わりにディスクキャッシュを使用することが推奨されます。

バージョンあるいはハッシュ引数を UnityWebRequestAssetBundle API に提供すると、Unity はアセットバンドルデータをディスクキャッシュ内に保存します。これらの引数を提供しなければ Unity はメモリキャッシュを使用します。Addressables はデフォルトではディスクキャッシュを使用します。この挙動は UseAssetBundleCache フィールドから制御することができます。

AssetBundle.LoadFromFile()AssetBundle.LoadFromFileAsync() は LZMA アセットバンドルに常にメモリキャッシュを使用します。したがって、代わりに UnityWebRequestAssetBundle API をご使用になることをお勧めします。UnityWebRequestAssetBundle API が使用できない場合は、AssetBundle.RecompressAssetBundleAsync() を使用して LZMA アセットバンドルをディスク上で書き換えることも可能です。

社内テストの結果、ディスクキャッシュを使用した場合とメモリキャッシュを使用した場合の RAM の使用量に少なくとも 1 桁の違いがあることが分かっています。アプリケーションごとに、メモリの消費量と、追加で必要になるディスク容量およびアセットのインストール時間とのバランスを考える必要があります。

アセットバンドルのメモリキャッシュがアプリケーションのメモリの使用状況にどのように影響しているかを把握するには、ネイティブのプロファイラー(あるいは Xcode の Allocations Instrument)ツールを使用して ArchiveStorageConverter クラスからの割り当てを調査してください。このクラスが 10MB を超える量の RAM を使用している場合は、恐らくメモリキャッシュが使用されています。

Xcode の Allocations Instruments ― ArchiveStorageConverter クラスの使用しているメモリの量が表示されています。

アセットバンドルが具体的に何を読み込んでいるか確認する

大きなプロジェクト向けにアセットバンドルをビルドする場合、Unity のデフォルトでは、そこに含まれる重複した情報の最小化は実行されません。生成されたアセットバンドル内の重複データのインスタンスを特定するには、Unity の Consulting & Development グループのメンバーが Python で記述した AssetBundle Analyzer が便利です。このツールをコマンドラインから使用すると、生成されたアセットバンドルから情報が抽出され、便利なビュー機能を備えた SQLite データベース内に格納されます。このデータベースは DB Browser for SQLite などのツールを使用してクエリすることができます。このツールは、(バンドルを手動で作成した場合でも Addressables 経由で作成した場合でも)ビルドパイプライン内の非効率性を見付けて解決します。

AssetBundle Analyzer ツールの結果を DB Browser for SQLite を使用して分析する

この他に、AssetBundle Browser ツールダウンロード可能となっています(プロジェクト内にそのまま統合できます)。このツールは Addressables と類似した機能を提供しますので、Addressables をご使用の場合は、このツールをご使用になる必要はありません。

AssetBundle Browser ツールは、特定の Unity プロジェクト内のアセットバンドルの設定を閲覧・編集できる機能だけでなく、ビルド機能も提供します。その他にも、依存が原因でプルされている重複アセット([例]テクスチャ)の通知など、便利な機能がいくつか搭載されています。

AssetBundle Browser ツールは、(複数のバンドルによってプルされている)依存に起因した重複に関する警告を表示します。

重複アセットによるメモリ消費

アセットをどのようにアセットバンドルにまとめるかを決める際には、依存関係に注意しなければなりません。アセットバンドルのトポロジーの如何に関わらず、Unity は、アプリケーションバイナリ内(Resources フォルダー内あるいはその関連フォルダー内)のアセットと、アセットバンドルから読み込む必要があるアセットとを区別します。これら 2 種類のアセットは、それぞれ別の世界に存在していると考えることができます。Resources フォルダーの世界に存在しているアセットのインスタンスへの強参照を持つアセットバンドルを作成することは不可能です。そうしたアセットを参照する場合、Unity は、その複製を作成し、それをアセットバンドルの世界で使用します。

2 つのコンポーネントが「logo」画像への強参照を持っています。これらのコンポーネントが(プレイヤーに関連付けらるか特定のアセットバンドルに含められる形で)異なるアーカイブ中にシリアライズされた場合、それぞれに、この画像の独自のコピーが含まれることになります。

例えば、ゲームのロゴを考えてみましょう。ロゴはゲーム開始時のローディングシーンの UI 内に表示されるとします。このローディングシーンは、リモートアセットがディスクにストリーミングされる前に表示される必要があるので、すぐに使用できるようにロゴアセットをビルドに含めるとします。

この同じロゴは、UI のオプションパネル(ユーザーが言語選択やサウンドなどの設定を行う場所)にも使用されます。この UI パネルがアセットバンドルから読み込まれるのであれば、そのアセットバンドルはロゴアセットの独自のコピーを作成します。

ローディングシーンとオプションパネルの両方が同時に読み込まれる場合、ロゴアセットの両方のコピーが読み込まれ、これが重複となってメモリを消費します。

この問題は、片方または両方のシーンのハードリンクを破壊することで解決されます。ロゴがアセットバンドル内に存在する場合は、アセットへの参照に先駆けて一部のストリーミングが行われなければなりません。ロゴがバイナリ内(例えば Resources フォルダー内など)に存在する場合は、UI パネルは、ロゴアセットへの弱参照を持たなければならず、Resources.Load などの API 経由で読み込まれる必要があります。

ロゴ画像への弱参照としてストリングが使用されています。ユーザースクリプトは画像をランタイムで読み込んで適切なコンポーネントにアサインするためにこのストリングを使用する必要があります。

ユーザースクリプトは画像をランタイムで読み込んで適切なコンポーネントにアサインするためにストリングを使用する必要があります。ロゴアセットを含むアセットバンドルをアプリケーションの StreamingAssets ディレクトリ内に含めるのも丁度良い方法です。この場合もアセットバンドルからアセットを読み込む必要はありますが、バンドルをローカルでホスティングしているので、コンテンツのダウンロードにコストが掛かりません。

アセットの参照にストリングやパスや GUID を使用するのは直感的ではありませんが、Unity のデフォルトのドラッグアンドドロップ参照機能を可能にするカスタムのインスペクターを、弱参照されたフィールドに作成すると良いかもしれません。また、Unity の MemoryProfiler パッケージを使用して、メモリ内で重複しているアセットを特定することもお忘れなく。Addressables システムには、依存の重複をチェックするための独自のメカニズムが備わっています(詳細はドキュメンテーションのこちらのページをご覧ください)。

まとめ

Addressables システムはアセットバンドルを抽象化によって補足しますが、内部的な仕組みを理解しておくことで、上記でご紹介したようなコストの高いパフォーマンス問題を回避することができます。

今後もこれに関連したブログ記事をシリーズで公開する予定です。特に取り上げてほしいトピックがございましたら、ぜひコメントをお寄せください!

14 replies on “アセットバンドルの使用方法を改善してメモリ消費を抑えよう”

I pay a quick visit daily a few websites and blogs to read
articles, but this blog provides feature based articles.

This might be a noob question but I can not find what will be included in the asset bundle. If I use the AssetBundle Browser tool to create a bundle then the only the prefab is bundled, not the FBX that this prefab is dependent on. Am I missing something? I was under the impression that ALL resources would be in the bundle?

Given that modern decompression can be so fast why not have an option for compressed in RAM. In theory you can use less ram and it will be so fast when used that it will probably be indistinguishable from direct RAM access or only a fraction slower?

** For Next Article **

Would be nice for you to demonstrate best practices for the advanced and complex use cases.

1) Adding additional game content to a published build, via addressables, without addressables
2) Updating existing game content on a published build, via addressables, without addressables,
3) How to solve the Pink Shader issue in Editor, via addressables, without addressables.
4) How to organize the assets of an AssetBundle, how to decide which assets should be separate AssetBundles, which should be in the same.
5) How to know in editor that some Assets were changed that are part of AssetBundles, which means that you need to reupload your AssetBundles.

Hello!
I find this post very useful since I am always using AssetBundles to dynamically load scenes in runtime.
But apparently there are conflict informations on which method to use.
This articles states this:

“AssetBundle.LoadFromFile() and AssetBundle.LoadFromFileAsync() always use the memory cache for LZMA AssetBundles. We therefore recommend using the UnityWebRequestAssetBundle API instead.”

But the documentation on the class UnityWebRequestAssetBundle.GetAssetBundle says otherwise:

“Note, that while you can use this API to load Asset Bundle from local storage (using file:// URI or jar:file// on Android), this is not recommended, use AssetBundle.LoadFromFileAsync instead.”

In my case, I am currently downloading the AssetBundles from StreamingAssets using the WebRequest class.
My question is: should I keep using the WebRequest method (as this article recommends), or should I switch to LoadFromFileAsync (as the documentation recommends)?

In short; Asset Bundles are garbage over which you have very little control.

They bundle hierarchy of references without telling you. Even with digging in some poor documentation, you would never figure out what exact type of bundles you need to use without running some profiling. Lot of undocumented thing; such if you try to load a type from a bundle, it loads EVERYTHING from the bundle. Don’t get me started on the stupid gymnastic needed to prevent shader duplication IN EVERY SINGLE BUNDLE! Or how Atlas don’t work with Asset Bundle out of the box.

Here’s some of the issues raised with them: https://forum.unity.com/threads/standard-shader-duplicated-in-asset-bundle-build.593248/#post-5020673

Addressables don’t fix any of the issues with Asset Bundles; it simply hides the issues under a layer of abstraction, making it even harder to figure out why something doesn’t work or eat so much memory.

It looks like AssetBundles and Addressables currently have some limitations. For example it would be nice if there would be exposed LZMA compression and extraction endpoints in a similar manner like dotnet uses for DeflateStream and GZipStream (i.e. by passing byte array or encoded string). This would allow to have generic, non-platform specific compressed assets plus it would give ability to create assets on runtime.

Great post! our teams are heavily using asset bundles already. a few questions:

1. We are manually downloading asset bundles (via WWW and then save them to disk using http://www.bytes), so we are not going through any of the asset bundle “specific” APIs – the disk cache is not used in these cases, correct ?

“When Unity downloads an LZMA AssetBundle using the WWW class (which is now deprecated) or UnityWebRequestAssetBundle (UWR), Unity optimizes the fetching, recompressing, and versioning of AssetBundles using two caches: the memory cache and the disk cache.”

I mean – this information is not relevant for our case at the moment.

2. “AssetBundles loaded into the memory cache consume a large amount of memory. Unless you specifically want to frequently and rapidly access the contents of an AssetBundle, the memory cache is probably not worth the memory cost. Instead, use the disk cache.”

Is there any way to avoid the memory cache then?

3. AssetBundle.LoadFromFile() and AssetBundle.LoadFromFileAsync() always use the memory cache for LZMA AssetBundles. We therefore recommend using the UnityWebRequestAssetBundle API

If all our bundles are using LZMA, will we benefit anything from moving to UnityWebRequestAssetBundle ?

Hey there!

I’ve run by your questions to one of our staff members who provided me with the following response:

1: Correct! The AssetBundleCache will not be used unless a cache version/hash is provided in the download.

2: The naming “memory cache” is rather misleading. This is simply decompression. If you have an LZMA AssetBundle, then it needs to fully decompress in memory as the entire file before any data can be used. If you are loading an LZ4 AssetBundle then it can decompress by chunks that cover the data blocks that you need. Uncompressed, it would not have any need to decompress into memory “cache” at all.

3: There could be some benefit in that it will recompress the AssetBundle. In this regard however, you can manage your own cache and use LoadFromFile. By using AssetBundle.RecompressAssetBundleAsync, using the two runtime enum LZ4/Uncompressed, you can mimic similar behaviour of using our Caching API. By recompressing an AssetBundle you receive from http://WWW.bytes to Uncompressed or LZ4 to manage how much decompress in memory is needed.

The assetbundles methods are very integrated into Unity and sometimes linked to versions of Unity and always by platform for things like Shaders. A better way might be to come up with independent cross-platform compatible assets – Textures (platform independant, convert on load or with copy per platform), Meshes, AudioClips, Shared Material (definition) library instead of shaders. This is less optimal but more flexible

The blog posting keep asking for a captcha but none exists

Perfect timing, I’m working on a live game for mobile right now, Addresables is a treasure! I easily implemented a remote config system for themes in the game, so for every occasion, the team can change UI and feel of the game without sending a forced update on the store.
However, I’m not completely sure if my system is robust and maintainable. More best practices and structural tutorials on addressables would be nice. <3

Comments are closed.