目录
一、Lua热更新原理
利用第三方Lua热更新库,在Unity中嵌入lua解释器,利用解释器执行lua脚本。需要热更新的时候,从服务器上下载需要更新的代码文件
1、为什么要用lua做热更新,而不用C#?
C#语言本身是一种静态编译语言,它的代码在编译时会被转换成机器码,然后才能被计算机执行。这种静态编译的特性使得C#在运行时无法直接修改已编译的代码。另外,C#语言在运行时使用的是Common Language Runtime (CLR)环境,CLR负责将C#代码转换成可执行的机器码。CLR在运行时会对代码进行各种优化和安全检查,这些优化和检查过程也使得热更新变得困难。
Lua是一种动态脚本语言,它的代码在运行时会被解释器逐行执行,而不需要进行静态编译。这种动态性使得Lua可以在运行时修改已加载的代码,实现热更新。具体来说,Lua的解释器可以在运行时动态加载新的脚本文件,并替换已加载的代码模块。这种动态加载和替换的机制使得Lua可以实现热更新,即在程序运行过程中修改和更新代码,而无需重新启动整个程序。
其次,C#不能打包成AB包,而Lua可以,Lua结合AB包使用起来非常方便。
2、C#能不能实现热更新?
可以,利用反射实现
反射是什么?
C#反射是指在运行时动态地获取和操作程序集、类型、成员等信息的机制。通过反射,可以在运行时通过程序集中的元数据来实例化对象、调用方法、获取和设置属性等,而不需要在编译时明确知道这些类型和成员的具体信息,主要用到Type,Assembly,Activator三个类,这里不过多赘述,详细内容可以自行学习。
C#反射实现热更新
将需要频繁更改的逻辑部分独立出来做成DLL(动态链接库(Dynamic Link Library),可以包含类,函数等资源,让其他程序动态加载使用),通过反射机制加载这些DLL,主模块代码不作修改,热更新时用新的dll替换旧的dll,主模块加载dll就起到了更新的效果。不过c#反射只适用于Android热更新,iOS因为自身的安全机制,新申请的内存空间(用于修改后的代码使用)不允许进行写操作,所以目前c#的反射基本不被商业项目用作热更新。
二、热更新流程
1、在C#调用lua代码
1、下载热更新库(XLua,toLua等),导入Unity项目
2、创建基于Lua解释器的LuaMgr,用lua解释器来解释执行lua代码
3、在Unity中需要使用lua代码的地方用require(“文件名.lua”)的方式调用lua代码
2、更新lua代码
搭建一个ftp服务器,在需要进行更新检查的地方(如游戏主页面),连接到服务器,下载对比文件获取到文件名列表和md5码,与本地的对比文件进行对比
a、如果有新的文件名,则证明有新增文件,下载即可
b、如果文件名相同,md5码不同,则证明文件有改动,需要下载并覆盖本地文件
c、如果文件名和md5码完全一致,则证明不需要更新
3、Hotfix补丁
对于一开始用C#开发的项目,可以用热补丁的形式将C#代码里的方法替换为Lua里的方法
三、XLua中的C#Api
LuaEnv类
object[] DoString(string chunk, string chunkName = “chuck”, LuaTable env = null)
执行一个代码块。
T LoadString(string chunk, string chunkName = “chunk”, LuaTable env = null)
加载一个代码块,但不执行,只返回类型可以指定为一个delegate或者一个LuaFunction
LuaTable Global;
代表lua全局环境的LuaTable
void Tick()
清除Lua的未手动释放的LuaBase对象(比如:LuaTable, LuaFunction),以及其它一些事情。
需要定期调用,比如在MonoBehaviour的Update中调用。
void AddLoader(CustomLoader loader)
增加一个自定义loader
void Dispose()
Dispose该LuaEnv
LuaTable类
T Get(string key)
获取在key下,类型为T的value,如果不存在或者类型不匹配,返回null;
T GetInPath(string path)
和Get的区别是,这个函数会识别path里头的“.”,比如var i = tbl.GetInPath(“a.b.c”)相当于在lua里头执行i = tbl.a.b.c,避免仅为了获取中间变量而多次调用Get,执行效率更高。
void SetInPath(string path, T val)
和GetInPaht对应的setter;
void Get<TKey, TValue>(TKey key, out TValue value)
上面的API的Key都只能是string,而这个API无此限制;
void Set<TKey, TValue>(TKey key, TValue value)
对应Get<TKey, TValue>的setter;
T Cast()
把该table转成一个T指明的类型,可以是一个加了CSharpCallLua声明的interface,一个有默认构造函数的class或者struct,一个Dictionary,List等等。
void SetMetaTable(LuaTable metaTable)
设置metaTable为table的metatable
LuaFunction类
object[] Call(params object[] args)
以可变参数调用Lua函数,并返回该调用的返回值。
object[] Call(object[] args, Type[] returnTypes)
调用Lua函数,并指明返回参数的类型,系统会自动按指定类型进行转换。
void SetEnv(LuaTable env)
相当于lua的setfenv函数。
XLua.CSObjectWrap:用于将C#对象包装为Lua对象的工具类。
Register(Type type, LuaCSFunction creator):注册C#类型到Lua环境中。
CreateDelegate(LuaFunction func, Type delegateType):创建一个委托对象。
XLua.CSharpCallLuaAttribute:用于标记C#调用Lua函数的特性。
LuaCallCSharp:标记Lua函数可以被C#调用。
CSharpCallLua:标记C#函数可以被Lua调用。
四、LuaMgr类参考
其中ABMgr是我自己实现的一个加载AB包的管理器,原理不复杂,可以自行实现
public class LuaMgr : BaseManager<LuaMgr>
{
private LuaEnv luaEnv;
public LuaTable Global
{
get
{
return luaEnv.Global;
}
}
public void Init()
{
if (luaEnv != null)
{
return;
}
luaEnv = new LuaEnv();
luaEnv.AddLoader(MyLoader);
luaEnv.AddLoader(MyCustomABLoader);
}
public void DoLuaFile(string fileName)
{
string str = string.Format("require('{0}')", fileName);
DoString(str);
}
//自动执行
private byte[] MyLoader(ref string filePath)
{
string path = Application.dataPath + "/Lua/" + filePath + ".lua";
if (File.Exists(path))
{
return File.ReadAllBytes(path);
}
else
{
Debug.Log("文件重定向失败,文件名:"+filePath);
}
return null;
}
private byte[] MyCustomABLoader(ref string filePath)
{
TextAsset lua= ABMgr.GetInstance().LoadRes<TextAsset>("lua", filePath + ".lua");
if (lua != null)
{
return lua.bytes;
}
else
{
Debug.Log("文件重定向失败,文件名:"+filePath);
return null;
}
}
public void DoString(string str)
{
if (luaEnv == null)
{
Debug.Log("解析器未初始化");
return;
}
luaEnv.DoString(str);
}
public void Tick()
{
luaEnv.Tick();
}
public void Dispose()
{
luaEnv.Dispose();
luaEnv = null;
}
}
四、总结
本期文章有点短,本来想再分析下lua的底层原理的,发现有点看不懂(; ̄ー ̄川,就先写这些吧