知っておくべきAssetBundle管理の原理

前回の記事[「AssetBundleを明らかにする」](https://qiita.com/UWATechnology/items/f04241f14d4add3389c1)に続き、AssetBundleを探し続けましょう。管理の原理から、アセットのロードとアンロードの原理について説明します。

---
#AssetBundleロードの基礎
AssetBundleを介したアセットのロードは、2つのステップに分かれています。一番目のステップはAssetBundleオブジェクトを取得することであり、2番目のステップはこのオブジェクトを介して必要なアセットをロードすることです。そして一番目のステップは2つの方法に分かれており、下記に一般的に使用されるAPIと併せて詳しく説明します。

##一、AssetBundleオブジェクトのよく使うAPIを取得する
###（1）最初にWWWオブジェクトを取得し、次にWWW.assetBundleを介してAssetBundleオブジェクトを取得します。

**●public WWW(string url);**
BundleファイルをロードしてWWWオブジェクトを取得します。完了すると、メモリにより大きなWebStream（解凍されたコンテンツは通常、元Bundleファイルサイズの4〜5倍であり、テクスチャアセットの比率が大きくなる場合があります）を作成しますが、後続のAssetBundle.Loadは直接にメモリに実行できます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/ac110cd3-65e2-f590-7e35-3f511e88dec9.png)

**●public static WWW LoadFromCacheOrDownload(string url, int version, uint crc = 0);**
BundleファイルをロードしてWWWオブジェクトを取得します。同時に、解凍されたBundleコンテンツをキャッシュとしてディスクに保存します（Bundleがすでにキャッシュにある場合、この手順は省略されます）。完了すると、小さなSerializedFileのみがメモリに作成されます。後続のAssetBundle.Loadは、IOを介してディスクのキャッシュから取得します。

**●public AssetBundle assetBundle;**
前の2つのインターフェイスを介してWWWオブジェクトを取得した後、WWW.assetBundleを介してAssetBundleオブジェクトを取得できます。

###（2）AssetBundleを直接取得します。

**●public static AssetBundle CreateFromFile(string path);**
圧縮されていないBundleファイルを介してAssetBundleオブジェクトを同期的に作成し、これは最も早い作成方法です。完了すると、小さなSerializedFileのみがメモリに作成されます。後続のAssetBundle.Loadは、IOを介してディスクから取得します。

**●public static AssetBundleCreateRequest CreateFromMemory(byte[] binary);**
Bundleのバイナリデータを介して、AssetBundleオブジェクトを非同期で作成します。完了すると、より大きなWebStreamがメモリに作成されます。コールするとき、Bundleの解凍は非同期で実行されるため、圧縮されていないBundleファイルに対して、このインターフェイスはCreateFromMemoryImmediateと同等です。

**●public static AssetBundle CreateFromMemoryImmediate(byte[] binary);**
このインターフェイスは、CreateFromMemoryの同期バージョンです。

*PS：バージョン5.3で、名前はLoadFromFile、LoadFromMemory、LoadFromMemoryAsyncに変更されて、LoadFromFileAsyncは追加され、メカニズムもある程度に変更しました。詳細については、Unityの公式ドキュメントを参照してください。*

##二、AssetBundleからアセットをロードするためのよく使うAPI

**●public Object Load(string name, Type type);**
指定された名前とアセットタイプにアセットをロードします。ロードする時に他の依存するアセットが自動的にロードされます。つまり、一つのPrefabをロードすると、引用されているTextureアセットは自動的にロードされます。

**●public Object[] LoadAll(Type type);**
特定のアセットタイプのすべてのアセットをBundleに一回にロードします。

**●public AssetBundleRequest LoadAsync(string name, Type type);**
このインターフェイスは、Loadの非同期バージョンです。

*PS：バージョン5.xで、名前はLoadFromFile、LoadFromMemory、LoadFromMemoryAsyncに変更されて、LoadFromFileAsyncも追加されました。*

---
#AssetBundleロード　進級
##一、インターフェイスの比較：new WWWとWWW.LoadFromCacheOrDownload
###（1）前者の利点
●後続のロード操作はメモリ内で実行され、後者に比べてIO操作のコストが少なくなります。
●キャッシュファイルを形成できません、後者はキャッシュを格納するために追加のディスクスペースが必要です。
●WWW.texture、WWW.bytes、WWW.audioClipなどのインターフェイスを介して直接外部アセットをロードでき、後者はAssetBundleのみロードできます。
###（2）前者の欠点
●毎回ロードすると解凍操作が含まれますが、後者は2回目のロードでの解凍のコストを節約します。
●メモリには大きなWebStreamがありますが、後者のメモリには通常は小さなSerializedFileしかありません。（これは一般的な状況ですが、絶対的なものではありません。シリアル化された情報が多いPrefabの場合、SerializedFileがWebStreamより大きい可能性もあります。）
##二、メモリ分析
![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/5ed7f88a-9c20-aff9-555d-cb76756f53cf.png)
AssetBundleを管理する場合、ロードプロセスにメモリへの影響を理解することは非常に重要です。上図では、中間にAssetBundleがアセットをロードする後に、メモリに各オブジェクトの分布をリストし、左側に各タイプのメモリの生成に関するロードAPIを示します。

●**WWWオブジェクト**：最初のステップのメソッド1で生成され、メモリコストが低いです。
●**WebStream**：new WWWまたはCreateFromMemoryを使用するときに生成され、通常、メモリのコストは大きいです。
●**SerializedFile**：最初のステップの二種類のメソッドでも生成され、通常、メモリコストが低いです。
●**AssetBundleオブジェクト**：最初のステップの二種類のメソッドでも生成され、メモリコストが低いです。
●**アセット（Prefabを含む）**：2番目のステップでロードによって生成されます。アセットタイプ次第に、メモリコストは別々のサイズがあります。
●**シーンオブジェクト（GameObject）**：2番目のステップでInstantiateによって生成され、通常、メモリのコストは小さいです。

次の章では、このマップにあるさまざまなメモリオブジェクトに対してアンロード方式を分析して、メモリの残留やリークを回避します。

##三、注意点
●CreateFromFileは非圧縮のAssetBundleにのみ適用でき、AndroidシステムのStreamingAssetsは圧縮ディレクトリ（.jar）にあるため、CreateFromFileを使用する前に、非圧縮のAssetBundleをSDカードに配置する必要があります。Application.streamingAsstsPath = "jar：file：//" + Application.dataPath + "！/ assets /";

●iOSシステムのオープンファイル上限は256であるため、メモリにCreateFromFileまたはWWW.LoadFromCacheOrDownloadによってロードされるAssetBundleオブジェクトもこの値より低くなります。新しいバージョンでは、LoadFromCacheOrDownloadが上限を超えると、自動的にnew WWWの形式に変更してロードしますが、以前のバージョンでロードは失敗になります。

●CreateFromFileとWWW.LoadFromCacheOrDownloadをコールすると、ResistentManager.Remapperのサイズが大きくなります。そしてPersistentManagerがアセットの永続ストレージを維持します。Remapperは、メモリにロードされたアセットHeapIDとソースデータFileIDの間のマッピング関係を保存します。これはMemory Poolで、動作はMonoメモリに似ていて、減少せずに増加するだけなので、これら2つのインターフェイスの使用について合理的な計画を立てる必要があります。

●依存関係のあるBundleパッケージに対して、ロードするときに順序に注意してください。たとえば、CanvasAがBundleAにあり、依存するAtlasBがBundleBにありと仮定します。アセットが正しく引用されるようにするために、BundleBのAssetBundleオブジェクトを作成する最も遅い時点は、CanvasAをインスタンス化する前です。つまり、BundleAのAssetBundleオブジェクトを作成する時とLoad(“CanvasA”)時に、BundleBのAssetBundleオブジェクトはメモリにないことが可能です。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/15aed969-a16b-e4ad-f4c5-38cccad8bd1c.png)
●経験に基づいて、AssetBundleファイルのサイズは1MBを超えないようにすることをお勧めします。これは、一般的に、バンドルのロード時間とそのサイズの関係は直線的にではありませんから、大きすぎるBundleがより大きいロードコストを起こす可能性があります。

●WWWオブジェクトのロードは非同期であるため、1つずつにロードすれば下図のようなCPUが暇な状況になりやすいです（選択したフレームのVsyncが大部分を占めます）。このとき、CPU使用率を上げるまたはロード完了のスピードを加速するために、複数のオブジェクトを同時にロードすることをお勧めします。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/aa789ea1-1c6b-70a8-c9d2-28278c4971e4.png)

---
#AssetBundleのアンロード
上記にAssetBundleを介してアセットをロードする際のメモリ割り当て状況について言及しました。次には、よく使うAPIを組み合わせて割り当てられたメモリをアンロードする方法を紹介します。最終的にすべての関連するメモリをクリアするという目的を達成します。
##一、メモリ分析
![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/a5d4b3f1-2cdd-3d9e-e8dc-858d9a9e066b.png)
上図の右側に、さまざまなメモリオブジェクトをアンロードする方法を示します。

●**シーンオブジェクト（GameObject）**：このタイプのオブジェクトは、Destroy関数を使用してアンロードできます。
●**アセット（プレハブを含む）**：Prefab以外のアセットファイルは三つの方法でアンロードできます。
>1）Resources.UnloadAssetで指定するアセットをアンロードでき、CPUコストは小さいです。

>2）Resources.UnloadUnusedAssetsで全ての引用されていないアセットを一回でアンロードします。CPUコストは大きいです。

>3）AssetBundle.Unload(true)でAssetBundleオブジェクトをアンロードする時に、ロードされたアセットを一緒にアンロードします。

>Prefabに対して、今はDestroyImmediateを介してのみアンロードできます。アンロード後、AssetBundleをリロードしないとPrefabをリロードできません。メモリコストが小さいため、特定的なアンロードは一般的にお勧めしません。

●**WWWオブジェクト**：オブジェクトのDispose関数をコールします。またはnullに設定します。
●**WebStream**：WWWオブジェクトと対応するAssetBundleオブジェクトをアンロードした後、この部分のメモリはエンジンによって自動的にアンロードされます。
●**SerializedFile**：AssetBundleをアンロードした後、この部分のメモリはエンジンによって自動的にアンロードされます。
●**AssetBundleオブジェクト**：AssetBundleをアンロードする方法は2つあります。
>1）AssetBundle.Unload（false）を使用して、AssetBundleオブジェクトをアンロードする時にメモリにロードされたアセットを保持します。

>2）AssetBundle.Unload（true）を使用して、AssetBundleオブジェクトをアンロードする時にメモリにロードされたアセットをアンロードします。この方法はアセット引用のロストを引き起こしやすいため、頻繁に使用することはお勧めしません。

##二、注意点
AssetBundleオブジェクトがAssetBundle.Unload（false）を介してアンロードされた後、オブジェクトを再作成して前にロードしたアセットをメモリにリロードすると、冗長性を導き、つまり同じアセットが2つ存在します。

スクリプトの静的変数が引用したアセットは、Resources.UnloadUnusedAssetsをコールする時にアンロードされません。Profilerにこの引用状況を見られます。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/12afd791-86dc-52cc-5f16-5e7651abf8a0.png)

---
#UWAからの推奨案
上記の説明を通じて、AssetBundleのロードとアンロードについて明確に理解されていると思います。以下では、API選択に関する推奨を簡単に説明します。

●メモリに常駐する必要なBundleファイルに対して、メモリ占用の削減を優先します。したがって、Prefab以外のアセット（特にテクスチャ）を格納するBundleファイルに対しては、WWW.LoadFromCacheOrDownloadまたはAssetBundle.CreateFromFileを使用してロードし、WebStreamがメモリに常駐することを回避できます。より多くのPrefabアセットを格納するBundleの場合、WWW.LoadFromCacheOrDownload でロードする時に生成するSerializedFile はnew WWW が生成するWebStream より大きい可能性があるため、new WWWロードを使用することを考えしてください。

●ロード後にアンロードされるBundleファイルに対して、2つの状況があります。速度（シーンのロード時）優先と流暢さ（ゲームの進行中）優先です。
>1）シーンをロードする場合、WWWオブジェクトを1つずつロードすることによって発生するCPUの暇状況を回避するように注意する必要があります。ロード速度の速いWWW.LoadFromCacheOrDownloadまたはAssetBundle.CreateFromFileの使用を考慮できますが、その後の大量のアセットロード、IOコストを引き起こすことを回避する必要があります（直接にLoadAllすることが試せます）。

>2）ゲームが実行している場合、重い状況を引き起こさないために同期操作の使用を回避することが必要です。だからnew WWWとAssetBundle.LoadAsyncを一緒に使ってスムーズにロードすることを考えられます。ただし、Shaderやより大きいTextureなどのアセットに対して、初期化操作は時間がかかり、重いやすいことに注意すべきです。シーンをロードする時にこのようなアセットをプリロードすることをお勧めします。

●CreateFromMemory インターフェイスの読み込みが遅いため、Bundleを暗号化する必要のある状況のみで考慮します。

●Resources.UnloadUnusedAssets()インターフェイスのコストが高く、重いやすいため、できる限りにゲームでこのインターフェイスを回避します。Resources.Unload(obj) を使用して1つずつアンロードし、ゲームのスムーズさを確保することができます。

---
**上記のメモリ管理は、Unity5.3より前のバージョンに適していることに注意してください。Unityエンジンは、5.3でAssetBundleのメモリ使用量に一定の調整を加えており、現在、さらなる勉強と研究を行っています。**

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

UWA公式サイト：https://jp.uwa4d.com
UWA公式ブログ：https://blog.jp.uwa4d.com