如何使用lua做热更新的
Unity引擎如何热更新了,现在可以选择的方案可是越来越多了。笔者收集到一下几个解决solution:
ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现的,快速、方便并且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新。
tolua# 不支持动态反射。动态反射对于重载函数有参数匹配问题,函数排序问题,ref,out 参数问题等等。
slua 偏向面向对象, 腾讯内部有个潘多拉的SDK项目组热更新采用的就是slua架构。
xlua是由腾讯维护的一个开源项目,除了常规的Lua绑定之外,还有一个比较有特色的功能就是代码热补丁。非常适合前期没有规划使用Lua进行逻辑开发,后期又需要在iOS这种平台获得代码热更新能力的项目。
下图给出ulua官网给出的性能对比:
详细对比各个solution效率测试, 参考下面这篇文章:Unity中SLua、Tolua、XLua和ILRuntime效率评测
龙之谷使用的是基于ulua的热更方案,就目前手游市场使用的情况来看,《王者荣耀》apk包破解之后,也是基于此方案。
lua 做热修和开发小功能
龙之谷使用ulua做了哪些事:
- 热修线上功能
- 开发小功能
所有上面的实现都是基于埋点来实现的。比如说我们在发布前在每个view的显示函数(基类的虚函数)或者隐藏的地方,埋下如下所示的点:
ILuaEngine luaEngine = XUpdater.XUpdater.singleton.XLuaEngine;
if (!luaEngine.hotfixMgr.TryFixRefresh(Mode.BEFORE, luaFileName, uiBehaviour.gameObject))
{
OnShow();
luaEngine.hotfixMgr.TryFixRefresh(Mode.AFTER, luaFileName, uiBehaviour.gameObject);
}
else
{
OnHide();
UIManager.singleton.OnDlgHide(s_instance);
ILuaEngine luaEngine = XUpdater.XUpdater.singleton.XLuaEngine;
luaEngine.hotfixMgr.TryFixRefresh(Mode.HIDE, luaFileName, uiBehaviour.gameObject);
}
OnShow和OnHide是每个界面的显示、隐藏函数。这样的话,每次刷新的UI都先去检查是否存在相应的lua热更文件,如果存在的话,则跳入对应lua的函数入口。具体的实现如下:
public bool TryFixRefresh(Mode _mode, string _pageName, GameObject go)
{
if (useHotfix && init)
{
string filename = "Hotfix" + _pageName + ".lua";
bool dolua = DoLuaFile(filename);
if (dolua)
{
_refresh = null;
if (_mode == Mode.BEFORE) _refresh = hot.lua.GetFunction(_pageName + ".BeforeRefresh");// : _pageName + ".AfterRefresh");
else if (_mode == Mode.AFTER) _refresh = hot.lua.GetFunction(_pageName + ".AfterRefresh");
else if (_mode == Mode.HIDE) _refresh = hot.lua.GetFunction(_pageName + ".Hide");
else if (_mode == Mode.UNLOAD) _refresh = hot.lua.GetFunction(_pageName + ".Unload");
if (_refresh != null)
{
object[] r = _refresh.Call(go);
_refresh.Release();
return r != null && r.Length > 0 ? (bool)r[0] : false;
}
else
{
Debug.Log("func is null!" + _pageName + " mode: " + _mode);
}
}
}
return false;
}
而如果有新的lua文件,则会跳入相应的lua函数中,lua的实现如下:
TestDlg = {}
local this = TestDlg
local m_go
function TestDlg.BeforeRefresh(go)
return false
end
function TestDlg.AfterRefresh(go)
if not IsNil(go) then
m_go = go;
print(str)
else
print("AfterRefresh: TestDlg.AfterRefresh is nil lua script ")
end
return false
end
function TestDlg.Hide(go)
return false
end
function TestDlg.Unload(go)
return false
end
lua 模板内置的函数诸如BeforeRefresh,AfterRefresh,Unload这些函数都会带有返回值,如果返回false,则是不覆盖之前的c#的逻辑,如果返回true,则是覆盖原来c#写的代码。类似的原理,我们还可以重载掉所有的c#里的click事件,覆盖所有的c#里网络协议。
Lua做新功能
用lua去实现一个新的系统
原理:
LuaUIManager:Load(“UI/GameSystem/Prefab”)
LuaUIManager:Destroy(“UI/GameSystem/Prefab”)
会加载一个通用的LuaDlg, 这样就实现了lua脚本和monobehaviour一样的生命周期和调用方式
脚本命名Lua+Prefab名字 (Prefab的首字母要大些)
脚本里有一个table, table的名字跟脚本名要相同 所有方法放在table中
有Awake、Start、OnEnable、OnDisable、OnShow、OnDestroy等接口
实现跟c#的类似,luadlg.cs 实现如下:
using LuaInterface;
using System.Text;
using UnityEngine;
public class LuaDlg : MonoBehaviour
{
private LuaScriptMgr mgr;
private string m_name { get { return name.Substring(0, 1).ToUpper() + name.Substring(1); } }
private const string AWAKE = "Awake";
private const string START = "Start";
private const string ENABLE = "OnEnable";
private const string DISABLE = "OnDisable";
private const string DESTROY = "OnDestroy";
void Awake()
{
mgr = HotfixManager.GetLuaScriptMgr();
mgr.DoFile("Lua" + m_name + ".lua");
LuaFunction func = mgr.GetLuaFunction(SPend(AWAKE));
if (func != null) func.Call(gameObject);
}
void Start()
{
if (mgr != null)
{
LuaFunction func = mgr.GetLuaFunction(SPend(START));
if (func != null) func.Call();
}
}
void OnEnable()
{
if (mgr != null)
{
LuaFunction func = mgr.GetLuaFunction(SPend(ENABLE));
if (func != null) func.Call();
}
}
void OnDisable()
{
if (mgr != null)
{
LuaFunction func = mgr.GetLuaFunction(SPend(DISABLE));
if (func != null) func.Call();
}
}
public void OnDestroy()
{
if (mgr != null)
{
try
{
LuaFunction func = mgr.GetLuaFunction(SPend(DESTROY));
if (func != null) func.Call();
}catch { }
}
}
private string SPend(string func)
{
StringBuilder sb = new StringBuilder("Lua");
sb.Append(m_name);
sb.Append(".");
sb.Append(func);
return sb.ToString();
}
}
xxxx_pb协议解析
协议解析文件 由protoc-gen-lua生成(不要手动编辑)
Protobuff不能解析ulong long 请转成string
PTC在LuaNotifyRoute里的PTC、PtcCB、NetworkOverideCSharp 注册(区别请去看注释) 注册一条协议包括 协议号、回调方法、协议名
RPC的发送:Hotfix.SendLuaRPC(46227, TestProtol.data, this.CB1, this.CB2)
CB1是网络回调 CB2是超时回调
IL注入,重写c#逻辑
借鉴于xlua可以利用IL注入的方式实现原c#代码覆盖,既不污染c#代码,也不需要埋点。现已在游戏中实现类似的功能。
使用方式:
在新的功能模块中的类名或者方法名加上[Hotfix]标签 而如果忽略某个函数可以使用[HotfixIgnore]
如果线上对应的代码出现问题,可以在HotfixPatch.lua 的Regist函数注册你要重载的函数,在HotfixCallback.lua实现要重载的函数
注意事项:
- 构造函数不能被热修
- 参数含有ref、out的不能被热修
- 加入[Hotfix]标签后执行LuaTools->Injector->Inject来注入,然后在编辑器里运行,确保注入成功没有错误后再执行LuaTools->Injector->Clean来清除IL注入。切记注入之后,不要上传dll,上传之前一定要清掉,避免污染代码
- 原先已经成熟的模块就不要在注入了 因为注入会增加额外的代码量
- 考虑到性能的问题 最好Update方法都主动加一个[HotfixIgnore] 不去热修