Unity AssetBundle加载和内存管理

AssetBundle资源加载

一个AssetBundle中包含两部分:数据头和数据段。

通过AssetBundle加载游戏资源,分为三步:

  1. 加载AssetBundle对象。

  2. 通过AssetBundle对象加载需要的资源。

  3. 对于非引用类型的资源,还需通过GameObject.Instantiate()创建clone。

1. 加载AssetBundle对象方式

  • AssetBundle.LoadFromMemory(可选择异步模式)
  • AssetBundle.LoadFromFile(可选择异步模式)
  • AssetBundle.LoadFromStream(可选择异步模式)
  • UnityWebRequest的DownloadHandlerAssetBundle (异步模式)
  • WWW.LoadFromCacheOrDownload (Unity 5.6以及更低版本)(异步模式) 
  • WWW(异步模式)

1.1 WWW.LoadFromCacheOrDownload(异步-缓存机制),2017以后被废弃。

Unity5.2之前推荐使用 WWW.LoadFromCacheOrDownload(),从2017开始被整合到UnityWebRequest中。以下适用于5.6以前版本。(异步加载,每次打包都需要更新资源版本,保存的路径无法更改,也没有接口去获取此路径)

  • 可以从远程服务器加载Object,也可以从本地存储加载Object。可以通过 file:// URL从本地存储加载文件。
  • 每次下载时候,都会创建一个线程,每个线程会在托管堆中缓存一份解压后的AssetBundle,这可能导致内存飙升。
  • 如果本地没有缓存,使用WWW网络下载文件。从网络下载的AssetBundle如果没有压缩,通过固定大小的缓冲区直接并写入本地缓存,如果有压缩还需要在固定大小的缓冲区中解压缩后,才能写入本地缓存。但是WWW对象将在托管堆中保留解压缩后的AssetBundle字节的完整副本。保留AssetBundle的额外副本以支持www.bytes等属性。一旦解压缩后的AssetBundle被缓存到本地,LoadFromCacheOrDownload将从本地缓存的解压缩AssetBundle加载数据头。
  • 如果本地已有缓存,调用LoadFromCacheOrDownload,和调用LoadFromFile一样,由于本地缓存中是未压缩的AssetBundle,因此只会加载AssetBundle数据头。
public class LoadFromCacheOrDownloadExample : MonoBehaviour
{
    IEnumerator Start()
    {
        while (!Caching.ready)
            yield return null;

        using (var www =             
        WWW.LoadFromCacheOrDownload("http://myserver.com/myassetBundle.unity3d", 5))
        {
            yield return www;
            if (!string.IsNullOrEmpty(www.error))
            {
                Debug.Log(www.error);
                yield return null;
            }
            var myLoadedAssetBundle = www.assetBundle;

            var asset = myLoadedAssetBundle.mainAsset;
        }
    }
}


1.2 WWW (异步-无缓存机制)

WWW是一个Unity封装的网络下载模块,支持Http以及file两种URL协议。

直接使用LoadFromCacheOrDownload和WWW不同的是,读取不会再走WWW这一套 WWW进行读取或者下载时候,都会创建一个线程,托管堆中缓存一份下载后的AssetBundle,同时开启多个下载,这可能导致内存飙升。使用WWW.Dispose将终止仍在加载过程中的进程,并释放掉托管堆中的AssetBundle。 

通过www下载资源,然后释放托管堆内存。代码如下:

/// <summary>
///从服务器下载到本地
/// </summary>
/// <param name="AssetsHost">服务器路径</param>
/// <param name="AssetName">请求资源名称</param>
/// <param name="saveLocalPath">保存到本地路径,一般存在Application.persistentDataPath</param>
/// <returns></returns>
IEnumerator DownLoadAssetsWithDependencies2Local(string AssetsHost, string AssetName, string saveLocalPath)
{
    WWW tempWwwAsset = new WWW(AssetsHost + "/" + AssetName);
    //获取加载进度
    yield return tempWwwAsset;

    //直接读取AssetBundle
    AssetBundle temp = tempWwwAsset.assetBundle;

    //保存到本地
    Stream tempStream = null;
    FileInfo fileInfo = new FileInfo(saveLocalPath + "/" + AssetName);
    if (fileInfo.Exists)
    {
        fileInfo.Delete();
    }
    //如果此文件不存在则创建
    tempStream = fileInfo.Create();
    //写入
    tempStream.Write(tempWwwAsset.bytes, 0, tempWwwAsset.bytes.Length);
    tempStream.Flush();
    //关闭流
    tempStream.Close();
    //销毁流
    tempStream.Dispose();
    Debug.Log(name + "成功保存到本地~");
}

  1. 如图所示,WWW对象和通过WWW.assetBundle属性所加载的AssetBundle对象会对Web Stream数据持有引用,同时AssetBundle也会引用到从它所加载的所有Assets。值得一提的是,真正的数据都是存放在Web Stream数据中,如纹理、模型等,而WWW和AssetBundle对象只是一个结构指向了Web Stream数据。
  2. 对于WWW对象本身,可以使用www = null或WWW.Dispose()来进行卸载。二者的效果一样,只是www = null不会及时引起内存释放操作,而是在系统自动垃圾回收时进行释放,WWW对象释放后,其对于Web Stream数据的引用计数也会相应减1。
  3. 对于Web Stream数据,其所占用的内存会在其引用计数为0时,自动被系统进行释放。如上图中,当AssetBundle对象以及WWW对象被释放后,Web Stream 数据所占用的内存也会被系统自动回收。

通过下载存储在内存中的WebStream,可以直接转化为Unity支持的资源格式:

Texture2D texture = WWW.texture;

string text = WWW.text;

byte[] bytes = WWW.bytes;

MovieTexture movie = WWW.movie; 

AssetBundle assetbundle = WWW.assetBundle; 

AudioClip audioClip = WWW.audioClip; 

1.3 LoadFromMemory(同步) LoadFromMemoryAsync(异步) , 一般只会拿来读取加密的AssetBundle

IEnumerator LoadAB(string _filePath)
{
    byte[] tempByteAry = File.ReadAllBytes(_filePath);
  AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(tempByteAry);
  yield return request;
  AssetBundle ab = request.assetBundle;
}
  1. 从托管堆中C#中的byte[])中加载AssetBundle。该方法总是将托管堆中的源数据复制到新分配的连续的非托管堆内存区块中。如果AssetBundle使用了LZMA压缩,在复制过程中AssetBundle会被解压;如果使用了LZ4压缩或者没有压缩,AssetBundle会被原样复制。
  2. 这个API占用内存的峰值至少是它所处理的AssetBundle大小的两倍:一份由此API创建在本机内存中的副本和一份传递给此API的位于托管堆字节数组中的副本。通过此API从AssetBundle加载的Asset会在内存中被复制3次:在托管堆的字节数组、本机内存中的副本以及在GPU或系统内存中的Asset本体。

1.4 LoadFromStream(本地-同步)  LoadFromStreamAsync(本地-异步)

        var fileStream = new FileStream(Application.streamingAssetsPath, FileMode.Open, 
        FileAccess.Read);

        var myLoadedAssetBundle = AssetBundle.LoadFromStream(fileStream);
        if (myLoadedAssetBundle == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }

        var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject");
        Instantiate(prefab);

        myLoadedAssetBundle.Unload(false);
  1. 可以通过文件流一点点读取,它会分次从文件里读取32KB放在非托管内存中,只到将文件全部放去非托管内存中,也就是说堆内存中它只会占用32KB。托管堆每次会读32KB放入非托管内存,只到全部读取完毕。
  2. 默认值32KB可以修改,比如修改为64KB。AssetBundle.LoadFromStream(fileStream,0,64)

1.5 UnityWebRequest(异步-提倡)

        var www = UnityWebRequest.Get("file://" +Application.streamingAssetsPath +                 
        "/10MB.unity3d");

        string downloadPath = Application.persistentDataPath + "/11MB.unity3d";
        www.downloadHandler = new DownloadHandlerFile(downloadPath);
        yield return www.SendWebRequest();

        Debug.Log("下载完成,加载assetbundle");
        var assetbundle = AssetBundle.LoadFromFile(downloadPath);
  1. UnityWebRequest绑定一个DownloadHandlerAssetBundle对象,DownloadHandlerAssetBundle可以使用一个线程(Worker Thread)通过固定大小的缓冲区( 8x64KB)把服务器下载数据存储到临时存储或者AssetBundle缓存中,具体的存储位置取决于DownloadHandler的配置。DownloadHandler不会保留所有下载的字节数据,进一步降低了下载AssetBundle的内存开销。
  2. 使用LZMA压缩的AssetBundle在下载时会被解压并使用LZ4压缩进行缓存。这一行为可以通过设置Caching.CompressionEnabled来修改。
  3. 下载完成后,可以通过DownloadHandler的assetBundle属性访问下载的AssetBundle,如同对下载的AssetBundle调用 AssetBundle.LoadFromFile 方法。
  4. 在Unity 5.6之前,UnityWebRequest系统使用一个固定的工作线程池和内部任务系统来防止重复、并发下载,线程池的大小不可配置。在Unity 5.6中,该保护措施被移除了,目的是适应现代硬件以及更快的访问HTTP响应代码和数据头。
  5. 下载时候固定大小的缓冲区( 8x64KB)托管堆内存,在非托管中生成未压缩或者LZ4格式的AssetBundle文件。(LZMA下载过程中会将LZMA解压后,重压缩为LZ4。)

allowSceneActivation = false,加载progress一直为0。原因(官方解释

1.6 LoadFromFile(本地-同步),LoadFromFileAsync(本地-异步)加载

// ---本地同步---
AssetBundle tempLogo = AssetBundle.LoadFromFile(AssetBundlesOutputPath + "/logo.unity3d");

// ---本地异步---
IEnumerator LoadAsyncCoroutine(string path, Action<AssetBundle> callback) {
    AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(path);
    yield return abcr;        
    callback(abcr.assetBundle);
}
  1. AssetBundle.LoadFromFile是用于从本地存储加载未压缩或使用LZ4压缩的AssetBundle的高效API。LZMA格式的将会先被解压,然后再压缩成LZ4格式,放到非托管堆。
  2. 在桌面系统(PC、Mac、Linux)、主机和移动平台上,这个API只会加载数据头,把其余的数据留在磁盘上。AssetBundle中的Object会在调用了加载方法(例如AssetBundle.Load)或者在它们的InstanceID被引用时按需加载。在这种情况下,不需要消耗额外的内存。在Unity编辑器中,这个API会将整个AssetBundle加载到内存中,就像这些字节被从磁盘中读出而且使用了 AssetBundle.LoadFromMemoryAsync 方法。如果在Unity编辑器中对项目进行分析,会发现在使用此方法加载AssetBundle时会出现内存使用高峰,在发布版本中这应该不会对设备性能造成影响。
  3. 提示:在Unity 5.3以及更早版本地Android工程中,使用此API从StreamAssets中加载AssetBundle会失败,这个问题已经在Unity 5.4中修复。更多详细内容,请查看本系列文章地第四篇中的分发-附加在项目中一节。

不同API加载方式,内存占用和效率对比:

加载方式

无压缩

块压缩(LZ4)

流压缩(LZMA)

WWW *

内存:未压缩的包的大小-非托管堆+(当WWW对象未释放时,未压缩的包的大小-托管堆内存)。

 

内存:LZ4压缩的包的大小-非托管堆+(当WWW对象未释放时,LZ4压缩的包的大小-托管堆内存)。

 

内存:LZ4压缩的包的大小-非托管堆+(当WWW对象未释放时,LZMA压缩的包的大小-托管堆内存)。

 

性能:没有额外的开销。 性能:没有额外的开销。 性能:LZMA解压+ 在下载过程中LZ4压缩。

LoadFromCacheOrDownload

(此行为本地加载时候,已缓存的都已解压缩,加载AssetBundle和LoadFromFile一样,只加载数据头)

内存:数据头。

 

内存:数据头。

 

内存:数据头。

 

性能:从磁盘读取。 性能:从磁盘读取。 性能:从磁盘读取。

LoadFromMemory(同步,异步)

内存:未压缩的包的大小-托管堆。+ (未压缩的包的大小-非托管堆内存)

内存:LZ4压缩的包的大小-托管堆。+ (LZ4未压缩的包的大小-非托管堆内存)

内存:LZ4压缩的包的大小-托管堆。

+ (LZ4未压缩的包的大小-非托管堆内存)

性能:没有额外的开销。 性能:没有额外的开销。 性能:LZMA解压+ LZ4压缩。

LoadFromFile(同步,异步)

内存:没有额外的开销。

 

内存:没有额外的开销。

 

内存:LZ4压缩的包的大小。

 

性能:从磁盘读取。 性能:从磁盘读取。 性能:从磁盘读取+ LZMA解压+ LZ4压缩。

WebRequest的(读取本地缓存时候和LoadFromFile一样)

内存:未压缩的包的大小-非托管堆。 

 

内存:LZ4HC压缩的包的大小-非托管堆。

内存:LZ4压缩的包的大小-非托管堆。

 

性能:没有额外开销[+是否从磁盘读取缓存]。 性能:没有额外的开销[+是否从磁盘读取缓存]。 性能:下载过程中LZMA压缩+ LZ4压缩[+是否从磁盘读取缓存]。

2. AssetBundle加载后,从已加载的AssetBundle中加载资源

AssetBundle tempAssetBundle = 通过上边各种方式加载的AssetBundle;
string[] tempAssetNames = tempAssetBundle.GetAllAssetNames();//此AssetBundle中包含的所有资源名称
tempAssetBundle.LoadAsset("");//通过资源名称从AssetBundle加载指定的资源
tempAssetBundle.LoadAllAssets();//加载当前资源包中所有的资源
tempAssetBundle.LoadAssetAsync("");//从资源包中异步加载资源

加载AssetBundle时候,此AssetBundle依赖其他AssetBundle,需要先加载其他AssetBundle。

    [MenuItem("AssetsBundle/Load")]
    static void LoadAssetbundle()
    {
        LoadAssetBundleManifest();
        LoadBundle();
	}

    //本地AssetBundle存储路劲 自己存哪读哪
    private static string m_rootFilePath = Application.dataPath + "/StreamingAssets/";
    private static AssetBundleManifest manifest = null;

    // 加载Manifest
    private static void LoadAssetBundleManifest()
    {
        var bundle = AssetBundle.LoadFromFile(System.IO.Path.Combine(m_rootFilePath,"StreamingAssets"));
        manifest = bundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        // 压缩包释放掉
        bundle.Unload(false);
        bundle = null;
    }

    //加载AssetBundle前,先加载AssetBundle依赖的AssetBundle
    private static void LoadBundle()
    {
        string bundleName = "logo";
        // GetAllDependencies会返回直接和间接关联的AssetBundle 当前AssetBundle所依赖的所有AssetBundle
        // 加载依赖包没有顺序要求,
        string[] dependence = manifest.GetAllDependencies(bundleName);
        for (int i = 0; i < dependence.Length; ++i )
        {
            //这地方同步加载,异步也可以(AssetBundle.LoadFromFileAsync)
            AssetBundle.LoadFromFile(System.IO.Path.Combine(m_rootFilePath, dependence[i]));
        }
        //加载完所有依赖的AssetBundle,加载本身
        var bundle = AssetBundle.LoadFromFile(System.IO.Path.Combine(m_rootFilePath, bundleName));
        //这里不需要手动LoadAsset 依赖的AssetBundle中资源
        //LoadAsset(logo) 资源的时候自动加载与它关联的Asset
        GameObject tempGameObject = bundle.LoadAsset<GameObject>("logo");
        GameObject tempGameObject1 = Instantiate(tempGameObject) as GameObject;
    }

        //这里不需要手动LoadAsset 依赖的AssetBundle中资源
        //LoadAsset(logo) 资源的时候自动加载与它关联的Asset

   AssetBundle内存卸载:

        var tempAb = AssetBundle.LoadFromFile(Application.streamingAssetsPath +     
        "/assetbundle.unity3d");
        var tempPrefab = tempAb.LoadAsset<GameObject>("GameObject");
        var tempGo = Instantiate<GameObject>(prefab);

        //tempAb.Unload(false);
        tempAb.Unload(true);

tempPrefab,tempGo,tempGo .Net 分配的内存,他们都指向非托管堆中的相应变量,如下图所示:

 

内存卸载:

方法 卸载内存
tempAb.Unload(false) 卸载assetbundle.unity3d
tempAb.Unload(true)

卸载assetbundle.unity3d 以及 prefab-mesh等

Resources.UnloadAsset(obj) 传入参数为mesh,material,shader等对象,卸载对应的非托管堆资源对应的内存
Resources.UnloadUnusedAssets() 会先调用一次GC,非托管堆没有托管堆引用变量引用的资源将被释放

tempAb,tempPrefab,tempGo 此处为临时变量,下次GC回收内存。

AssetBundle中可以实例化的类型:

Texture

设置read/write enabled后可以

AudioClip

不可以

Scene

可以

TextAsset

可以,但一般没有必要

Material

可以

Shader

可以

Mesh

可以

Animator

可以

Animation

可以

TerrainData

可以

GameObject

可以,Prefab就是GameObject,一般都是实例化prefab,实例化其他的没有必要。

 


参考:

https://blog.csdn.net/swj524152416/article/details/54022282 

https://docs.unity3d.com/Manual/UnityWebRequest.html

https://blog.csdn.net/itsxwz/article/details/83378045

https://learn.unity.com/tutorial/assets-resources-and-assetbundles#5c7f8528edbc2a002053b5a8

http://stalhandske.dk/UnityDocs/Manual/AssetBundleCompression.html

https://blog.uwa4d.com/archives/ABTheory.html

https://blog.csdn.net/lodypig/article/details/51879702

https://www.cnblogs.com/wgslucky/p/11523575.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章