Unity热更新技术学习——AssetsBundle详解

热更新

热更新是指,你需要为应用程序修改某种资源,或者增加某种资源的时候,不需要新发布一个新的应用程序到应用商店让用户下载并重新安装,只需要联网,然后下载更新的内容即可。比如游戏出了一款新皮肤,推出一个新活动,或者修复某个紧急的小bug,如果这些小事情每次都要用户重新下载应用程序,就特别烦,尤其是如今中国国民级的手游动辄3到5个G。

为此,考虑需要频繁更新的内容就可以分成两个部分:

  • 普通的资源文件,如材质,模型,动画,音效等等。
  • 脚本

Unity没有给我们提供官方的脚本热更方案,而对于普通的资源文件的热更,就是AssetsBundle技术。

AssetsBundle

在说AssetsBundle之前,还要说说Resources,或者Resources这个目录。

Resources

Resources是Unity最早提出的资源动态加载的方案。当你需要动态加载某个资源的时候,你需要把对应的资源,比如预置体,材质等放入Resources目录下,然后用Unity提供的API(Resources.Load())去动态加载。

这个目录在以前游戏不是很大的情况下很方便很好用。但在项目做大的时候,它的缺点就暴露得很明显了(缺点来自官方文档):

  1. 难以进行细粒度的内存管理
  2. 无论你用不用,Resources目录下的资源都会打包进游戏包里。这不仅会导致游戏构建时长增大,游戏包体大小激增,资源还难以管理
  3. 没法使用Resources进行热更新。在你构建的时候,Unity会自动帮你打包好资源,而之后游戏也只会使用这些打包好的的资源。
  4. 主线程加载。这意味着,如果你需要动态加载的资源多了,你的游戏会假死。

为了解决这些问题,AssetsBundle诞生了。

AssetsBundle

AssetsBundle是Unity另一套资源管理的方式。它和Resources的相同之处,也是他们最主要的用途就是允许工程动态加载里面的资源。不同之处在于:

  • AssetsBundle是和应用程序分开存储的
  • 允许用户下载新的AssetBundle并使用里面的资源
  • 需要用户在Unity编辑器模式下,手动编写代码构建AssetsBundle

第二个不同之处就是Unity资源热更的核心。而手动构建虽然显得更加繁琐,但也给你更多的选择。你可以选择构建AssetsBundle的路径,选择它的名称和更细分的变体(Variants),你还可以构建好AssetsBundle之后,将其存储到服务器上,让用户在需要资源更新的时候下载新的AssetsBundle。

存储目录

关于AssetsBundle的第二个话题就是它的存储位置。这里涉及到两个重要的目录:StreamingAssetsPathPersistentDataPath

StreamingAssetsPath 可以通过Application.streamingAssetsPath获取路径。在Unity编辑器模式下,它是Assets目录下的StreamingAssets目录。在Android平台里,它就是assets目录。

Android管理资源有两种方式,一种是res/raw目录,res目录里面的文件会参与Android的R文件编译,以便你能够通过R.id去访问,这也同样是为什么你在解压apk,然后尝试读取res/AndroidMenifest.xml时,得到的却是一个二进制文件。另一种就是assets目录。Android对它不会做任何事情,然而,你无法通过文件系统去访问这个目录下的资源。取而代之的是使用Android提供的API——AssetsManager。

和Android一样,Unity也不会对这个目录下的文件做任何事情,包括C#脚本,Shader和材质也不会参与编译(Unity编辑器里,你如果在这个目录下创建一个C#脚本,你会发现它的图标和正常的C#脚本图标不一样,因为Unity根本没把它当成一个需要正常编译的C#脚本)。

你应该使用这个目录去存储Unity的资源。在PC上,你可以直接使用文件系统访问Application.streamingAssetsPath获取里面的资源。而在Android和WebGL平台上,正如上面注解所讲,你无法通过文件系统直接访问。Android提供了AssetsManager这个API去访问,反映到Unity层面,就是WWW(过时)或者UnityWebRequest。

一般来说,在Editor下打出来需要在构建过程直接进入游戏包体的AssetsBundle都会放入这个目录下。但是这个目录在Android上是只读的,所以你无法将新下载的AssetsBundle放入这个目录。如果要更新的AssetsBundle,还需要另一个目录——

PersistentDataPath 可以通过Application.persistentDataPath获取路径。

  • Unity编辑器:/User/AppData/Local/Packages/<productname>/LocalState
  • Android:/sdcard/Android/data/<packagename>/files
    这个目录是可读可写的。新下载的AssetsBundle就会放入这个地方。
目录实例

在Android Studio中新建一个项目,然后在任何目录上右键 -> New -> Folder -> AssetsFolder。这会帮你创建一个assets目录。注意,你无论在哪个目录右键,新生成的assets目录都创建到/src/main下。同理,选择New一个Raw Resources Folder,目录结构应该如下:
在这里插入图片描述
注意assets和res文件右下角都有一个小图标,表示这两个目录是属于资源目录。而著名的Android清单文件AndroidManifest.xml就位于res目录下。这个目录下的资源都会被编译,想要查看apk里面AndroidMenifest.xml的内容,可以通过Android Studio -> File -> Profile or debug APK 查看。

下面我们要创建几个Unity工程,查看他们的apk的目录结构的差别。

  • 创建一个空工程,即保留新建工程的所有东西不变,打一个包——空.apk。
  • 然后在此基础上新建一个Resources目录,里面放点东西,打个包——Resources.apk。其工程目录结构如图一,其与空.apk的比较如图二。
  • 在空工程基础上新建一个StreamingAssets目录,里面放点东西,打个包——StreamAssets.apk。其工程目录结构如图三,其与空.apk的比较如图四。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    Resources和StreamingAssets的相同之处就是,它里面的资源都被放进了/assets目录下。不同之处在于:
  • Resources被放进/assets/bin/Data下,并且文件都是经过编码和压缩的文件,其文件名就是资源的guid,同时还多了一个零号guid的文件。
  • 而StreamingAssets直接处于/assets目录下,并且文件被原封不动地打进了apk。

我在Resources下放置了一个内置的Standard Surface Shader,原大小1.7kB。到了apk里面就变成了36.7kB,说明Unity对其进行了编译,生成了完整的目标平台图形学API代码(OpenGL ES)。

构建AssetsBundle

因为是编辑器手动构建,所以构建代码需要放到Editor目录下。随意建一个Editor目录,然后新建一个脚本放到Editor目录下,内容为:

using UnityEditor;
using System.IO;
using UnityEngine;

public class CreateAssetsBundle
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        string dir = Application.streamingAssetsPath; // AB包将放到StreamingAssetsPath下
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        // 开始打包
        BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, EditorUserBuildSettings.activeBuildTarget);
    }
}

在Unity里随便建点东西,比如资源里弄点预置体,材质,着色器和贴图,然后场景中随便建两个物体,之后给Assets目录下的新东西赋予AssetsBundle的名称(在右下角)。我对应的文件目录和物体对应的AssetBundle Name如下(预置体采用的是下图中的Red材质球,这很重要,因为我们后面需要看到一些因此而带来的变化):
在这里插入图片描述

物体AssetBundle Name
Red(材质)ab_mat
RedShader(着色器)ab_mat
Cube(预置体)ab_prefab
Sphere(预置体)ab_prefab
SampleScene(场景文件)ab_scene
file(贴图 png)ab_tex
folder(贴图 png)ab_tex

然后点击 Assets -> Build AssetBundles 开始打包。打完包之后就会出现上图中的StreamingAssets目录。打开它你会发现这些东西:
在这里插入图片描述
可以发现,里面有两种文件,每种文件分别有一个AssetBundle文件,一个manifest清单文件

忽略.meta文件,因为这是Unity给任何Assets目录下的文件生成的身份信息文件,跟AssetBundle无关

清单文件

manifest清单文件是文本文件,我们打开来看看其中预置体对应的AB包的清单文件的内容:
在这里插入图片描述

  • ManifestFileVersion:AB的清单文件版本

其实就是AssetsBundle的版本,一直是0,直到。。。Unity 2019。在Unity 2019以前,你都可以在Library目录下找到一个叫metadata的目录,是的,2019后就不见了。。。这又是另一个话题

  • CRC:CRC校验码
  • Hashes:资源文件和TypeTree的Hash值,可能是用来做完整性验证的
  • Assets:描述了这个AB包里包含了什么资源
  • Dependencies:顾名思义,就是这个AB包的依赖。上面我把材质球赋予了这两个预置体,所以包含这两个预置体的AB包自然就要依赖该材质球所在的AB包——ab_mat。

HashAppended和ClassType不重要(其实是我也不懂。。。

我们将场景文件也打成了AB包,而且场景中有我们预置体的实例,所以查看场景AB包的清单文件,你也会在Dependencies下发现东西。

AB包

清单文件只是描述AB包的基础信息,而真正的资源都包含在AB包里。为了查看里面的内容,我们需要使用两个特殊的工具来解压和文本化。在Unity的安装目录的Editor/Data/Tools目录下会找到这两个工具:

  • WebExtract.exe
  • binary2text.exe

将这个目录加入环境变量,以便我们能在命令行终端的任何路径下访问他们。打开命令行终端,定位到AB包所在的位置。然后先用WebExtract.exe尝试解压预置体所在的AB包:
在这里插入图片描述
得到一个新的 ab_prefab_data 目录,cd到里面,然后用binary2text.exe对里面唯一的一个文件进行文本化:
在这里插入图片描述
得到一个txt文件,打开它:
在这里插入图片描述
这虽然是一个纯文本文件,但是还有有一定结构的。推荐使用Sublime text查看,因为它提供了按照缩进进行代码折叠的功能,我们将所有缩进的文本折叠起来:
在这里插入图片描述
AB包的结构就变得一目了然了:

  • 以External References开头,这个Header描述了当前AB包中的资源都需要引入哪些外部的AB包。由于是被依赖的资源,这些AB包都会在当前AB包加载之前事先被加载进来。如果你有引用Unity内置的一些资源,比如Shader或者贴图,也会在这里被列出来。
  • 每个数据块都有一个ID,一个Class ID,和一个名称。他们代表当前AB包中包含的资源。在上述的例子中,AB包中包含了构成两个Prefab的所有组件,以及一个固定ID为1的AssetBundle类型的数据。
  • 每个数据都描述了各自的属性,以及他们的外部依赖。

以下面的一个元数据为例:
在这里插入图片描述

  • ID为-8598688866515239023,这里的ID,也叫做的PathID,是该资源在当前AB包中的唯一标识。
  • ClassID为4,即Transform。它是Transform这个类的唯一标识。
  • 然后接下来所有第一个缩进的部分,都是这个资源的属性。比如你可以通过Transform.gameObject访问当前Transform类组件所在的物体的GameObject组件,所以这里出现了一个m_GameObject
  • 属性右边的括号标识这个属性的类型,PPtr<GameObject>表示一个指向GameObject类的指针。
  • 第二个缩进的数据有很多不同的含义。比如
    • m_FileID, m_PathID用来表示当前这个属性其实是另一个资源,m_FileID为0表示该资源在当前AB包中,否则需要在Header引入的AB包列表中对应查找。m_PathID就是该资源的唯一标识符。
    • x,y, z, w就是属性m_LocalRotation的具体的值了,这个属性是一个四元数类。

上面就是AB包的基本内容。然后,还要提及AB的类型,分成由场景文件Scene打包而来的场景AB包,以及普通资源打成的松散AB包。在松散AB包中,每一个包中都包含一个固定ID为1的,名字叫AssetBundle的资源。除了这个以外,其余所有的资源的ID都是一个绝对值很大的看起来很像是Hash的ID,这个ID一般来说是全局唯一的,如果两个AB包中包含同一个PathID的资源,就表示资源冗余了。而在场景AB包中,ID从1开始依次给场景中的资源计数,所以不同场景包中的资源的ID有重复自然就不奇怪了!另外,场景AB包在用WebExtract解压出来之后,是得到两个二进制文件,每个都需要单独用binary2text进行文本化。相比不同的AB包,场景AB包多出来的那个二进制文件是SharedAsset,描述当前场景中所有物体所共享的资源。一般来说,主要是材质,着色器以及与光照有关的资源。

binary2text.exe这个工具还可以用来文本化很多Unity的二进制文件,比如第一版元数据管理系统中,Unity会在Library/metadata下保存一些二进制文件,这些二进制文件其实就是当前工程中所有资源的序列化。通过binary2text.exe文本化这些二进制文件,你会看到很多类似的东西。

另外,binary2text还可以带一些参数,例如通过-detailed参数可以使得文本化之后的文件附带很多额外的数据,包括表征当前资源大小的数据。

AssetBundle依赖

假设两个预置体被分别打进了两个AssetBundle。他们依赖同一个材质,材质使用一个贴图,然而材质和贴图都没有打AssetBundle。我们看看打出来的文件大小:
在这里插入图片描述
然后第二种方案是将材质也指定一个AssetBundle,再看看文件大小:
在这里插入图片描述
现象:材质没有打AssetBundle之前,两个预置体的AssetBundle大小都达到了87KB。材质打了AssetBundle之后,多了一个大小87KB的AssetBundle,但是两个预置体的大小都降低到了2KB。

为了内存的细粒度管理,项目喜欢把单个预置体打成一个AssetBundle。在这种情况下,上面第一种方案的内存会按照O(n)的复杂度增长,而第二种方案的内存增长的复杂度则是O(1)。

实际上,如果一个AssetBundle内的资源所依赖的另一个资源没有打AssetBundle,Unity会将其拷贝进AssetBundle里面。如果多个不同的AssetBundle的资源都依赖同一个没有打AssetBundle的资源,那么打包后该资源会在多个AssetBundle各存在一份拷贝。相反,如果将这个资源打包,那么那些依赖该资源的AssetBundle只会保存该资源所在的AssetBundle的引用。

AssetBundle Browser

Unity提供了一个插件叫AssetBundle Browser去查看工程里面的关于AssetBundle的信息。

下载地址:AssetBundles-Browser

将下载下来的文件整个目录拷贝到工程内的任何一个目录下,然后通过 Window -> AssetBundle Browser 打开。

AssetBundle Browser有三个页签,分别讲解:

Configure

在这里插入图片描述
Configure 页签有四个网格。

  • 左上角网格展示你当前工程定义的所有AssetBundle Name,尽管它可能没有被赋予任何物体。
  • 左下角网格说明当前在左上角选中的AssetBundle Name的基本信息。包括它包含的物体的总大小(未压缩)和依赖的AssetBundle。
  • 右上角显示该AssetBundle打包之后都会包含哪些物体。
  • 右下角显示一些输出信息。

我当前选中的是一个预置体所在AssetBundle Name,可以看到有三个网格都打出Warning(警告)图标。提示的信息说明,当前AssetBundle Name所包含的资源有一些对外依赖,而这些依赖不从属于任何AssetBundle。右下角提示贴图Blender_icon被自动包含进了本AssetBundle中,而原本这个贴图是不从属于任何AssetBundle的。

Build

打包构建页签。AssetBundle Browser已经帮你写好打包的代码了,使用方法略(因为一般项目都会制定自己的打包策略)。

Inspect

在这里插入图片描述
这个页签会展示已经打成AssetBundle的包的内部信息。选择左上角的 Add Folder,选择AssetBundle所在的目录(比如我的是Assets/StreamingAssets)。然后在展示的文件列表里面选择一个AssetBundle,右边会展示AssetBundle内部的信息,包括它包含的资源,预加载的组件和依赖,等等。

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值