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

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