研究了大半年的热更,才做出了一套相对完善的热更架构。不得不说,这块的知识点还是多而杂的,值得专门开篇博文来记录梳理。
知识梳理
AssetBundle
定义
AssetBundle,也就是俗称的AB包,是Unity中使用的一种文件类型,是一种可以存在硬盘上的文件,简单来说,就是“资源压缩包”,Unity中的资源,包括场景、预制体、贴图、模型等任何资源,都可以打包进AB包内,以字节的形式存储在文件中,在游戏运行过程中可以通过特定的函数一模一样的加载出来。
优势
- 所有的资源都可以在游戏运行时通过代码从AB包中动态读取,可以不改变源代码,仅通过外部更新AB包文件来实现资源的更新。
- 可以将游戏中的某些资源压缩进AB包,从外部读取,减少安装包的大小,方便引流下载。
- AB包会自动保存资源依赖,比如材质球与预制体,分别打包成两个AB包:甲、乙,当AB包甲加载完毕后再加载AB包乙中的预制体,将会自动读取AB包甲中依赖的材质球资源。
- AB包中可以采用LZMA、LZ4的压缩算法,可以减少资源大小,加快网络传输速率。
压缩算法
- LZMA:默认压缩算法,包体最小,加载速度慢,加载时需要先解压整个包
- LZ4:主流压缩算法,包体中等,加载速度中等,加载时只需要解压需要的模块
- 不压缩:包体最大,加载速度最快,加载时无需解压
综合考虑包体与加载速度,通常都是通过LZ4的算法压缩。
打包方式
- AssetBundlesBrowser:一款用于处理AssetBundle的U3D插件,提供了可视化界面,适合新手上手打包
BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)
:U3D编辑器AB包打包函数,可以自己选择导出根目录、打包算法及使用平台,通过自行编写打包逻辑,可以一键完成AB包打包、加密、压缩等逻辑,参数详解如下:- outputPath:打包出的AB包文件存放的目录
- assetBundleOptions:AB包压缩算法
- BuildAssetBundleOptions.None:LZMA算法
- BuildAssetBundleOptions.UncompressedAssetBundle:不使用压缩算法
- BuildAssetBundleOptions.ChunkBasedCompression:LZ4算法
- targetPlatform:打包出AB包使用的目标平台
- BuildTarget.Android:Android平台
- BuildTarget.StandaloneWindows64:Windows64位系统
- BuildTarget.iOS:iOS平台
加载/卸载
加载AB包
- AssetBundle.LoadFromFile(string filePath):通过AB包文件路径加载资源
- AssetBundle.LoadFromFileAsync(string filePath):通过AB包文件路径异步加载资源
- AssetBundle.LoadFromMemory(string filePath):通过字节集加载资源
- AssetBundle.LoadFromMemoryAsync(string filePath):通过字节集异步加载资源
卸载AB包
- assetBundle.Unload(bool unloadAllLoadedObjects):卸载AB包
- unloadAllLoadedObjects: 是否清理所有加载出来的资源,这里需要注意预制体资源实例化的物体不会清理掉,但所有引用类型的资源,比如物体上的材质球,都会清理掉造成丢失。
加载AB包中的资源
- assetBundle.LoadAsset
(string name):从AB包中加载T类型的资源name - assetBundle.LoadAsset(string name,Type type):从AB包中加载type类型的资源name
分包策略
AB包的尺寸会影响到加载速度,测试发现,AB包大小控制在1MB左右,可以保证较高的加载效率,同时,较小的包体细度,也可以避免玩家每次热更下载AB包,过多的浪费资源下载AB包中捆绑未更新的其他资源,因此,AB包的分包策略十分重要,这里举例几种:
- 根据类别分类:资源类别相同的打包到一起,如图片资源打包到一起、材质球打包到一起,若单种资源过多,则分成多个包,如p1、p2、mat1、mat2
- 优点:最为通用,不需要考虑资源用途,只需要考虑每种资源拆分出AB子包的尺寸
- 缺点:加载包含复杂依赖的资源时,可能需要加载多个AB包中的依赖资源,导致加载速度变慢、内存占用高。
- 根据用途分类:业务逻辑需要同时加载的资源打包到一起,如一个UI中所有的预制体、图片、材质球等都放到一起
- 优点:加载速度快,使用时不需要加载太多的依赖包;玩家热更下载的AB包大部分是需要更新的资源;
- 缺点:公用资源较难处理,每种资源放入前都要规划好用途,一旦公用,还得提出至公用资源AB包;单个AB包尺寸容易超过限制。
- 根据更新频率分类:更新频率差不多的资源打包到一起
- 优点:避免玩家热更时下载过多未更新的资源
- 缺点:具体分组策略仍需要参考上面两种
具体项目,可以综合考虑以上几种情况,根据项目特点,来制定分包策略。
使用性能优化建议
AB包大概能分为三个方面的优化:下载速度、加载速度、内存管理,建议如下:
- 下载速度:
- 通过多线程下载
- AB包文件压缩成压缩包,下载到本地后解压
- 加载速度
- AB包大小控制在1MB
- 通过进度条界面,预加载或者初始化对象池,加载较大资源
- 内存管理
- 如无必须进行资源加密的需求,建议使用
LoadFromFile
的方式加载AB包 - AB包与资源加载使用后及时卸载
- AB包中依赖相关包不宜太多、太大
- 如无必须进行资源加密的需求,建议使用
热更新
热更类型
U3D的热更新可以分为资源热更跟逻辑热更
热更原理
- 资源热更
资源热更主要基于AssetBundle技术实现,项目中所有需要热更的资源均读取自AB包文件,通过在线更新外部的资源文件,来实现整个项目资源的热更新。 - 逻辑热更
逻辑热更有C#与Lua两种语言的实现方案,目前我还没接触过C#热更,暂且只讲基于Lua语言的热更方案,其原理是通过基于U3D的Lua语言解释架构,可以将脚本语言Lua变成可执行的代码,而Lua属于脚本语言,其本质是一个Txt文本,在U3D里属于TextAsset资源,逻辑热更的本质,就是资源热更,通过资源热更获取最新的TextAsset资源中的Lua文本,然后通过架构转换成可执行的代码,目前比较流行的lua架构有xlua、tolua,各有各的优点,打算到时候开一篇专门的博文介绍逻辑热更时再说,这里就简单的提一下。
热更相关路径
- Application.streamingAssetsPath:可读不可写,打包时不会压缩,可以在外部直接得到,一般用于存放跟随包体的默认资源AB包,值得注意的是,在Android平台下,无法通过IO操作读取,只能通过Unity自带的WWW方式读取到数据
- Application.persistentDataPath:可读可写,不会跟随包体打包,一般用于存放下载来的最新AB包
- Resources:不可读不可写,跟随包体打包,仅可通过Resources.Load加载资源,一般用于存放架构中不涉及热更的资源,如UI架构的UICanvas预制体
- Application.dataPath:Android平台下不可读不可写,一般仅用于编译器环境下开发使用,如导出AB包到项目的某个文件夹中。
热更工作流
这里仅阐述下我在项目中所使用的热更工作流,供以借鉴。
打包工作流
- 打包AB包文件至AB包导出目录
- 生成资源依赖文件,遍历AB包资源,动态查询依赖资源以及所属的AB包,以json形式存储,数据字段如下:
- AB包名
- 资源名
- 所依赖的AB包名数组
- 资源名
- AB包名
- 生成版本文件,版本文件以json形式存储,数据字段如下:
- Version:资源版本号,用于判断资源是否为最新版本
- ClientVersion:客户端版本号,若C#底层修改需要重下客户端则修改该版本号
- IsRestart:更新完毕是否需要重启客户端,框架部分资源已存储至缓存,更新完毕后若需要更新该部分缓存,则直接重启客户端
- Content:更新描述,若客户端需要显示更新信息则使用该字段
- Assets:资源文件版本信息数组
- name:资源文件名字,用于找到对应文件
- size:资源文件大小,用于验证资源文件是否损坏
- hash:资源文件Hash值,用于验证资源文件是否损坏
- 加密上述文件,并保存至导出文件夹,注意若加密数据,上述资源文件版本信息中的size、hash值需要通过加密后的数据获得。
- 将部分需要与包体绑定携带的资源文件,放置在streamingAssetsPath目录,这里需要注意,当热更工作流中需要用到的资源,如lua文件、热更界面、版本文件、资源依赖文件等资源,必须与包体绑定携带
- 将导出文件夹中所有的资源文件,都放置在下载服务器上
校验工作流
- 从下载服务器中读取到版本文件并解密
- 通过加载工作流获得本地版本文件,与云端进行以下对比
- 本地版本文件不存在,则提示第一次打开应用,需要下载资源文件
- 对比本地资源文件的Size、Hash值,若发现不匹配,则对比资源版本号判断更新原因:
- 若资源版本号相同,则提示资源损坏
- 若资源版本号不同,则提示资源需要更新
- 对比自身客户端版本号,若发现不匹配,则提示前往下载最新客户端
下载工作流
- 从下载服务器中下载需要更新的资源文件至persistentDataPath目录中的Temp文件夹
- 当所有资源文件下载完毕后,前往Temp文件夹进行校验,所有资源文件校验通过,则移动至persistentDataPath目录中的Asset文件夹
- 所有资源文件下载完毕后,更新版本文件,保存至persistentDataPath目录
加载工作流
所有资源均放置在Resources目录中,目录包含一个AssetBundles文件夹及若干自定义key文件夹
加载资源,需要传入key、assetName值
尝试通过AB包形式加载资源,若不存在,则进入下一步
开发环境下,通过IO流查找AssetBundles文件夹中的具体资源文件路径,并通过Resources.Load加载
发布环境下,读取AB包字节集,并通过AssetBundle.LoadFromMemory加载AB包,字节集读取方式如下:
- 查询缓存内是否存在字节集,若不存在则进入下一步。
- 通过persistentDataPath目录尝试读取,若存在,则解密存储进缓存,防止多次加载解密造成多余的时间损耗;若不存在则进入下一步。
- 通过streamingAssetsPath目录尝试读取,由于上述打包工作流第五点规则,一定能读取到对应资源文件的字节集,解密存储进缓存。
发布环境加载AB包资源时,注意需要通过资源依赖文件获取相关依赖,提前加载依赖AB包。
尝试通过Resources.Load([key]/[assetName])加载,一些框架固定不会更新的资源,比如UI架构的UICanvas预制体,可通过这种方式加载
Tip:版本文件及资源依赖文件的资源加载单独处理,通过上述工作流中获取字节集的步骤,获得字节集并解密转为字符串,序列化为Json对象。
开源项目
放上一个开源的Unity热更架构,工作流如上,以供参考