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,目录结构应该如下:
Unity热更新技术学习——AssetsBundle
注意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的比较如图四。
    Unity热更新技术学习——AssetsBundle
    Unity热更新技术学习——AssetsBundle
    Unity热更新技术学习——AssetsBundle
    Unity热更新技术学习——AssetsBundle
    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)。

【未完待续。。。】