当前位置: 首页 > news >正文

Unity热更新

1,热更新的概念与作用


app更新通常分为两类,一种是整包更新(换包),一种是热更新(不换包,通过网络下载,动态更新资源等)。

  • 整包更新,是指在需要更新时,需要用户手动到应用商店或官方网站下载新版本安装包并重新安装的一种更新方式,该方式成本较高,一般只有在无法热更新时才使用。
  • 热更新,是指在不需要重新编译发布应用程序的情况下,通过远程更新服务器向客户端推送程序代码、资源文件等数据的一种技术手段,以修复程序漏洞、优化游戏性能、更新游戏内容等。

热更新又分资源热更新和代码热更新,资源热更新较为简单,一般的app都可实现,而代码热更新,由于考虑到安全性,代码编译等问题,实现起来较为困难,一种实用的方法就是把代码当成资源。Unity热更新就是把代码(如Lua代码)打包成AssetBundle,达到和其它资源一样的更新效果。

在当今快节奏时代,app更新频繁,尤其是游戏app,如果每次更新都需要换包,十分影响用户体验,极易造成用户流失,代价成本实在太高,因此app热更新是十分必要的。


如果有了热更新,会带来什么好处呢?

  • 提高用户体验。热更新可以实现及时修复bug和添加新功能等,减少了玩家等待更新的时间和下载流量,提高了用户的体验感。
  • 降低开发成本。热更新可以在不重新打包的情况下实现游戏的更新,避免了频繁发布新版本的成本和风险。
  • 提高迭代效率。热更新可以快速地进行游戏内容的调整和修改,加快了游戏迭代周期,提高了开发效率。

2,热更新原理

热更新,是通过把最新的资源或代码放到网络服务器,app检测到需要更新版本时,通过网下载资源或代码到本地包,将新的代码或资源加载到应用程序中,以替换旧的代码或资源。

Unity以C#为主要开发语言,如何能做到代码的热更新呢?
C#是编译型语言,Unity在打包后,会将C#编译成一种中间代码IL,后续对这些IL的编译方式不同可以分为AOT和JIT,最终编译为各平台的NativeCode,在没有特殊处理的情况下,无法直接通过替换NativeCode,来达成热更新的。
一种理想化的C#热更新流程是:

  • 把需要更新的代码编译成动态链接库
  • 游戏启动时加载新的动态链接库
  • 用反射的形式获取动态链接库中的实例或方法

这种模式在PC和Android平台是可以的,但在IOS平台是不可行的。因为IOS对申请的内存禁止了可执行权限,所以运行时创建/加载的NativeCode是无法执行的。


为了解决IOS上的热更新问题,有两个主流方案:ILRuntime 和 HybridCLR。


ILRuntime

Unity会把C#代码打包成DLL,ILRuntime在运行时用自己的解释器来解释IL并执行,而不是直接调用.NET FrameWork或Mono虚拟机来运行代码。它借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。
但是ILRuntime会有不少限制

  • ILRuntime和原始的 compiler是两套东西,也就是说你的热更DLL和主工程的DLL实质是不互通的(如热更DLL中一个类要继承主工程DLL的一个类),所以就存在跨域问题,需要写委托适配器,委托转换器。在发布版本后这些不能热更,使用之前一定要预留好可能会使用的
  • 部分 C# 语法不支持:由于 ILRuntime 是基于 Mono 实现的,而 Mono 不支持所有 C# 语法,所以 ILRuntime 在某些 C# 语法方面也有限制,比如属性、泛型委托、可选参数等
  • 需要特殊处理的代码:由于 ILRuntime 的实现方式,一些特殊的代码需要进行特殊处理,比如反射、LINQ、协程等
  • 性能问题:由于 ILRuntime 需要动态解析和执行代码,相对于编译时静态绑定的方式,其性能会有一定程度的下降。同时,在使用过程中也需要注意避免频繁的跨域调用和反射操作,以免影响性能
  • ILRuntime对多线程Thread不兼容,在热更代码里使用多线程会导致Unity崩溃闪退

HybridCLR

是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
IL2CPP是一个纯静态的AOT运行时,不支持运行时加载dll,因此不支持热更新。HybridCLR扩充了IL2CPP的代码,使其由纯AOT Runtime变成“AOT+Interpreter”混合Runtime,进而原生支持动态加载Assembly,使得基于IL2CPP打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行。


HybridCLR是近年来一种划时代的Unity原生C#热更新技术,见https://hybridclr.doc.code-philosophy.com/

相比于直接热更新C#代码,使用C#+Lua脚本的热更新方案是目前最主流的实现方式。
Lua是一种跨平台的脚本语言,它主要依赖解释器和虚拟机实现跨平台功能,Lua是解释型语言,并不需要事先编译,而是运行时动态解释执行的。这样Lua就和普通的游戏资源如图片,文本没有区别。由于解释器和虚拟机都是跨平台的,lua脚本也就可以在不同的平台上运行了。
本质上就是利用相关插件(如ulua、slua、tolua、xlua等)提供一个Lua的运行环境(虚拟机),为Unity提供Lua编程的能力,让C#和Lua可以相互调用和访问。

3,xLua热更新方案

xLua是腾讯一个开源项目,xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。
xLua在功能、性能、易用性都有不少突破,这几方面分别最具代表性的是:

  • 可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;
  • 编辑器下无需生成代码,开发更轻量;
  • 出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;

下载地址:https://github.com/Tencent/xLua


4,xLua的简单使用

4.1,xLua安装使用

xLua下载后,将xLua文件中的Assets文件夹下的文件放到项目中的Assets文件下,就完成了XLua的安装。
新建C#代码LuaManager.cs

using UnityEngine;
using XLua;public class LuaManager : MonoBehaviour
{LuaEnv m_luaEnv;void Start(){m_luaEnv = new LuaEnv();m_luaEnv.DoString("print('Hello World')");}
}

新建场景,挂上LuaManager.cs,运行,看到打印 Hello World ,则安装成功了

4.2,自定义Lua加载器

要想执行lua文件,就要用上Lua加载器了,修改LuaManager.cs


using System;
using System.IO;
using UnityEngine;
using XLua;public class LuaManager : MonoBehaviour
{public static string LuaDir = "src"; // 存放lua文件的位置,Assets根目录下LuaEnv m_luaEnv;Action m_startAction;Action m_updateAction;void Start(){m_luaEnv = new LuaEnv();m_luaEnv.AddLoader(new LuaEnv.CustomLoader(this.LuaLoaderFromRes));// 请求执行src下的Main.lua文件m_luaEnv.DoString("require('Main')", "chunk");LuaTable luaTable = this.m_luaEnv.Global.Get<LuaTable>("Main");if (luaTable != null){m_startAction = luaTable.Get<Action>("Start");m_updateAction = luaTable.Get<Action>("Update");}// 执行Main.lua Start方法m_startAction?.Invoke();}void Update(){// 执行Main.lua Update方法m_updateAction?.Invoke();}private byte[] LuaLoaderFromRes(ref string filePath){filePath = filePath.Replace('.', '/');if (!filePath.EndsWith(".lua")){filePath += ".lua";}#if UNITY_EDITORstring path = Application.dataPath + "/" + LuaDir + "/" + filePath;if (File.Exists(path)){//读取路径下的文件的值以字节形式返回return File.ReadAllBytes(path);}
#endif// TODO// android ios 等平台读取lua文件return null;}
}

在Assets目录下新建文件夹src,src文件夹下新建文件Main.lua

Main = {}
setmetatable(Main, {__index = _G})
local _ENV = Mainfunction Start()print("Lua Start")
endfunction Update()-- TODO
endreturn Main

运行,看到打印 Lua Start ,表示成功,Update()可增加每帧的逻辑,可在src下继续增加其它lua文件

4.3,Lua调用C#

[LuaCallCSharp],在C#类加上标签[LuaCallCSharp],就可在Lua中访问了
新建C#代码GameTest.cs

using UnityEngine;
using XLua;namespace MyGame
{[LuaCallCSharp] // 建立Lua调用C#的映射public class GameTest : MonoBehaviour{public string Name;void Start(){Debug.Log("Name:" + Name);}public void CallTest(string text){Debug.Log("Lua Call:" + text);}}
}

修改Main.lua

Main = {}
setmetatable(Main, {__index = _G})
local _ENV = Mainfunction Start()print("Lua Start")-- 访问C#的类,使用CS + 命名空间 + 类名local go = CS.UnityEngine.GameObject("LuaGameObject")local test = go:AddComponent(typeof(CS.MyGame.GameTest))test.Name = "Game Test"-- 调用方法,使用:test:CallTest("666")
endfunction Update()-- TODO
endreturn Main

如果不想在每个类中加标签[LuaCallCSharp],也可以参考XLua/Editor/ExampleConfig,集中配置。
注意,如果需要打包,需提前生成Wrap文件,执行菜单命令:XLua/Generate Code

至于C#调用Lua,4.2代码已有了,更详细的参考官方例子

推荐一个基于xLua的Unity游戏纯lua客户端完整框架:https://github.com/smilehao/xlua-framework

5,xLua可热更规则:

  • 进入lua层后的一切逻辑、资源都可热更
  • app中基本所有的资源(图片、声音、3d模型、动作、特效、文本文件)、lua代码都可热更
  • C#层代码不可热更(也不完全不能,xlua.hotfix可以修改C#代码的执行,替换原来的逻辑,但这是lua代码,不是直接修改C#)
  • 需要新增或修改的代码必须是C#代码则不可热更

6,热更新流程

6.1,更新前准备

  1. 打包AssetBundle,打包程序会比较Unity所有资源,与上次打包后对比实现增量打包,生成md5信息文件(assetbundlemd5.txt),版本信息文件(version.txt),递增资源版本号
  2. 上传AssetBundle,assetbundlemd5.txt,version.txt到网络服务器(cdn)
  3. 停服或后台通知用户在线更新

6.2,更新流程

  1. 启动app,下载版本信息文件version.txt
  2. 版本号的比较,如果版本号不同才继续以下流程
  3. 下载资源服务器上的md5对比文件(assetbundlemd5.txt)
  4. 确定下载列表,将最新下载的md5对比文件和本地旧md5对比文件对比,记录缺少或不同的文件。(assetbundlemd5.txt中的md5码实现此步骤)
  5. 根据下载列表,下载所需的资源。(一般放在Application.persistentDataPath)
  6. 保证下载成功后,用最新的md5对比文件覆盖本地的md5对比文件(更新assetbundlemd5.txt),记录最新的版本号

7,Unity热更新实现

版本信息文件version.txt

{"code":0,"data":{"isUpdateClient":0,"isUpdateRes":1,"version":"1.0","resVersion":"1.0.0.1","clientUrl":"https://aa.bb.cc.com/game/client.apk","resUrl":"https://aa.bb.cc.com/game/res/"}
}

这是version.txt的结构参考,JSON格式,字段说明:

  • isUpdateClient:是否强制更新整包,视情况是否打开
  • isUpdateRes:是否更新资源开头,无特殊情况都是打开
  • version:客户端版本号,如果此版本号对比不一样,需考虑更新整包
  • resVersion:资源版本号,如果此版本号对比不一样,则进行热更新,每次打包递增
  • clientUrl:客户端整包的更新地址,可根据后缀,跳转网站,或直接下载安装
  • resUrl:热更新资源的地址

App启动,下载version.txt,版本比较代码

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;public class GameStart : MonoBehaviour
{public string VersionUrl = "https://aa.bb.cc.com/game/version.txt"; // version.txt网络服务器地址public string appVersion = "1.0"; // 当前客户端版本号public string currentResVersion = "1.0.0.1"; // 当前最新资源版本号void Start(){// app 启动前逻辑,如读取客户端版本号,最新资源版本号//currentResVersion = PlayerPrefs.GetString("currentResVersion");StartCoroutine(RequestVersionInfo());}IEnumerator RequestVersionInfo(){// 加上时间戳,确保下载的是最新文件UnityWebRequest request = new UnityWebRequest(VersionUrl + "?time=" + System.DateTime.Now.Ticks);request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return request.SendWebRequest();if (request.error == null){string text = request.downloadHandler.text;LitJson.JsonData versionInfo = LitJson.JsonMapper.ToObject(text);int isUpdateClient = (int)versionInfo["data"]["isUpdateClient"];int isUpdateRes = (int)versionInfo["data"]["isUpdateRes"];string version = (string)versionInfo["data"]["version"];string clientUrl = (string)versionInfo["data"]["clientUrl"];string resUrl = (string)versionInfo["data"]["resUrl"];string resVersion = (string)versionInfo["data"]["resVersion"];if (isUpdateClient == 1){if (compareResVersion(version, appVersion)){// 提示客户端更新// Application.OpenURL(clientUrl);}}if (isUpdateRes == 1){if (compareResVersion(resVersion, currentResVersion)){// 进入热更新;//StartHotUpdate(resUrl);}}}request.Dispose();}public bool compareResVersion(string resVersion1, string resVersion2){var arr1 = resVersion1.Split('.');var arr2 = resVersion2.Split('.');for (int i = 0; i < arr1.Length; i++){if (int.Parse(arr1[i]) > int.Parse(arr2[i])){return true;}}return false;}}

热更新流程代码

IEnumerator StartHotUpdate(string resUrl)
{bool downloadFailed = false;// 下载网络服务器最新md5信息文件string md5Url = resUrl + "assetbundlemd5.txt";UnityWebRequest md5Request = new UnityWebRequest(md5Url + "?version=" + currentResVersion); // 加上版本号,确保下载的是最新文件md5Request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return md5Request.SendWebRequest();if (md5Request.error == null){AssetBundleMD5Infos remoteMd5_info = new AssetBundleMD5Infos(md5Request.downloadHandler.data); // 网络服务器最新md5信息文件AssetBundleMD5Infos tmpMd5_info; // 由于出错中断暂时保存的md5信息文件string dirPath = Application.persistentDataPath + "/" + Utility.GetPlatformName();if (!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}dirPath = dirPath + "/";if (File.Exists(dirPath + "assetbundlemd5.tmp")){byte[] fileContent = File.ReadAllBytes(dirPath + "assetbundlemd5.tmp");tmpMd5_info = new AssetBundleMD5Infos(fileContent);}else{tmpMd5_info = new AssetBundleMD5Infos(null);}List<string> needUpdateAbs = new List<string>(); // 需要下载更新的ab文件列表foreach (var abName in remoteMd5_info.m_AssetBundleMD5.Keys){string remoteMd5 = remoteMd5_info.GetAssetBundleMD5(abName);// 与网络服务器最新md5比较,不同则加载下载更新列表,AssetBundleManager.GetAssetBundleMD5(abName)本地最新md5if (tmpMd5_info.GetAssetBundleMD5(abName) != remoteMd5 && remoteMd5 != AssetBundleManager.GetAssetBundleMD5(abName)){needUpdateAbs.Add(abName);}}// 下载更新的ab文件foreach (string abName in needUpdateAbs){UnityWebRequest abRequest = new UnityWebRequest(resUrl + abName + "?version=" + currentResVersion); // 加上版本号,确保下载的是最新文件abRequest.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();yield return abRequest.SendWebRequest();if (abRequest.error == null){// 保存到最新的ab文件到本地File.WriteAllBytes(dirPath + abName, abRequest.downloadHandler.data);tmpMd5_info.AddAssetBundleMD5(abName, remoteMd5_info.GetAssetBundleMD5(abName), remoteMd5_info.GetAssetBundleSize(abName), remoteMd5_info.GetAssetBundleMiniGameId(abName));} else{downloadFailed = true;}abRequest.Dispose();}if (needUpdateAbs.Count > 0){if (!downloadFailed){// 保存最新的md5文件remoteMd5_info.SerializeToFile(dirPath + "assetbundlemd5.txt");File.Delete(dirPath + "assetbundlemd5.tmp");}else{tmpMd5_info.SerializeToFile(dirPath + "assetbundlemd5.tmp"); // 出错中断保存临时的md5,避免下次更新重新下载}}}else{downloadFailed = true;}md5Request.Dispose();if (downloadFailed){// 出错重新执行更新流程StartCoroutine(StartHotUpdate(resUrl));}
}

相关文章:

  • 干货分享 | 3D WEB轻量化引擎HOOPS Communicator如何读取复杂大模型文件?
  • 全媒体整合营销时代,如何做好网络营销?
  • 视频剪辑技巧:批量合并视频,高效省时,添加背景音乐提升品质
  • 3、Sentinel 动态限流规则
  • postMessage
  • 聊一聊GPT——让我们的写作和翻译更高效
  • 如何设置没有采购申请不允许创建采购订单(TCODE:OMET)<转载>
  • 目标检测YOLO系列从入门到精通技术详解100篇-【目标检测】SLAM(补充篇)
  • 无效的标记: --release
  • 包装印刷行业万界星空科技云MES解决方案
  • eBPF BCC开源工具简介
  • 如何从视图中取消nspopover?
  • UUID 的 5 个版本
  • llinux的更目录下的文件作用和举例
  • SpringBoot+Swagger详细使用方法
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • 《Java编程思想》读书笔记-对象导论
  • 230. Kth Smallest Element in a BST
  • Docker入门(二) - Dockerfile
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • JavaScript设计模式与开发实践系列之策略模式
  • JAVA并发编程--1.基础概念
  • js
  • js操作时间(持续更新)
  • JS专题之继承
  • Kibana配置logstash,报表一体化
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • mockjs让前端开发独立于后端
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 我这样减少了26.5M Java内存!
  • 延迟脚本的方式
  • 一天一个设计模式之JS实现——适配器模式
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • 关于Android全面屏虚拟导航栏的适配总结
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • ​什么是bug?bug的源头在哪里?
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • #pragma data_seg 共享数据区(转)
  • #pragma multi_compile #pragma shader_feature
  • (4)logging(日志模块)
  • (c语言)strcpy函数用法
  • (HAL库版)freeRTOS移植STMF103
  • (Java实习生)每日10道面试题打卡——JavaWeb篇
  • (Java数据结构)ArrayList
  • (MonoGame从入门到放弃-1) MonoGame环境搭建
  • (Spark3.2.0)Spark SQL 初探: 使用大数据分析2000万KF数据
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (南京观海微电子)——COF介绍
  • (五)网络优化与超参数选择--九五小庞
  • (转)Unity3DUnity3D在android下调试