【Unity】Asset资源加载详解

加载类型:

加载方式 加载接口 加载后缀 异步 热更 卸载某个
Resource Load ×
AssetDatabase LoadAssetAtPath 文件后缀 × × ×
AssetBundle GetDependent→LoadDpAB→LoadAB→LoadAsset

加载资源,指从磁盘加载到内存中,反序列化然后实例化Instance才会真正到Scene中。

资源加载接口

  1. Resource.Load
     

    Resource.Load是Unity加载Resources文件夹的加载方式,Resources文件夹会随着打包一起被打到游戏包内。
    正式项目切勿使用此接口,无法热更,没有找到分包的方法,每次发包都要重新打。

    一般来说最先接触到的资源加载接口是Res.Load,对于项目前期或者小项目会比较方便,因为不需要对打包资源做任何操作,放在resources下的资源就能随着build一起打了,但对于大项目需要热更项目来说,基本不会使用了,因为不能分包也无法热更(也可能是我没找到办法),上线就肯定用AssetBundle



     
  2. UnityEditor.AssetDatabase.LoadAssetAtPath
     

     Editor下的加载方式,加载路径是项目下的路径,除了Resources文件夹都不会随着打包打到游戏本体内。
    注意路径是带Assets开头,并且需要后缀

    AssetDataBase不支持异步,所以editor下的效果和打包效果不一样,而且这样的加载还需要后缀,或者用ab名去索引,否则导致会多些一些代码,不过问题不大,习惯就好。
    如果DestroyImmediately(true)后除非重启unity,再也加载不出来

  3. AssetBundle.Load

     加载Asset Bundle可以用于热更资源。

    public class SampleBehaviour : MonoBehaviour
    {
        IEnumerator Start()
        {
            var uwr = UnityWebRequestAssetBundle.GetAssetBundle("http://myserver/myBundle.unity3d");
            yield return uwr.SendWebRequest();
    
            // Get an asset from the bundle and instantiate it.
            AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(uwr);
            var loadAsset = bundle.LoadAssetAsync("Assets/Players/MainPlayer.prefab");
            yield return loadAsset;
    
            Instantiate(loadAsset.asset);
        }
    }

    AssetBundle(简称AB)这是一个分包和热更的方案,调用打包接口之后就能产生出一堆的AB包,这样做使得增量打包更快(不需要每次把所有资源重新打),热更时更新的内容也相对的小。
    Asset才是真正的资源例如贴图、网格、动画那些都是Asset,AB可以理解成Asset的包,AB只会记录里面有那些Asset,依赖了其他的哪些AB,是一个非常小的东西
    一般来说同一类的资源会打成一个AB,例如角色Prefab会依赖贴图网格动画等..
    AB可能又会依赖多个其他AB,所以加载经常会需要先加载依赖,否则加载出来的东西也还是空的,依赖使用Manifest获取
    Manifest是会随着打包产生出来,最好能在初始化的时候把Manifest加载了,然后存起来使用。
    但AB缺点是editor下使用比较麻烦,总不能每次改了资源都重新打AB,所以editor可以用AssetDataBase

认识Asset

meta

guid就唯一标识这个资源
同时保存了一些设置 和 ABName..

Library

Unity会把Asset下支持的资源导入成自身识别的格式,以及编译代码成为DLL文件,都放在Library文件夹中。
如果打包编译的shader也会存在这
如果打图集也会存在这
大项目如果删除这个文件夹会导致导入很久很久...
在AssetDataBaseV2之前,如果切平台也会重导很久...

Asset

用到的资源,比如,模型文件,贴图文件,声音文件等等
注:这才是真正占用内存的大头,一般Prefab的目标Asset也很小,大的是依赖,依赖被目标Asset引用,只有卸载了Asset再调用UnloadUnuse才真正的释放了内存
hashCode为正

GameObject

hashCode为负数

AssetBundle

AssetBundle是一个存档文件,其中包含平台在运行时加载的特定资产(模型,纹理,预制,音频剪辑,甚至整个场景)。
AssetBundles可以表示彼此之间的依赖关系;例如AssetBundle A中的一个材质可以引用AssetBundle B中的一个纹理。
为了通过网络进行有效的传递,可以根据用例要求,选择内置算法(LZMA和LZ4)来对AssetBundles进行压缩。
AssetBundles可用于可下载内容(DLC),减少初始安装大小,加载为最终用户平台优化的资产,并降低运行时内存压力。

【Unity】Asset资源加载详解_第1张图片

由上图可见AssetBundle是包含了多个或者单个Asset的包,里面的Asset可能是贴图可能是模型,
所以加载过程上来说,一个prefab可能会包含多个AssetBundle,
加载必须先加载AssetBundle以此来寻找Asset,可以通过AssetBundleManifest.GetAllDependencies获取依赖的AssetBundle。

细节:AssetBundle是一个小文本,记录了依赖和aaset的一些预制信息,一个没有依赖的AB镜像文件大概会有7kb左右的内存,一般文件会在10多k,一般几个依赖的ab加载会有0-2ms左右,很小如果把很多文件都打成一个ab会引起依赖特别多的情况,所以分包是一个需要好好控制的事情

Manifest

随着调用打包的API会自动产生Manifest,是一个保存所有AssetBundle关系的清单。
一般在加载过程中都需要通过Manifest获取依赖然后再加载真正的Asset.

加载与Instance
为什么要Instance呢,因为可能会有多个重复加载,用一个加载好的,通过部分复制,大部分引用的方式来使用。

图示

【Unity】Asset资源加载详解_第2张图片

AssetBundle使用流程

1.设计AssetBundle

//todo 如果多人看就搞个全程截图教程

【Unity】Asset资源加载详解_第3张图片

【Unity】Asset资源加载详解_第4张图片

应用:
按照一定策略打包
一个关卡/UI面板多张贴图打成一张贴图,以此减少drawcall但也增加了内存。
如果不使用loadassetall,单独的资源打成一个Asset,减少加载量和加载的包量,但也使需要打的包数量增加。

2.打包

接口:BuildPipeline.BuildAssetBundles
使用:会打包所有标记了assetbundle名的资源,并且会产生一个AssetBundleManifest 其会listing all AssetBundles included in this build.

BuildPipeline.BuildAssetBundles("Assets/ABs", BuildAssetBundleOptions, BuildTarget);
public class TestEditor
{
    [MenuItem("Test/build Test")]
    public static void BuildTest()
    {
        BuildAssetBundleTest(EditorUserBuildSettings.activeBuildTarget);
    }
    const string AssetBundlesOutputPath = "Assets/StreamingAssets/";
    static void BuildAssetBundleTest(BuildTarget buildTarget)
    {
        string outputPath;
        outputPath = AssetBundlesOutputPath;
        Debug.Log("outputPath:" + outputPath);
        if (!Directory.Exists(outputPath))
        {
            Directory.CreateDirectory(outputPath);
        }
        BuildAssetBundleOptions buildOptions = BuildAssetBundleOptions.ChunkBasedCompression |
                                                BuildAssetBundleOptions.DeterministicAssetBundle |
                                                BuildAssetBundleOptions.DisableWriteTypeTree;
        BuildPipeline.BuildAssetBundles(outputPath, buildOptions, buildTarget);

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log("打包完成");
    }
}

3.Build Asset上传到场外

【Unity】Asset资源加载详解_第5张图片

4.加载依赖

获取 : Manifest.GetAllDependencies(abPath);

4.同步加载

【Unity】Asset资源加载详解_第6张图片

4.AB异步加载

目的:缓解同步加载卡顿
注意:异步的时候同步加载会造成冲突,导致加载失败等问题

【Unity】Asset资源加载详解_第7张图片

5.Cache

把加载的AB Cache住,因为ab不能重复加载。
把加载的Asset Cache住,不用每次都去判断ab是否加载、寻找依赖、加载依赖。load的时候直接返回cache。

应用:
方案①做池管理加载和卸载,底层不cache只负责unload
方案②cache了加载好的Obj,用于下次调用直接返回obj ,每次调用进行了计数+1,调用Unload可以-1

6.实例化

【Unity】Asset资源加载详解_第8张图片

注:实例化如果是异步,可以在底层列队
 

7.卸载Assets

目的: 
①游戏内容多,导致内存过大,
②没有内存问题的游戏可以只在切换场景的时候才清空Cache、GC释放内存

使用api:
①asset.unload(false) 只会卸载包头文件
②asset.unload(true) 包头与asset都会删除(如果场景还有GameObject会丢失 或者 如果不卸载会内存过高/泄漏)

说明:
加载系统一般会保存目标asset,而其引用着依赖的asset
目标asset的unload只会unload目标 但一般引用的asset才是内存占用最高的 所以存在2种卸载方式①调用卸载时 把依赖的也unloadtrue(目前只卸载了目标ab和asset) ②调用unloadunuse才卸载依赖的asset

如何卸载:
①当asset引用为0 unload(true) 此时只是把目标的AB卸载掉 依赖依然在内存中 但引用为0  调用UnloadUnuseAssets 下次GC便会清除 
②切场景全部删除(除了列表里的)

UnloadUnuseAsset这个API是Unity写的 是个异步的 一般会有几帧的小卡 做个回调 等UnloadUnuse做完才调用C#的GC C#的GC是会同步的卡顿的。需要在适合的时机调用 

优化

  • AB.tolower的Dic//存起来 减少gc//可以在打包时存 也可以在首次加载的时候存一下
  • AB同步,Asset异步
  • 异步加载回调分帧Invoke
  • 拆分加载和反序列化
  • 预先反序列化和实例化
  • Asset的DontDestroy列表
  • 压缩,lz4知道第几块 然后取出 lam是要整个压缩 整个读取
  • 打bundle包是存在粒度问题,太大的包会使得热更时包很大,包太小会使得ab非常的多,加载IO和耗时上升
  • 可以使用Addressable 异步实例化(实际里面也是做了拆分?)
  • 更多应用级优化看大世界加载优化

资源内存Profiler

assetbundle:Not Saved→AssetBundle  序列化Other→SerializedFile

问题记录:

  • 加载卡住或者加载不出或者闪退的也许原因:同步异步冲突、ab与Asset对不上、Editor下AB与Unity版本不对
  • 如果对AB.LoadFormFile进行耗时检测 那么这个时间会在0-几毫秒之间 似乎AB的内容多少 但很迷有时候会有几十毫秒 但是看文件却又不大 DeepF开了也只是看到File.Open或者找不到这么高的耗时 很迷..
  • 回调的方式不是很好,做Task会比较优雅
  • 如果不卸载依赖的AB ,问题也不大因为AB也不大,也怕如果有计数,计数错误的导致问题(虽然做了但注释了),AB对Asset其实有引用关系,如果把AB Unload(false),又被其他引用,那么可能会泄露
  • Editor下使用AssetDataBase无法卸载和异步,无法模拟真实AB,而项目又常常非常的大,打AB很不方便,解决就得搞个自动打AB,然后还得放项目外,项目大了就检查资源卡,如果想debug卸载,还得给个口子开关log和设定要debug的资源名字。。。
    想要editor下跑AB,设置一些接口简单的开启AB,把AB放到库里,打包机定期打包上传,让每个人都轻易跑本地AB,然后想要打单独AB也比较简单的界面操作就好了
  • 打包很久,主要在shader和AB,即使增量AB的检查也是很久的,每个文件hash检查的IO也很久。
  • shader可以收集和提前WarnUp,但是还是会卡,shader首次运行会从CommonShader生成对应平台shader,第二次就会快一些,但有些shader的复杂逻辑也依然会卡

-------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class AssetBundleMgr
{
    protected AssetBundleManifest manifest;
    protected string manifestPath = "StreamingAssets";
    protected Dictionary cacheAB = new Dictionary();

    private static AssetBundleMgr mInstance = null;
    public static AssetBundleMgr GetInstance()
    {
        if (mInstance == null)
        {
            mInstance = new AssetBundleMgr();
        }

        return mInstance;
    }

    private AssetBundleMgr()
    {
        AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, manifestPath));
        if (ab != null)
        {
            var manifest = ab.LoadAsset("AssetBundleManifest");
            if (manifest == null)
                Debug.LogError("Load Manifest Asset Fail");
            else
                this.manifest = manifest as AssetBundleManifest;
        }
        else
        {
            Debug.LogError("Load Manifest AssetBundle Fail");
        }
    }

    public Object Load(string abPath, string prefabName)
    {
        LoadDependencies(abPath);
        if (!cacheAB.TryGetValue(abPath, out var ab))
            ab = LoadAssetBundle(abPath);
        var asset = LoadAsset(ab, prefabName);
        return asset;

    }
    protected void LoadDependencies(string abPath)
    {
        if (manifest == null)
            return;
        string[] dependences = manifest.GetAllDependencies(abPath);
        for (int i = 0; i < dependences.Length; i++)
        {
            string dependABPath = dependences[i];
            if (!cacheAB.ContainsKey(dependABPath))
            {
                cacheAB[dependABPath] = LoadAssetBundle(dependABPath);
            }
        }
    }
    protected AssetBundle LoadAssetBundle(string abPath)
    {
        var fullAbPath = Path.Combine(Application.streamingAssetsPath, abPath);
        var ab = AssetBundle.LoadFromFile(fullAbPath);
        cacheAB[abPath] = ab;
        if (ab == null)
            Debug.LogError("Failed to load AssetBundle" + abPath);
        return ab;
    }
    protected Object LoadAsset(AssetBundle assetBundle, string assetName)
    {
        Object asset = assetBundle.LoadAsset(assetName);
        if (asset == null)
            Debug.Log("Failed to load asset:" + assetName);
        return asset;
    }
}
using System.IO;
using UnityEditor;
using UnityEngine;

public class BuildTest
{
    static string mResPath = "Assets/Res";
    static string outputPath = Application.streamingAssetsPath;

    [MenuItem("Tools/Build PC")]
    public static void BuildPCAssetBundle()
    {
        BuildAssetBundle(BuildTarget.StandaloneWindows);
    }
    public static void BuildAssetBundle(BuildTarget buildTarget)
    {
        SetFolderBundleName(mResPath);
        if (!Directory.Exists(outputPath))
        {
            Directory.CreateDirectory(outputPath);
        }
        BuildAssetBundleOptions buildOptions = 
            BuildAssetBundleOptions.ChunkBasedCompression | 
            BuildAssetBundleOptions.DeterministicAssetBundle | 
            BuildAssetBundleOptions.DisableWriteTypeTree;

        BuildPipeline.BuildAssetBundles(outputPath, buildOptions, buildTarget);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log("打包完成");
    }

    [MenuItem("Tools/Set All BundleName")]
    public static void SetResFolderAllBundleName()
    {
        SetFolderBundleName(mResPath);
        Debug.Log("设置AB Name完成");
    }
    public static void SetFolderBundleName(string rootPath)
    {
        DirectoryInfo folder = new DirectoryInfo(rootPath);
        FileSystemInfo[] files = folder.GetFileSystemInfos();
        int length = files.Length;
        for (int i = 0; i < length; i++)
        {
            if (files[i] is DirectoryInfo)
            {
                SetFolderBundleName(files[i].FullName);
            }
            else
            {
                if (!files[i].Name.EndsWith(".meta") &&
                    !files[i].Name.EndsWith(".cs"))
                {
                    file(files[i].FullName);
                }
            }
        }
    }
    static void file(string source)
    {
        string assetPath = "Assets" + source.Substring(Application.dataPath.Length);
        string assetName = source.Substring(Application.dataPath.Length + 1);
        //在代码中给资源设置AssetBundleName
        AssetImporter assetImporter = AssetImporter.GetAtPath(assetPath);
        if (Path.GetExtension(assetName) != null && Path.GetExtension(assetName) != "") { assetName = assetName.Replace(Path.GetExtension(assetName), ""); }
        assetImporter.assetBundleName = assetName;
    }
}

----------------------------------------------------------------------------------

end

你可能感兴趣的:(Unity,unity,跨平台,unity)