Learn to save memory usage by improving the way you use AssetBundles
Whether your application streams assets from a content delivery network (CDN) or packs them all into one big binary, you’ve probably heard of AssetBundles. An AssetBundle is a file that contains one or more serialized assets (Textures, Meshes, AudioClips, Shaders, etc.) and is loadable at runtime.
AssetBundles can be used directly or through systems like the Unity Addressable Asset System (aka Addressables). The Addressables system is a package that provides a more accessible and supported way to manage Assets within your projects. It is an abstraction on top of AssetBundles. While Addressables minimizes the direct interactions developers have with AssetBundles, it is helpful to understand how the usage of AssetBundles can affect memory usage. For an overview of the Addressables system, please refer to this blog post and this session from Unite Copenhagen 2019.
Developers working on new projects should consider using Addressables rather than working with AssetBundles directly. If you are working on a project with an already established AssetBundles approach, the information here about how AssetBundles affect runtime memory will help you get the best possible results.
Losing memory to the memory cache
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.
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.
If you provide a version or a hash argument to the UnityWebRequestAssetBundle API, Unity stores your AssetBundle data into the disk cache. If you do not provide these arguments, Unity uses the memory cache. Note that Addressables uses the disk cache by default. This behavior can be controlled via the UseAssetBundleCache field.
AssetBundle.LoadFromFile() and AssetBundle.LoadFromFileAsync() always use the memory cache for LZMA AssetBundles. We therefore recommend using the UnityWebRequestAssetBundle API instead. If it is not feasible to use the UnityWebRequestAssetBundle API, you may use AssetBundle.RecompressAssetBundleAsync() to rewrite an LZMA AssetBundle on disk.
Internal testing shows that there is at least an order of magnitude difference in RAM between using the disk cache and using the memory cache. You must weigh the tradeoff in memory cost versus added disk space requirements and Asset instantiation time for your application.
To determine what effect the AssetBundle memory cache may have on your application’s memory usage, use a native profiler (our tool of choice is Xcode’s Allocations Instrument) to examine allocations from the ArchiveStorageConverter class. If this class is using more than 10MB of RAM, you’re probably using the memory cache.
Find out what your AssetBundles are actually loading
When building AssetBundles for large projects, do not assume that Unity by default will minimize the amount of duplicated information across them. To identify instances of duplicated data in the generated AssetBundles, you can use the handy AssetBundle Analyzer, written in Python by one of our colleagues in the Consulting & Development group. Used via the command line, the tool extracts information from generated AssetBundles, which is then stored in an SQLite database that features several useful views. You can then query the database using tools such as DB Browser for SQLite. This tool can help you find and resolve any inefficiencies in your build pipeline, whether you created bundles manually or via Addressables.
Alternatively, check out the AssetBundle Browser tool, which you can download and integrate into your project straight away. Note that this tool provides similar functionality to Addressables, so if you are using Addressables, this tool is not relevant.
The AssetBundle Browser tool allows you to view and edit the configuration of AssetBundles in a given Unity project and provides build functionality. It also provides some neat features, such as informing users about duplicated Assets that are being pulled due to dependencies, such as textures.
Losing memory to duplicate Assets
When deciding how to organize your Assets into AssetBundles, you need to be careful about dependencies. Regardless of your AssetBundle topology, Unity makes a distinction between Assets that live inside the application binary (in or involving a Resources folder) and those that you need to load from AssetBundles. You can think of these two types of Assets as living in different worlds. It is impossible to create an AssetBundle that has a hard reference to the instance of an Asset inside the Resources folder world. To reference those Assets, Unity instead makes a copy of the Assets that it uses in the AssetBundle world.
Take for example a game’s logo. The logo may be displayed in the UI of a loading scene while the game starts up. Because this loading screen must be shown before remote Assets are streamed to disk, you might include the logo Asset in the build so that it can be used immediately.
This same logo is also used on an options panel in the UI, where users can select their language, sound preferences, and other settings. If this UI panel is loaded from an AssetBundle, then that AssetBundle will create its own copy of the logo Asset.
If both the loading screen and the options panel are loaded at the same time, both copies of the logo Asset will be loaded, which is a duplication that costs memory.
The solution to this problem is to break the hard link between one or both screens. If the logo lives in an AssetBundle, then some amount of streaming needs to occur before you can get a reference to the Asset. If the logo lives in the binary (inside a Resources folder, for example), then the UI panel will need to have a weak reference to the logo Asset and be loaded via an API such as Resources.Load.
User scripting will need to use the string to load the image at runtime and assign it to the proper component.A happy middle ground may be to include the AssetBundle containing the logo Asset inside the application’s StreamingAssets directory. You will still load the Asset from the AssetBundle, but since you are hosting the bundle locally, you will not pay the cost in time required to download the content.
It is not intuitive to use strings, paths or GUIDs to reference Assets, but you may want to create custom inspectors that enable Unity’s default drag-and-drop reference functionality on your weakly referenced fields. And don’t forget to use Unity’s MemoryProfiler package to identify Assets that are duplicated in Memory. Note that the Addressables system has its own mechanism for checking for duplicates in dependencies (for more information, see the documentation).
Even though the Addressables system provides an abstraction on top of AssetBundles, knowing how things work under the hood can help you avoid costly performance problems like the ones described in this article.
We’re planning a roadmap for future entries of this series. Is there any area you’d like us to focus on? Leave a comment to let us know!