type
status
date
summary
tags
category
icon

前言

在Unity开发中,AssetBundle方面的知识十分重要,所以我希望记录下这些知识点,方便去复习和查找,文章可能会随着自己的理解不断进行更新

一、AssetBundle的定义

根据Unity官方文档的定义:
📌
AssetBundle 是一个文件集合,包含可在运行时加载的特定于平台的资源(模型、纹理、预制件、音频剪辑甚至整个场景),就像一个文件夹,其中包含了两种不同的文件:
  • 序列化文件:资产被序列化分解成单独的对象并写入到一个单独的文件中
  • 资源文件:是为某些资产(纹理和音频)单独存储的二进制数据块,以允许Unity从另一个线程上的磁盘有效加载它们

二、AB包详解

在Unity开发中,可以被压缩的资产包括模型、贴图、预制体(prefab)、音效、材质球等。AB包作为一种热更新技术,对于可下载内容(DLC)、减少游戏客户端初始安装大小、以及减少程序运行时内存压力都有很大帮助。
💡
其中C#脚本不能够被打进AB包中,因为C#是编译解释型语言,需要经过编译器编译后才能被执行,因此一般使用Lua语言对项目中的C#脚本进行热更新

1. AB包的优势

谈及AB包时,我们经常会将其和Resources包进行对比。 Resources文件夹下的所有资源在项目打包时会被打成一个整包(Resources文件夹在Editor文件夹下除外),我们无法对打包后的文件进行具体查看,更无法对其内容修改,通常通过接口Resources.Load() 动态的对其中的资源进行加载。
  • Resources文件夹下的所有内容都会被打包进游戏的安装包中,随着游戏内容不断更新,安装包的体积会急速增加
  • 不能进行热更新,当需要对游戏资源进行更改时,玩家需要重新下载客户端
 
与Resources包相比,AB包不仅能够自定义打包的内容,在项目运行中动态的从服务端进行更新,还能够自定义资源的存储位置和压缩方式,更加的灵活与方便。以下是三种不同的资源压缩方式:
No Compression不进行压缩,解压速度快,没有进行压缩所以包体很大 LZMALZMA是流压缩方式(stream-based)。流压缩再处理整个数据块时使用同一个字典,它提供了最大可能的压缩率,但是只支持顺序读取。所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。 LZ4LZ4是块压缩方式(chunk-based)。块压缩的数据被分为大小相同的块,并被分别压缩。如果需要实时解压随机读取,块压缩是比较好的选择。LoadFromFile()LoadFromStream()都只会加载AB包的Header,相对LoadFromMemory()来说大大节省了内存。大部分时候较为推荐使用

2. 热更新

notion image
通过AB包,客户端能够只携带必要的默认资源,然后在游戏初始化时动态地通过例如MD5码,HashCode等方式和资源服务器中的文件去比对,检测需要对哪些AB包进行下载和更新。通过这种方式,玩家能够避免在每次版本更新时,都需要重新下载整个客户端。
在一些玩家用户对游戏加载时长特别敏感的平台下(微信小游戏等),游戏首包能够只包含首个场景的资源,然后在游戏中通过AB包的形式,动态加载其他不同的场景和资源

三、AB包流程相关

1. 资源打包

1.1 通过代码进行打包

一般在需要自己开发一个AB包打包工具时会需要使用这种方法,这里暂时仅贴出链接:AssetBundle 工作流程 - Unity 手册 (unity3d.com)

1.2 通过工具进行打包

在较早的Unity版本中,可以在包管理器中下载官方的AssetBundles-Browser-master 工具将资源打成AB包,但在较近的版本中,该功能被封装到了Addressables中,这里示范如何使用AssetBundles-Browser-master 工具进行打包。

2. 查找资源依赖

2.1 资源丢失

当一个资源使用到了其他AB包中的资源(例如材质等)而没有加载对应的AB包时,这个资源被加载时会出现资源丢失的情况。
📌
Unity 5.x版本里提供了更加人性化的依赖自动管理机制——对指定打包的资源,Unity会自动收集并分析其依赖的资源
如果包内资源依赖的其他资源,没有被显式指定打包到ab包中,那么被依赖的资源在打包时会自动的被打包进同一个包内
如果被依赖的其他资源已经被指定打包进其他ab包里,那么这两个ab包之间就会构成依赖关系。为了让资源能够正确显示,加载目标资源的ab包时也需要加载其依赖的ab包

2.2 资源冗余

如果不同AssetBundle中的A资源和B资源都依赖了一个没有被指定要打包的资源C,那么C就会被复制到两个AssetBundles中,这时C的两个副本将被视为具有不同标识符的不同Objects,这就造成了资源的冗余,增加了安装包的大小。有几种办法可以解决这个问题:
将AssetBundles分类,这样就不会同时加载两个共享依赖项的AssetBundles
  • 这种方法可能适用于特定类型的游戏,例如基于关卡的游戏,将不同关卡内的资源分别打进不同的AssetBundles中能够在很大程度上避免这个问题的发生。但缺点是,它仍然增加了项目AB包的大小,如果关卡内有很多资源,它会显著的增加场景的构建和加载时间
确保所有存在依赖的Asset都被打进了同一个AssetBundle中
  • 这完全消除了冗余资产的风险,但也带来了复杂性。应用程序必须跟踪AssetBundles之间的依赖关系,并确保在调用任何AssetBundle.LoadAssetAPI之前加载正确的AssetBundles

Unity中加载AB包依赖项的示例:

3. 资源加载

3.1 加载AssetBundle的API

AssetBundle.LoadFromFile(Async optional)
  • 功能AssetBundle.LoadFromFile 是一个高效的API,用于从本地存储器(如硬盘或SD卡)加载未压缩LZ4压缩的AssetBundle。
  • 描述
    • 移动设备:API只会加载AssetBundle的文件头,不会从硬盘中加载剩余的数据。AssetBundle的Objects只有被加载方法调用(比如AssetBundle.Load),或实例ID被间接引用时才会被加载。在这个情境中没有额外的内存被消耗。
      Unity编辑器:这个API将会把整个AssetBundle加载到内存中,就如同调用AssetBundle.LoadFromMemoryAsync一样,将所有字节从硬盘中读出。如果项目在Unity编辑器中进行评估,这个API在AssetBundle加载时,将会引起内存峰值。不过这不会影响实机上的表现,在实机上需要重新测试下,确定会遇到峰值问题再进行补救行为。
  • 示例
AssetBundle.LoadFromMemoryAsync(Async optional)
  • 功能:从托管代码字节数组(在C#中的字节[])中加载一个AssetBundle 。它始终将来自托管代码字节数组的源数据复制到新分配的连续本机内存块中。如果AssetBundle是LZMA压缩的,它将在复制时解压缩AssetBundle。未压缩的和LZ4压缩的AssetBundles将被逐字复制。
  • 描述
    • 此API消耗的最大内存量至少为AssetBundle的两倍:由API创建的本地内存中的一个副本,以及传递给API的托管字节数组中的一个副本。因此,从通过此API创建的AssetBundle加载的资产将在内存中重复三次一次位于托管代码字节数组中,一次位于AssetBundle的本机内存副本中,第三次位于GPU或系统内存中用于资产本身。
  • 示例
    UnityWebRequest.GetAssetBundle
    • 功能
      • UnityWebRequest API允许API允许开发人员指定统一究竟应该如何处理下载的数据,并允许开发者以消除不必要的内存使用情况。使用UnityWebRequest下载AssetBundle的最简单方法是调用UnityWebRequest.GetAssetBundle
    • 描述
      • LZMA压缩格式的AssetBundle在下载过程中会进行解压缩,并通过LZ4压缩方式进行缓存。通过设置Caching.CompressionEnabled可以更改此行为。当下载完成时,DownloadHandlerassetBundle属性提供了下载AssetBundle的方法,就好像对下载的AssetBundle调用了AssetBundle.LoadFromMemory 方法。如果将缓存信息提供给UnityWebRequest对象,并且所请求的AssetBundle已经存在于Unity的缓存中,则AssetBundle将立即变为可用,此API将以AssetBundle.LoadFromMemory 同样的方式运行。
    • 示例
      📌
      总体来说,AssetBundle.LoadFromFile无论在何时都被推荐优先使用,因为这个API无论在运行速度,磁盘存储还是内存使用上都是最高效的

      3.2 从AssetBundle加载Asset的API

      AssetBundle.LoadAsset(LoadAssetAsync)
      • 功能:通过指定的name从AssetBundle加载资源
      • 示例
        AssetBundle.LoadAllAssets(LoadAllAssetsAsync)
        • 功能:从AssetBundle中加载所有的资源。只有在需要加载AssetBundle中的大部分或全部对象时才能使用它。与其他两个API相比,LoadAllAssets比多次调用LoadAssets稍快。因此,如果要加载的资产数量很大,但数量少于AssetBundle中资源总数的66%时,请考虑将AssetBundle拆分为多个较小的bundles并对它们使用LoadAllAssets
        • 示例
          AssetBundle.LoadAssetWithSubAssets(LoadAssetWithSubAssetsAsync)
          • 功能:加载包含多个嵌入对象的复合资产时,应使用这个API,例如嵌入动画的FBX模型或嵌入多个精灵的精灵图集。如果需要加载的对象全部来自同一个资产,但与许多其他不相关的对象一起存储在AssetBundle中,则使用此API。
          • 示例
            📌
            同步版本总是比其异步版本快至少一帧,异步加载将每帧加载多个对象,直到它们的时间片限制

            4. 资源卸载

            对于ab包中常用的资源种类来说,卸载时分别有以下几种情况:
            • GameObject:由于通常情况下会对其进行改动,所以当对加载出的GameObject进行实例化时,是复制一份该资源进行实例化。这意味着,当AB包中的GameObject从内存中卸载后,实例化的GameObject不会因此丢失,并且对实例化对象的修改不会影响到GameObject资源。
            • Shader,Texture:由于通常情况下不需要对其进行改动,所以实例化时是通过引用的方式进行实例化。也就是说,当AB包中的Shader和Texture资源从内存中卸载后,实例化的Shader和Texture会出现资源丢失的情况。并且对实例化对象的修改会影响到Shader和Texture资源。
            • Material,Mesh:由于有时候可能需要对其进行改动,所以是通过引用+复制来进行的实例化。当AB包中的Material和Mesh资源从内存中卸载后,实例化的Material和Mesh会出现资源丢失的情况,但是对实例化对象的修改不会影响到Material和Mesh资源。
            notion image

            如果一个AB包被不恰当的卸载了,可能会引起Object在内存中重复存在,在某些情况下也会导致一些预期不符的表现,所以弄清AssetBundle.Unload分别传入true和false参数时会发生什么十分重要。这里通过Unity官方文档的举例来说明:
            notion image
            假设M是从AssetBundle中通过Load加载的素材
            参数为true时:
            AssetBundle会被卸载,与此同时M也会被删除,这意味着场景中引用了M的资源(例如上面过的shader和texture)会出现资源丢失的情况。这种卸载方式十分彻底,会将AssetBundle在内存中的镜像和通过Load加载的对象都从内存中删除,但需要一套机制(目前通常使用的方法是引用计数)来关注是不是还有资源引用,会不会引起异常。
            notion image
            参数为false时:
            参数为false时,AssetBundle内的文件内存镜像会被释放,实例化的物体还都保持完好。简单的说就是断开了AssetBundle内存镜像和实例之间的联系。如果再次实例化对象,也不会返回以前实例化过的AssetBundle内存镜像,而是重新实例化一个新的AssetBundle内存镜像,那么这样就出现了冗余,同样的资源,内存中会出现多份。
            notion image
            对于大多数的项目来说,这种情况都不是预期中的。这样看来,使用AssetBundle.Unload(true)来确保Objects在内存中唯一存在或许是一个好的办法,两种常见的方法是:
            1. 在应用程序的生命周期内定义一个合适的节点,并在此期间卸载不需要的AssetBundle。例如在关卡切换时或者在场景加载期间,我们应该在这个节点卸载尽可能多的对象并加载新的对象
            1. 维护单个对象的引用计数,并在AssetBundle加载的所有对象都未被使用时才卸载该AB包。这允许应用程序卸载和重新加载单个对象,而无需复制内存
            如果应用程序必须使用AssetBundle.Unload(false) ,则只能通过两种方式卸载各个对象:
            1. 消除场景和代码中对Objects的引用。完成此操作后调用Resources.UnloadUnusedAssets
            1. 切换另一个场景。这个操作会消除当前场景中所有的Objects然后自动调用Resources.UnloadUnusedAssets

            资源卸载总结

            1. AssetBundle.Unload(false)卸载AssetBundle后需要留意通过对象的引用问题,防止内存中出现资源冗余,较好的做法是卸载AssetBundle后调用Resources.UnloadUnusedAssets
            1. AssetBundle.Unload(true)在使用中常见的做法是给创建出来的AB包添加引用计数。当通过AB包加载一个资源时,该AB包的引用计数加一。相反当销毁一个资源时,该AB包的引用计数减一。这样,当AB包的引用计数不为0时,表示场景或代码中仍有该AB包的资源被引用,而当计数为0时,表示没有引用了,此时便可以调用AssetBundle.Unload(true)卸载该包。
              1. 📌
                一个AssetBundle在本地存储中以一个文件的形式存在时,其占用的内存开销很小,几乎不会超过10-40kb,但发生资源冗余时内存占用就比较高了

            Unity中卸载AB包常用的API:

            五、参考资料

             
            如图片加载失败,可以刷新多试几次
             
            Unity 文件系统模块 GUID,Local ID和Instance ID详解《Unity3D主程手记》笔记:3D模型合并
            • Twikoo
            • Cusdis