使用XLua 与 Lua 热重载 (记录 3)
//
使用方式
1、打开该特性
添加HOTFIX_ENABLE宏,(在Unity3D的File->Build Setting->Scripting Define Symbols下添加)。编辑器、各手机平台这个宏要分别设置!如果是自动化打包,要注意在代码里头用API设置的宏是不生效的,需要在编辑器设置。
(建议平时开发业务代码不打开HOTFIXENABLE,只在build手机版本或者要在编译器下开发补丁时打开HOTFIXENABLE)
2、执行XLua/Generate Code菜单。
3、注入,构建手机包这个步骤会在构建时自动进行,编辑器下开发补丁需要手动执行"XLua/Hotfix Inject In Editor"菜单。打印“hotfix inject finish!”或者“had injected!”才算成功,否则会打印错误信息。
如果已经打印了“hotfix inject finish!”或者“had injected!”,执行xlua.hotfix仍然报类似“xlua.access, no field _Hitfix0Update”的错误,要么是该类没配置到Hotfix列表,要么是注入成功后,又触发了编译,覆盖了注入结果。
约束
不支持静态构造函数。
目前只支持Assets下代码的热补丁,不支持引擎,c#系统库的热补丁。
API
xlua.hotfix(class, [method_name], fix)
- 描述 : 注入lua补丁
- class : C#类,两种表示方法,CS.Namespace.TypeName或者字符串方式"Namespace.TypeName",字符串格式和C#的Type.GetType要求一致,如果是内嵌类型(Nested Type)是非Public类型的话,只能用字符串方式表示"Namespace.TypeName+NestedTypeName";
- method_name : 方法名,可选;
- fix : 如果传了methodname,fix将会是一个function,否则通过table提供一组函数。table的组织按key是methodname,value是function的方式。
base(csobj)
- 描述 : 子类override函数通过base调用父类实现。
- csobj : 对象
- 返回值 : 新对象,可以通过该对象base上的方法
例子(位于HotfixTest2.cs):
lua xlua.hotfix(CS.BaseTest, 'Foo', function(self, p) print('BaseTest', p) base(self):Foo(p) end)
util.hotfixex(class, methodname, fix)
- 描述 : xlua.hotfix的增强版本,可以在fix函数里头执行原来的函数,缺点是fix的执行会略慢。
- method_name : 方法名;
- fix : 用来替换C#方法的lua function。
标识要热更新的类型
和其它配置一样,有两种方式
方式一:直接在类里头打Hotfix标签(不建议,示例只是为了方便演示采取这种方式);
方式二:在一个static类的static字段或者属性里头配置一个列表。属性可以用于实现的比较复杂的配置,比如根据Namespace做白名单。
~~~csharp //如果涉及到Assembly-CSharp.dll之外的其它dll,如下代码需要放到Editor目录 public static class HotfixCfg { [Hotfix] public static List by_field = new List() { typeof(HotFixSubClass), typeof(GenericClass<>), };
[Hotfix]
public static List<Type> by_property
{
get
{
return (from type in Assembly.Load("Assembly-CSharp").GetTypes()
where type.Namespace == "XXXX"
select type).ToList();
}
}
} ~~~
Hotfix Flag
Hotfix标签可以设置一些标志位对生成代码及插桩定制化
- Stateless、Stateful
遗留设置,Stateful方式在新版本已经删除,因为这种方式可以用xlua.util.state接口达到类似的效果,该接口的使用可以看下HotfixTest2.cs里的示例代码。
由于没Stateful,默认就是Stateless,所以也没必要设置该标志位。
- ValueTypeBoxing
值类型的适配delegate会收敛到object,好处是代码量更少,不好的是值类型会产生boxing及gc,适用于对text段敏感的业务。
- IgnoreProperty
不对属性注入及生成适配代码,一般而言,大多数属性的实现都很简单,出错几率比较小,建议不注入。
- IgnoreNotPublic
不对非public的方法注入及生成适配代码。除了像MonoBehaviour那种会被反射调用的私有方法必须得注入,其它仅被本类调用的非public方法可以不注入,只不过修复时会工作量稍大,所有引用到这个函数的public方法都要重写。
- Inline
不生成适配delegate,直接在函数体注入处理代码。
- IntKey
不生成静态字段,而是把所有注入点放到一个数组集中管理。
好处:对text段影响小。
坏处:使用不像默认方式那么方便,需要通过id来指明hotfix哪个函数,而这个id是代码注入工具时分配的,函数到id的映射会保存在Gen/Resources/hotfixidmap.lua.txt,并且自动加时间戳备份到hotfixidmap.lua.txt同级目录,发布手机版本后请妥善保存该文件。
该文件的格式大概如下(注意:该文件仅IntKey模式使用,当你没类型指定IntKey模式注入,该文件只返回个空表):
~~~lua
return { ["HotfixTest"] = { [".ctor"] = { 5 }, ["Start"] = { 6 }, ["Update"] = { 7 }, ["FixedUpdate"] = { 8 }, ["Add"] = { 9,10 }, ["OnGUI"] = { 11 }, }, }
~~~
想要替换HotfixTest的Update函数,你得
~~~lua
CS.XLua.HotfixDelegateBridge.Set(7, func)
~~~
如果是重载函数,将会一个函数名对应多个id,比如上面的Add函数。
能不能自动化一些呢?可以,xlua.util提供了autoidmap函数,执行一次后你就可以像以前那样直接用类,方法名去指明修补的函数。
~~~lua
(require 'xlua.util').autoidmap() xlua.hotfix(CS.HotfixTest, 'Update', function(self) self.tick = self.tick + 1 if (self.tick % 50) == 0 then print('<<<<<<<<Update in lua, tick = ' .. self.tick) end end)
~~~
前提是hotfixidmap.lua.txt放到可以通过require 'hotfixidmap'引用到的地方。
使用建议
- 对所有较大可能变动的类型加上Hotfix标识;
- 建议用反射找出所有函数参数、字段、属性、事件涉及的delegate类型,标注CSharpCallLua;
- 业务代码、引擎API、系统API,需要在Lua补丁里头高性能访问的类型,加上LuaCallCSharp;
- 引擎API、系统API可能被代码剪裁调(C#无引用的地方都会被剪裁),如果觉得可能会新增C#代码之外的API调用,这些API所在的类型要么加LuaCallCSharp,要么加ReflectionUse;
打补丁
xlua可以用lua函数替换C#的构造函数,函数,属性,事件的替换。lua实现都是函数,比如属性对于一个getter函数和一个setter函数,事件对应一个add函数和一个remove函数。
- 函数
method_name传函数名,支持重载,不同重载都是转发到同一个lua函数。
比如:
```csharp
// 要fix的C#类 [Hotfix] public class HotfixCalc { public int Add(int a, int b) { return a - b; }
public Vector3 Add(Vector3 a, Vector3 b)
{
return a - b;
}
}
```
```lua
xlua.hotfix(CS.HotfixCalc, 'Add', function(self, a, b) return a + b end)
```
静态函数和成员函数的区别是,成员函数会加一个self参数,这个self在Stateless方式下是C#对象本身(对应C#的this)
普通参数对于lua的参数,ref参数对应lua的一个参数和一个返回值,out参数对于lua的一个返回值。
泛化函数的打补丁规则和普通函数一样。
- 构造函数
构造函数对应的method_name是".ctor"。
和普通函数不一样的是,构造函数的热补丁并不是替换,而是执行原有逻辑后调用lua。
- 属性
对于名为“AProp”的属性,会对应一个getter,methodname等于getAProp,setter的methodname等于setAProp。
- []操作符
赋值对应setItem,取值对应getItem。第一个参数是self,赋值后面跟key,value,取值只有key参数,返回值是取出的值。
- 其它操作符
C#的操作符都有一套内部表示,比如+号的操作符函数名是op_Addition(其它操作符的内部表示可以去请参照相关资料),覆盖这函数就覆盖了C#的+号操作符。
- 事件
比如对于事件“AEvent”,+=操作符是addAEvent,-=对应的是removeAEvent。这两个函数均是第一个参数是self,第二个参数是操作符后面跟的delegate。
通过xlua.privateaccessible(版本号大于2.1.11不需要调用xlua.privateaccessible)来直接访问事件对应的私有delegate的直接访问后,可以通过对象的"&事件名"字段直接触发事件,例如self['&MyEvent'](),其中MyEvent是事件名。
- 析构函数
method_name是"Finalize",传一个self参数。
和普通函数不一样的是,析构函数的热补丁并不是替换,而是开头调用lua函数后继续原有逻辑。
- 泛化类型
其它规则一致,需要说明的是,每个泛化类型实例化后都是一个独立的类型,只能针对实例化后的类型分别打补丁。比如:
csharp public class GenericClass<T> { }
你只能对GenericClass\<double>,GenericClass\<int>这些类,而不是对GenericClass打补丁。
对GenericClass打补丁的实例如下:
csharp luaenv.DoString(@" xlua.hotfix(CS.GenericClass(CS.System.Double), { ['.ctor'] = function(obj, a) print('GenericClass<double>', obj, a) end; Func1 = function(obj) print('GenericClass<double>.Func1', obj) end; Func2 = function(obj) print('GenericClass<double>.Func2', obj) return 1314 end }) ");
- Unity协程
通过util.cs_generator可以用一个function模拟一个IEnumerator,在里头用coroutine.yield,就类似C#里头的yield return。比如下面的C#代码和对应的hotfix代码是等同效果的
~~~csharp
[XLua.Hotfix] public class HotFixSubClass : MonoBehaviour { IEnumerator Start() { while (true) { yield return new WaitForSeconds(3); Debug.Log("Wait for 3 seconds"); } } }
~~~
~~~csharp
luaenv.DoString(@" local util = require 'xlua.util' xlua.hotfix(CS.HotFixSubClass,{ Start = function(self) return util.cs_generator(function() while true do coroutine.yield(CS.UnityEngine.WaitForSeconds(3)) print('Wait for 3 seconds') end end) end; }) ");
~~~
- 整个类
如果要替换整个类,不需要一次次的调用xlua.hotfix去替换,可以整个一次完成。只要给一个table,按method_name = function组织即可
```lua
xlua.hotfix(CS.StatefullTest, {
['.ctor'] = function(csobj) return util.state(csobj, {evt = {}, start = 0, prop = 0}) end;
setAProp = function(self, v) print('setAProp', v) self.prop = v end;
getAProp = function(self) return self.prop end;
getItem = function(self, k) print('getItem', k) return 1024 end;
setItem = function(self, k, v) print('setItem', k, v) end;
addAEvent = function(self, cb) print('add_AEvent', cb) table.insert(self.evt, cb) end;
removeAEvent = function(self, cb)
print('removeAEvent', cb)
for i, v in ipairs(self.evt) do
if v == cb then
table.remove(self.evt, i)
break
end
end
end;
Start = function(self)
print('Start')
for _, cb in ipairs(self.evt) do
cb(self.start, 2)
end
self.start = self.start + 1
end;
StaticFunc = function(a, b, c) print(a, b, c) end;
GenericTest = function(self, a) print(self, a) end; Finalize = function(self) print('Finalize', self) end
})```
///
namespace XLuaTest
{
[Hotfix]
public class HotfixTest : MonoBehaviour
{
LuaEnv luaenv = new LuaEnv();
private int tick = 0;
// Use this for initialization
void Start()
{
}
// Update is called once per frame
void Update()
{
if (++tick % 50 == 0)
{
Debug.Log(">>>>>>>>Update in C#, tick = " + tick);
}
}
void OnGUI()
{
if (GUI.Button(new Rect(10, 10, 300, 80), "Hotfix"))
{
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.HotfixTest, 'Update', function(self)
self.tick = self.tick + 1
if (self.tick % 50) == 0 then
print('<<<<<<<<Update in lua, tick = ' .. self.tick)
end
end)
");
}
string chHint = @"在运行该示例之前,请细致阅读xLua文档,并执行以下步骤:
1.宏定义:添加 HOTFIX_ENABLE 到 'Edit > Project Settings > Player > Other Settings > Scripting Define Symbols'。
(注意:各平台需要分别设置)
2.生成代码:执行 'XLua > Generate Code' 菜单,等待Unity编译完成。
3.注入:执行 'XLua > Hotfix Inject In Editor' 菜单。注入成功会打印 'hotfix inject finish!' 或者 'had injected!' 。";
string enHint = @"Read documents carefully before you run this example, then follow the steps below:
1. Define: Add 'HOTFIX_ENABLE' to 'Edit > Project Settings > Player > Other Settings > Scripting Define Symbols'.
(Note: Each platform needs to set this respectively)
2.Generate Code: Execute menu 'XLua > Generate Code', wait for Unity's compilation.
3.Inject: Execute menu 'XLua > Hotfix Inject In Editor'.There should be 'hotfix inject finish!' or 'had injected!' print in the Console if the Injection is successful.";
GUIStyle style = GUI.skin.textArea;
style.normal.textColor = Color.red;
style.fontSize = 16;
GUI.TextArea(new Rect(10, 100, 500, 290), chHint, style);
GUI.TextArea(new Rect(10, 400, 500, 290), enHint, style);
}
}
}
using UnityEngine;
using XLua;
namespace XLuaTest
{
[Hotfix]
public class HotfixCalc
{
public int Add(int a, int b)
{
return a - b;
}
public Vector3 Add(Vector3 a, Vector3 b)
{
return a - b;
}
public int TestOut(int a, out double b, ref string c)
{
b = a + 2;
c = "wrong version";
return a + 3;
}
public int TestOut(int a, out double b, ref string c, GameObject go)
{
return TestOut(a, out b, ref c);
}
public T Test1<T>()
{
return default(T);
}
public T1 Test2<T1, T2, T3>(T1 a, out T2 b, ref T3 c)
{
b = default(T2);
return a;
}
public static int Test3<T>(T a)
{
return 0;
}
public static void Test4<T>(T a)
{
}
public void Test5<T>(int a, params T[] arg)
{
}
}
public class NoHotfixCalc
{
public int Add(int a, int b)
{
return a + b;
}
}
[Hotfix]
public class GenericClass<T>
{
T a;
public GenericClass(T a)
{
this.a = a;
}
public void Func1()
{
Debug.Log("a=" + a);
}
public T Func2()
{
return default(T);
}
}
[Hotfix]
public class InnerTypeTest
{
public void Foo()
{
_InnerStruct ret = Bar();
Debug.Log("{x=" + ret.x + ",y= " + ret.y + "}");
}
struct _InnerStruct
{
public int x;
public int y;
}
_InnerStruct Bar()
{
return new _InnerStruct { x = 1, y = 2 };
}
}
public class BaseTestHelper
{
}
public class BaseTestBase<T> : BaseTestHelper
{
public virtual void Foo(int p)
{
Debug.Log("BaseTestBase<>.Foo, p = " + p);
}
}
[Hotfix]
[LuaCallCSharp]
public class BaseTest : BaseTestBase<InnerTypeTest>
{
public override void Foo(int p)
{
Debug.Log("BaseTest<>.Foo, p = " + p);
}
public void Proxy(int p)
{
base.Foo(p);
}
public override string ToString()
{
return base.ToString();
}
}
[Hotfix]
public struct StructTest
{
GameObject go;
public StructTest(GameObject go)
{
this.go = go;
}
public GameObject GetGo(int a, object b)
{
return go;
}
public override string ToString()
{
return base.ToString();
}
public string Proxy()
{
return base.ToString();
}
}
[Hotfix]
public struct GenericStruct<T>
{
T a;
public GenericStruct(T a)
{
this.a = a;
}
public T GetA(int p)
{
return a;
}
}
public class HotfixTest2 : MonoBehaviour
{
// Use this for initialization
void Start()
{
LuaEnv luaenv = new LuaEnv();
HotfixCalc calc = new HotfixCalc();
NoHotfixCalc ordinaryCalc = new NoHotfixCalc();
int CALL_TIME = 100 * 1000 * 1000;
var start = System.DateTime.Now;
for (int i = 0; i < CALL_TIME; i++)
{
calc.Add(2, 1);
}
var d1 = (System.DateTime.Now - start).TotalMilliseconds;
Debug.Log("Hotfix using:" + d1);
start = System.DateTime.Now;
for (int i = 0; i < CALL_TIME; i++)
{
ordinaryCalc.Add(2, 1);
}
var d2 = (System.DateTime.Now - start).TotalMilliseconds;
Debug.Log("No Hotfix using:" + d2);
Debug.Log("drop:" + ((d1 - d2) / d1));
Debug.Log("Before Fix: 2 + 1 = " + calc.Add(2, 1));
Debug.Log("Before Fix: Vector3(2, 3, 4) + Vector3(1, 2, 3) = " + calc.Add(new Vector3(2, 3, 4), new Vector3(1, 2, 3)));
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.HotfixCalc, 'Add', function(self, a, b)
return a + b
end)
");
Debug.Log("After Fix: 2 + 1 = " + calc.Add(2, 1));
Debug.Log("After Fix: Vector3(2, 3, 4) + Vector3(1, 2, 3) = " + calc.Add(new Vector3(2, 3, 4), new Vector3(1, 2, 3)));
double num;
string str = "hehe";
int ret = calc.TestOut(100, out num, ref str);
Debug.Log("ret = " + ret + ", num = " + num + ", str = " + str);
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.HotfixCalc, 'TestOut', function(self, a, c, go)
print('TestOut', self, a, c, go)
if go then error('test error') end
return a + 10, a + 20, 'right version'
end)
");
str = "hehe";
ret = calc.TestOut(100, out num, ref str);
Debug.Log("ret = " + ret + ", num = " + num + ", str = " + str);
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.HotfixCalc, {
Test1 = function(self)
print('Test1', self)
return 1
end;
Test2 = function(self, a, b)
print('Test1', self, a, b)
return a + 10, 1024, b
end;
Test3 = function(a)
print(a)
return 10
end;
Test4 = function(a)
print(a)
end;
Test5 = function(self, a, ...)
print('Test4', self, a, ...)
end
})
");
int r1 = calc.Test1<int>();
double r2 = calc.Test1<double>();
Debug.Log("r1:" + r1 + ",r2:" + r2);
string ss = "heihei";
int r3 = calc.Test2(r1, out r2, ref ss);
Debug.Log("r1:" + r1 + ",r2:" + r2 + ",r3:" + r3 + ",ss:" + ss);
r3 = HotfixCalc.Test3("test3");
r3 = HotfixCalc.Test3(2);
r3 = HotfixCalc.Test3(this);
Debug.Log("r3:" + r3);
HotfixCalc.Test4(this);
HotfixCalc.Test4(2);
calc.Test5(10, "a", "b", "c");
calc.Test5(10, 1, 3, 5);
Debug.Log("----------------------before------------------------");
TestStateful();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
luaenv.DoString(@"
local util = require 'xlua.util'
xlua.hotfix(CS.XLuaTest.StatefullTest, {
['.ctor'] = function(csobj)
util.state(csobj, {evt = {}, start = 0, prop = 0})
end;
set_AProp = function(self, v)
print('set_AProp', v)
self.prop = v
end;
get_AProp = function(self)
return self.prop
end;
get_Item = function(self, k)
print('get_Item', k)
return 1024
end;
set_Item = function(self, k, v)
print('set_Item', k, v)
end;
add_AEvent = function(self, cb)
print('add_AEvent', cb)
table.insert(self.evt, cb)
end;
remove_AEvent = function(self, cb)
print('remove_AEvent', cb)
for i, v in ipairs(self.evt) do
if v == cb then
table.remove(self.evt, i)
break
end
end
end;
Start = function(self)
print('Start')
for _, cb in ipairs(self.evt) do
cb(self.start, 2)
end
self.start = self.start + 1
end;
StaticFunc = function(a, b, c)
print(a, b, c)
end;
GenericTest = function(self, a)
print(self, a)
end;
Finalize = function(self)
print('Finalize', self)
end
})
");
Debug.Log("----------------------after------------------------");
TestStateful();
luaenv.FullGc();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
var genericObj = new GenericClass<double>(1.1);
genericObj.Func1();
Debug.Log(genericObj.Func2());
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.GenericClass(CS.System.Double), {
['.ctor'] = function(obj, a)
print('GenericClass<double>', obj, a)
end;
Func1 = function(obj)
print('GenericClass<double>.Func1', obj)
end;
Func2 = function(obj)
print('GenericClass<double>.Func2', obj)
return 1314
end
})
");
genericObj = new GenericClass<double>(1.1);
genericObj.Func1();
Debug.Log(genericObj.Func2());
InnerTypeTest itt = new InnerTypeTest();
itt.Foo();
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.InnerTypeTest, 'Bar', function(obj)
print('lua Bar', obj)
return {x = 10, y = 20}
end)
");
itt.Foo();
StructTest st = new StructTest(gameObject);
Debug.Log("go=" + st.GetGo(123, "john"));
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.StructTest, 'GetGo', function(self, a, b)
print('GetGo', self, a, b)
return nil
end)
");
Debug.Log("go=" + st.GetGo(123, "john"));
GenericStruct<int> gs = new GenericStruct<int>(1);
Debug.Log("gs.GetA()=" + gs.GetA(123));
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.GenericStruct(CS.System.Int32), 'GetA', function(self, a)
print('GetA',self, a)
return 789
end)
");
Debug.Log("gs.GetA()=" + gs.GetA(123));
try
{
calc.TestOut(100, out num, ref str, gameObject);
}
catch (LuaException e)
{
Debug.Log("throw in lua an catch in c# ok, e.Message:" + e.Message);
}
BaseTestBase<InnerTypeTest> bt = new BaseTest();
bt.Foo(1);
Debug.Log(bt);
luaenv.DoString(@"
xlua.hotfix(CS.XLuaTest.BaseTest, 'Foo', function(self, p)
print('BaseTest', p)
end)
xlua.hotfix(CS.XLuaTest.BaseTest, 'ToString', function(self)
return '>>>' .. base(self):ToString()
end)
");
bt.Foo(2);
Debug.Log(bt);
}
void TestStateful()
{
StatefullTest sft = new StatefullTest();
sft.AProp = 10;
Debug.Log("sft.AProp:" + sft.AProp);
sft["1"] = 1;
Debug.Log("sft['1']:" + sft["1"]);
System.Action<int, double> cb = (a, b) =>
{
Debug.Log("a:" + a + ",b:" + b);
};
sft.AEvent += cb;
sft.Start();
sft.Start();
sft.AEvent -= cb;
sft.Start();
StatefullTest.StaticFunc(1, 2);
StatefullTest.StaticFunc("e", 3, 4);
sft.GenericTest(1);
sft.GenericTest("hehe");
}
// Update is called once per frame
void Update()
{
}
}
}
using UnityEngine;
namespace XLuaTest
{
[XLua.Hotfix]
public class StatefullTest
{
public StatefullTest()
{
}
public StatefullTest(int a, int b)
{
if (a > 0)
{
return;
}
Debug.Log("a=" + a);
if (b > 0)
{
return;
}
else
{
if (a + b > 0)
{
return;
}
}
Debug.Log("b=" + b);
}
public int AProp
{
get;
set;
}
public event System.Action<int, double> AEvent;
public int this[string field]
{
get
{
return 1;
}
set
{
}
}
public void Start()
{
}
void Update()
{
}
public void GenericTest<T>(T a)
{
}
static public void StaticFunc(int a, int b)
{
}
static public void StaticFunc(string a, int b, int c)
{
}
~StatefullTest()
{
Debug.Log("~StatefullTest");
}
}
}
/
//
/