AssetBundle fundamentals

版本检查: 2017.3

-

难度: 高级

This chapter discusses AssetBundles. It introduces the fundamental systems upon which AssetBundles are built, as well as the core APIs used to interact with AssetBundles. In particular, it discusses both the loading and unloading of AssetBundles themselves, as well as the loading and unloading of specific Asset and Objects from AssetBundles.

For more patterns and best practices on the uses of AssetBundles, see the next chapter in this series.

3.1. Overview

The AssetBundle system provides a method for storing one or more files in an archival format that Unity can index and serialize. AssetBundles are Unity's primary tool for the delivery and updating of non-code content after installation. This permits developers to submit a smaller app package, minimize runtime memory pressure, and selectively load content optimized for the end-user's device.

Understanding the way AssetBundles work is essential to building a successful Unity project for mobile devices. For an overall description of AssetBundle contents, review the AssetBundle documentation.

3.2. AssetBundle layout

To summarize, an AssetBundle consists of two parts: a header and data segment.

The header contains information about the AssetBundle, such as its identifier, compression type, and a manifest. The manifest is a lookup table keyed by an Object's name. Each entry provides a byte index that indicates where a given Object can be found within the AssetBundle's data segment. On most platforms, this lookup table is implemented as a balanced search tree. Specifically, Windows and OSX-derived platforms (including iOS) employ a red-black tree. Therefore, the time needed to construct the manifest will increase more than linearly as the number of Assets within an AssetBundle grows.

The data segment contains the raw data generated by serializing the Assets in the AssetBundle. If LZMA is specified as the compression scheme, the complete byte array for all serialized assets is compressed. If LZ4 is instead specified, bytes for separate Assets are individually compressed. If no compression is used, the data segment will remain as raw byte streams.

Prior to Unity 5.3, Objects could not be compressed individually inside an AssetBundle. As a consequence, if a version of Unity before 5.3 is instructed to read one or more Objects from a compressed AssetBundle, Unity had to decompress the entire AssetBundle. Generally, Unity cached a decompressed copy of the AssetBundle to improve loading performance for subsequent loading requests on the same AssetBundle.

3.3. Loading AssetBundles

AssetBundles can be loaded via four distinct APIs. The behavior of these four APIs is different depending on two criteria:

Whether the AssetBundle is LZMA compressed, LZ4 compressed or uncompressed

The platform on which the AssetBundle is being loaded

These APIs are:

AssetBundle.LoadFromMemory(Async optional)

AssetBundle.LoadFromFile(Async optional)

UnityWebRequest's DownloadHandlerAssetBundle

WWW.LoadFromCacheOrDownload (on Unity 5.6 or older)

3.3.1 AssetBundle.LoadFromMemory(Async)

Unity's recommendation is not to use this API.

AssetBundle.LoadFromMemoryAsync loads an AssetBundle from a managed-code byte array (byte[] in C#). It will always copy the source data from the managed-code byte array into a newly-allocated, contiguous block of native memory. If the AssetBundle is LZMA compressed, it will decompress the AssetBundle while copying. Uncompressed and LZ4-compressed AssetBundles will be copied verbatim.

The peak amount of memory consumed by this API will be at least twice the size of the AssetBundle: one copy in native memory created by the API, and one copy in the managed byte array passed to the API. Assets loaded from an AssetBundle created via this API will therefore be duplicated three times in memory: once in the managed-code byte array, once in the native-memory copy of the AssetBundle and a third time in GPU or system memory for the asset itself.

Prior to Unity 5.3.3, this API was known as AssetBundle.CreateFromMemory. Its functionality has not changed.

3.3.2. AssetBundle.LoadFromFile(Async)

AssetBundle.LoadFromFile is a highly-efficient API intended for loading uncompressed or LZ4-compressed AssetBundle from local storage, such as a hard disk or an SD card.

On desktop standalone, console, and mobile platforms, the API will only load the AssetBundle's header, and will leave the remaining data on disk. The AssetBundle's Objects will be loaded on-demand as loading methods (e.g. AssetBundle.Load) are called or as their InstanceIDs are dereferenced. No excess memory will be consumed in this scenario. In the Unity Editor, the API will load the entire AssetBundle into memory, as if the bytes were read off disk and AssetBundle.LoadFromMemoryAsync was used. This API can cause memory spikes to appear during AssetBundle loading if the project is profiled in the Unity Editor. This should not affect performance on-device and these spikes should be re-tested on-device before taking remedial action.

Prior to Unity 5.3, this API was known as AssetBundle.CreateFromFile. Its functionality has not been changed.

3.3.3. AssetBundleDownloadHandler

The UnityWebRequest API allows developers to specify exactly how Unity should handle downloaded data and allows developers to eliminate unnecessary memory usage. The simplest way to download an AssetBundle using UnityWebRequest is call UnityWebRequest.GetAssetBundle.

For the purposes of this guide, the class of interest is DownloadHandlerAssetBundle. Using a worker thread, it streams downloaded data into a fixed-size buffer and then spools the buffered data to either temporary storage or the AssetBundle cache, depending on how the Download Handler has been configured. All of these operations occur in native code, eliminating the risk of expanding the managed heap. Additionally, this Download Handler does not keep a native-code copy of all downloaded bytes, further reducing the memory overhead of downloading an AssetBundle.

LZMA-compressed AssetBundles will be decompressed during download and cached using LZ4 compression. This behavior may be changed by setting Caching.CompressionEnabled.

When the download is complete, the assetBundle property of the Download Handler provides access to the downloaded AssetBundle, as if AssetBundle.LoadFromFile had been called on the downloaded AssetBundle.

If caching information is provided to a UnityWebRequest object, and the requested AssetBundle already exists in Unity's cache, then the AssetBundle will become available immediately and this API will operate identically to AssetBundle.LoadFromFile.

Prior to Unity 5.6, the UnityWebRequest system used a fixed pool of worker threads and an internal job system to safeguard against excessive, concurrent downloads. The size of the thread pool was not configurable. In Unity 5.6, these safeguards have been removed to accommodate more modern hardware, and allow for faster access to HTTP response codes and headers.

3.3.4. WWW.LoadFromCacheOrDownload

*Note: Beginning in Unity 2017.1, WWW.LoadFromCacheOrDownload simply wraps around UnityWebRequest. Accordingly, developers using Unity 2017.1 or higher should migrate to UnityWebRequest. WWW.LoadFromCacheOrDownload will be deprecated in a future release. *

The following information is applicable to Unity 5.6 or older.

WWW.LoadFromCacheOrDownload is an API that allows loading of Objects from both remote servers and local storage. Files can be loaded from local storage via a file:// URL. If the AssetBundle is present in the Unity cache, this API will behave exactly like AssetBundle.LoadFromFile.

If an AssetBundle has not yet been cached, then WWW.LoadFromCacheOrDownload will read the AssetBundle from its source. If the AssetBundle is compressed, it will be decompressed using a worker thread and written into the cache. Otherwise, it will be written directly into the cache via the worker thread. Once the AssetBundle is cached, WWW.LoadFromCacheOrDownload will load header information from the cached, decompressed AssetBundle. The API will then behave identically to an AssetBundle loaded with AssetBundle.LoadFromFile. This cache is shared between WWW.LoadFromCacheOrDownload and UnityWebRequest. Any AssetBundle downloaded with one API will also be available via the other API.

While the data will be decompressed and written to the cache via a fixed-size buffer, the WWW object will keep a full copy of the AssetBundle's bytes in native memory. This extra copy of the AssetBundle is kept to support the WWW.bytes property.

Unlike UnityWebRequest, each call to this API will spawn a new worker thread. Accordingly, on platforms with limited memory, such as mobile devices, only a single AssetBundle at a time should be downloaded using this API, in order to avoid memory spikes. Be careful of creating an excessive number of threads when calling this API multiple times. If more than 5 AssetBundles need to be downloaded, create and manage a download queue in script code to ensure that only a few AssetBundle downloads are running occurring simultaneously.

3.3.5. Recommendations

In general, AssetBundle.LoadFromFile should be used whenever possible. This API is the most efficient in terms of speed, disk usage and runtime memory usage.

For projects that must download or patch AssetBundles, it is strongly recommended to use UnityWebRequest for projects using Unity 5.3 or newer, and WWW.LoadFromCacheOrDownload for projects using Unity 5.2 or older. As detailed in the Distribution section of the next chapter, it is possible to prime the AssetBundle Cache with Bundles included within a project's installer.

When using either UnityWebRequest *or *WWW.LoadFromCacheOrDownload, ensure that the downloader code properly calls Dispose after loading the AssetBundle. Alternately, C#'s using statement is the most convenient way to ensure that a WWW or UnityWebRequest is safely disposed.

For projects with substantial engineering teams that require unique, specific caching or downloading requirements, a custom downloader may be considered. Writing a custom downloader is a non-trivial engineering task, and any custom downloader should be made compatible with AssetBundle.LoadFromFile. See the Distribution section of the next chapter for more details.

3.4. Loading Assets From AssetBundles

UnityEngine.Objects can be loaded from AssetBundles using three distinct APIs that are all attached to the AssetBundle object, which have both synchronous and asynchronous variants:

The synchronous versions of these APIs will always be faster than their asynchronous counterpart, by at least one frame.

Asynchronous loads will load multiple Objects per frame, up to their time-slice limits. See the Low-level loading details section for the underlying technical reasons for this behavior.

LoadAllAssets should be used when loading multiple independent UnityEngine.Objects. It should only be used when the majority or all of the Objects within an AssetBundle need to be loaded. Compared to the other two APIs, LoadAllAssets is slightly faster than multiple individual calls to LoadAssets. Therefore, if the number of assets to be loaded is large, but less than 66% of the AssetBundle needs to be loaded at a single time, consider splitting the AssetBundle into multiple smaller bundles and using LoadAllAssets.

LoadAssetWithSubAssets should be used when loading a composite Asset which contains multiple embedded Objects, such as an FBX model with embedded animations or a sprite atlas with multiple sprites embedded inside it. If the Objects that need to be loaded all come from the same Asset, but are stored in an AssetBundle with many other unrelated Objects, then use this API.

For any other case, use LoadAsset or LoadAssetAsync.

3.4.1. Low-level loading details

UnityEngine.Object loading is performed off the main thread: an Object's data is read from storage on a worker thread. Anything which does not touch thread-sensitive parts of the Unity system (scripting, graphics) will be converted on the worker thread. For example, VBOs will be created from meshes, textures will be decompressed, etc.

From Unity 5.3 onward, Object loading has been parallelized. Multiple Objects are deserialized, processed and integrated on worker threads. When an Object finishes loading, its Awake callback will be invoked and the Object will become available to the rest of the Unity Engine during the next frame.

The synchronous AssetBundle.Load methods will pause the main thread until Object loading is complete. They will also time-slice Object loading so that Object integration does not occupy more than a certain number of milliseconds of frame time. The number of milliseconds is set by the property Application.backgroundLoadingPriority:

ThreadPriority.High: Maximum 50 milliseconds per frame

ThreadPriority.Normal: Maximum 10 milliseconds per frame

ThreadPriority.BelowNormal: Maximum 4 milliseconds per frame

ThreadPriority.Low: Maximum 2 milliseconds per frame.

From Unity 5.2 onwards, multiple Objects are loaded until the frame-time limit for Object loading is reached. Assuming all other factors equal, the asynchronous variants of the asset loading APIs will always take longer to complete than the comparable synchronous version due to the minimum one-frame delay between issuing the asynchronous call and the object becoming available to the Engine.

3.4.2. AssetBundle dependencies

The dependencies among AssetBundles are automatically tracked using two different APIs, depending on the runtime environment. In the Unity Editor, AssetBundle dependencies can be queried via the AssetDatabase API. AssetBundle assignments and dependencies can be accessed and changed via the AssetImporter API. At runtime, Unity provides an optional API to load the dependency information generated during an AssetBundle build via a ScriptableObject-based AssetBundleManifest API.

An AssetBundle is dependent upon another AssetBundle when one or more of the parent AssetBundle's UnityEngine.Objects refers to one or more of the other AssetBundle's UnityEngine.Objects. For more information on inter-Object references, see the Inter-Object references section of the Assets, Objects and Serialization article.

As described in the Serialization and instances section of that article, AssetBundles serve as sources for the source data identified by the FileGUID & LocalID of each Object contained within the AssetBundle.

Because an Object is loaded when its Instance ID is first dereferenced, and because an Object is assigned a valid Instance ID when its AssetBundle is loaded, the order in which AssetBundles are loaded is not important. Instead, it is important to load all AssetBundles that contain dependencies of an Object before loading the Object itself. Unity will not attempt to automatically load any child AssetBundles when a parent AssetBundle is loaded.

Example:

Assume material A refers to texture B. Material A is packaged into AssetBundle 1, and texture B is packaged into AssetBundle 2.

In this use case, AssetBundle 2 must be loaded prior to loading Material A out of AssetBundle 1.

This does not imply that AssetBundle 2 must be loaded before AssetBundle 1, or that Texture B must be loaded explicitly from AssetBundle 2. It is sufficient to have AssetBundle 2 loaded prior to loading Material A out of AssetBundle 1.

However, Unity will not automatically load AssetBundle 2 when AssetBundle 1 is loaded. This must be done manually in script code.

For more information on AssetBundle dependencies, refer to the manual page.

3.4.3. AssetBundle manifests

When executing the AssetBundle build pipeline using the BuildPipeline.BuildAssetBundles API, Unity serializes an Object containing each AssetBundle's dependency information. This data is stored in a separate AssetBundle, which contains a single Object of the AssetBundleManifest type.

This Asset will be stored in an AssetBundle with the same name as the parent directory where the AssetBundles are being built. If a project builds its AssetBundles to a folder at (projectroot)/build/Client/, then the AssetBundle containing the manifest will be saved as (projectroot)/build/Client/Client.manifest.

The AssetBundle containing the manifest can be loaded, cached and unloaded just like any other AssetBundle.

The AssetBundleManifest Object itself provides the GetAllAssetBundles API to list all AssetBundles built concurrently with the manifest and two methods to query the dependencies of a specific AssetBundle:

AssetBundleManifest.GetAllDependencies returns all of an AssetBundle's hierarchical dependencies, which includes the dependencies of the AssetBundle's direct children, its children's children, etc.

Note that both of these APIs allocate string arrays. Accordingly, they should only be used sparingly, and not during performance-sensitive portions of an application's lifetime.

3.4.4. Recommendations

In many cases, it is preferable to load as many needed Objects as possible before players enter performance-critical areas of an application, such as the main game level or world. This is particularly critical on mobile platforms, where access to local storage is slow and the memory churn of loading and unloading Objects at play-time can trigger the garbage collector.