Unity WebGL asset bundle memory is not releasing

2019-06-17 01:15发布

问题:

I am loading and Caching Asset Bundles using below function in unity webgl:

 IEnumerator DownloadAndCacheAB(string assetName)
    {
        // Wait for the Caching system to be ready
        while (!Caching.ready)
            yield return null;

        // Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache
        using (WWW www = WWW.LoadFromCacheOrDownload(finalUrl, 1))
        {

            yield return www;

            if (www.error != null)
            {
                Debug.Log("WWW download had an error:" + www.error);
            }

            AssetBundle bundle = www.assetBundle;

            if (assetName == "")
            {
                GameObject abObject = (GameObject)Instantiate(bundle.mainAsset, gameObject.transform.position, gameObject.transform.rotation);
                abObject.transform.parent = this.transform;
                SetTreeShaderSettings(abObject);
            }
            else
            {
                GameObject abObject = (GameObject)Instantiate(bundle.LoadAsset(assetName), gameObject.transform.position, gameObject.transform.rotation);
                abObject.transform.parent = this.transform;

            }

            // Unload the AssetBundles compressed contents to conserve memory
            bundle.Unload(false);

        } // memory is freed from the web stream (www.Dispose() gets called implicitly)
    }

And whenever I want to remove the object i use this function.

 public void RemoveBundleObject()
    {
        //if (www != null)
        //{
        //    www.Dispose();
        //    www = null;
        if (loadBundleRef != null)
        {
            StopCoroutine(loadBundleRef);
        }
        if (this.gameObject.transform.childCount > 0)
        {
            Destroy(this.gameObject.transform.GetChild(0).gameObject);
            System.GC.Collect();
        }
        //}
    }

As you can see I am deleting the first child (which I got from asset bundle) then call GC Collect to force garbage collector. but the problem is that my memory is not releasing whenever i unload/destroy the object. Now you will thinking that how i am measuring the memory? I am using WebGLMemoryStats from here. And i am getting this after several iteration of assetbundle load.

Edit: I am using now WebRequest Class to download assetbundle (find here) but still unable to release the memory and sometime get aw snap error.

Even when I am trying to load an empty webgl build again and again its increase the memory heap and then sometime i get aw snap or memory error and chrome crash.

As you can see the initial load is about 1.2 Mb but when i refresh page and take snap shot, it snapshot brings with incremented memory.

回答1:

As a note, you'll never get this to function as perfectly as you may think. There are utilities for cleaning up garbage and unloading assets but even if you follow all best practices, you still might not get Unity to clean up all the garbage (it does a lot in the background that'll retain allocated memory and there's not much you can do about it). If you're curious about why this is, I'd suggest their writeup on Heap Fragmentation as well as their Memory Optimization Guide.


As far as what you're doing with your specific project, there are at least some improvements you can make:

1) Avoid using the WWW class at all costs. It creates a lot more garbage than it's worth, doesn't always clean up well, and is obsolete in newest versions of Unity. Use UnityWebRequest to download bundles instead.

2) Don't get in the habit of loading a bundle just to load a single asset and then unloading that bundle. If you have lots of loads occurring at runtime, this will cause thrashing and is a fairly inefficient way of managing your bundles. I noticed you're calling bundle.Unload(false) which means the bundle is being unloaded but the loaded prefab asset isn't. I'd suggest restructuring this in a way that you can:

  1. load the bundle you need
  2. load the assets you need from that bundle
  3. wait for the lifetime of all those assets to end
  4. call bundle.Unload(true)

3) Be careful with your call to StopCoroutine(loadBundleRef); (if loadBundleRef is a Coroutine object that is running your web request and bundle loading logic). Interrupting these async operations could lead to memory issues. You should have something in place that ensures web requests and bundle loads either finish completely or, on failure, throw and let your game recover. Don't allow something like StopCoroutine to interrupt them.

4) System.GC.Collect(); is slow and garbage collection happens periodically anyway. Use it sparingly and you might also want to call Resources.UnloadUnusedAssets before calling it.



回答2:

Please consider the @Foggzie Answer too for some best practices (I used some of them)!

There was two issue with my code or testing procedeure.

1. Don't Use WWW.LoadFromCacheOrDownload

Unity Built-in WWW.LoadFromCacheOrDownload is crap! Seriously! Why ? Ben Vinson and unity also explain in their blogs:

Another source of memory-related problems is the IndexedDB filesystem used by Unity. Any time you cache an asset bundle or use any filesystem-related methods, they are stored in a virtual filesystem backed by IndexedDB.

What you might not realize is that this virtual filesystem is loaded into and persisted in memory as soon as your Unity application starts. This means that if you are using the default Unity caching mechanism for Asset Bundles, you are adding the size of all of those bundles to the memory requirements for your game, even if they are not being loaded.

And Unity Clearly mentioned in his blog to use UnityWebRequest instead LoadFromCacheOrDownload:

A longer term solution to minimize asset bundle caching memory overhead is to use WWW Constructor instead of LoadFromCacheOrDownload() or use UnityWebRequest.GetAssetBundle() with no hash/version parameter if you are using the new UnityWebRequest API.

Then use an alternative caching mechanism at the XMLHttpRequest-level, that stores the downloaded file directly into indexedDB, bypassing the memory file system. This is exactly what we have developed recently and it is available on the asset store. Feel free to use it in your projects and customize it if you need to.

The memory increment on page reload problem seems related to FireFox internal mechanism. But It was occurring in chrome due to the dev tools which were open when I was checking the memory.

2. Don't Measure Memory when Chrome Dev Tools are Open

Once I was measuring my tab memory in chrome the dev tools was open so it increment the memory with each page refresh. This reason defined by one of the Unity official at forum

One point to note is that when profiling memory usage on page reloads in Firefox, make sure to have the Firefox web console (and debugger) window closed. Firefox has a behavior that if web console is open, it keeps the Firefox JS debugger alive, which pins all visited pages to be cached in memory, never reclaiming them. Closing the Firefox web page console allows freeing the old pages from memory.

I do not know if Chrome might have similar behavior, but for good measure, it is good to have its web console closed to ensure it is not keeping pages alive.

If someone has a public test case available that they can post, that would be helpful in pinpointing the source.

But actually my test suggest that this problem still persist in fireFox while resolve for Google Chrome.

Remember, I got this answer after a long struggle.