1.Xlua的配置应用
xLua所有的配置都支持三种方式:打标签;静态列表;动态列表。
配置要求:
列表方式均必须是static的字段/属性
列表方式均必须放到一个static类
建议不用标签方式
建议列表方式配置放Editor目录(如果是Hotfix配置,而且类位于Assembly-CSharp.dll之外的其它dll,必须放Editor目录)
1.打标签
xLua用白名单来指明生成哪些代码,而白名单通过attribute来配置,该方式方便,但在il2cpp下会增加不少的代码量,不建议使用。
1.LuaCallCSharp
一个C#类型加了这个配置,xLua会生成这个类型的适配代码(包括构造该类型实例,访问其成员属性、方法,静态属性、方法)。一个类型的扩展方法(Extension Methods)加了这配置,也会生成适配代码并追加到被扩展类型的成员方法上。xLua只会生成加了该配置的类型,不会自动生成其父类的适配代码,当访问子类对象的父类方法,如果该父类加了LuaCallCSharp配置,则执行父类的适配代码。如果没有适配代码则会尝试用反射来访问,但反射访问性能不佳,在il2cpp下有可能因为代码剪裁而导致无法访问。
1.打标签
[LuaCallCSharp]
public class TestObj
{public void test(testStruct t){Debug.Log($"{t.a},{t.c}");}
}
2.静态列表
[LuaCallCSharp]public static List<Type> luaCallCsharp = new List<Type>{typeof(TestObj),};
上面的类加了此标签,就会生成适配代码
2.ReflectionUse
一个C#类型类型加了这个配置,xLua会生成link.xml阻止il2cpp的代码剪裁。对于扩展方法,必须加上 LuaCallCSharp 或者 ReflectionUse 才可以被访问到。
建议所有要在Lua访问的类型,要么加LuaCallCSharp,要么加上ReflectionUse,这才能够保证在各平台都能正常运行。
如何配置?
[ReflectionUse]
public class TestReflectionUseObj
{}
link.xml保存着打上LuaCallCSharp和ReflectionUse标签的类
3.DoNotGen
指明一个类里头的部分函数、字段、属性不生成代码,通过反射访问。
只能标准 Dictionary<Type, List<string>> 的field或者property。key指明的是生效的类,value是一个列表,配置的是不生成代码的函数、字段、属性的名字。
如何配置?
[LuaCallCSharp]
public class TestDontGen
{public int a1;public int a2;
}
1.打标签(这种方式行不通)
在生成代码中,a2还是生成和a1一样的适配函数
2.静态列表
public static class ExportCfg
{[DoNotGen]public static Dictionary<Type,List<string>> dontGenDic = new Dictionary<Type, List<string>> {[typeof(TestDontGen)] = new List<string> { "a2" },};
}
4.CSharpCallLua
把一个lua函数适配到一个C# delegate(一类是C#侧各种回调:UI事件,delegate参数,比如List<T>:ForEach;另外一类场景是通过LuaTable的Get函数指明一个lua函数绑定到一个delegate),或者把一个lua table适配到一个C# interface时,使用该标签。
如何配置?
1.打标签:
[CSharpCallLua]public interface IPlayerPosition{int x { get; set; }int y { get; set; }int z { get; set; }void add(int x, int y, int z);void sub(int x, int y, int z);}[CSharpCallLua]public delegate void AddMethod(LuaTable self, int x, int y, int z);[CSharpCallLua]public delegate Action addAc(LuaTable t, int x, int y, int z);[CSharpCallLua]public delegate void test1(int x);[CSharpCallLua]public delegate Action test2(int x);
2.静态列表:
[CSharpCallLua]
public static List<Type> CsharpCallLua = new List<Type> { typeof(IPlayerPosition),typeof(AddMethod),typeof(addAc),typeof(test1),typeof(test2),
};
生成代码如下:接口生成独立文件,委托事件生成在DelegateGenBridge文件中
5.GCOptimize
xLua无参构造函数的复杂类型(struct)的默认传递方式是引用传递,当lua侧使用完毕释放该对象的引用时,会产生一次gc。
一个C#纯值类型(注:指的是一个只包含值类型的struct,可以嵌套其它只包含值类型的struct)或者C#枚举值加上这个标签之后,XLua会生成gc优化代码,在lua和c#间传递将不产生(C#)gc alloc,该类型的数组(一维?)访问也不产生gc。
如果没有配置该标签的值类型传递
public class Test{ [CSharpCallLua]public delegate Action testAction(TestGCOptimizeValue x);void Start(){luaenv = new LuaEnv();luaenv.AddLoader(customLoader);luaenv.DoString("require 'main'");ta = luaenv.Global.Get<testAction>("testAction");}private void Update(){if (Application.isPlaying){ta?.Invoke(ts);}}
}public struct TestGCOptimizeValue
{public int a;public int b;private float f;//[AdditionalProperties]public float F { get => f; set => f = value; }
}
生成的代码如下:
将struct类型装箱转换为object再进行处理。
如何配置?
[GCOptimize]
public struct TestGCOptimizeValue
{public int a;public int b;public float f;
}
加上此标签后,就会在PackUnpack中生成适配代码
然后调用函数生成代码如下:
struct 满足条件如下:
1.struct允许嵌套其它struct,但它以及它嵌套的struct只能包含这几种基本类型:byte、sbyte、short、ushort、int、uint、long、ulong、float、double;例如UnityEngine定义的大多数值类型:Vector系列,Quaternion,Color。。。均满足条件,或者用户自定义的一些struct
2.该struct配置了GCOptimize属性(对于常用的UnityEngine的几个struct,Vector系列,Quaternion,Color。。。均已经配置了该属性),这个属性可以通过配置文件或者C# Attribute实现;
3.使用到该struct的地方,需要添加到生成代码列表;
6.AdditionalProperties
该标签GCOptimize的扩展配置,如果struct有的字段是私有的,需要通过属性来访问,这时就需要用到该配置(默认情况下GCOptimize只对public的field打解包)
如何配置?
如果不加此标签
[GCOptimize]
public struct TestGCOptimizeValue
{public int a;public int b;private float f;public float F { get => f; set => f = value; }
}
生成代码如下:无法生成访问f字段的代码
1.打标签方式
[GCOptimize]
public struct TestGCOptimizeValue
{public int a;public int b;private float f;[AdditionalProperties]public float F { get => f; set => f = value; }
}
2.静态列表
[AdditionalProperties]static Dictionary<Type, List<string>> AdditionalProperties = new Dictionary<Type, List<string>>{[typeof(TestGCOptimizeValue)] = new List<string> { "F" },};
生成代码如下:
7.BlackList
一个类型的一些成员不需要适配代码,可以通过加上该标签来实现。考虑到有可能需要把重载函数的其中一个重载列入黑名单,配置方式比较复杂,类型是 List<List<string>>,对于每个成员,在第一层List有一个条目,第二层List是个string的列表,第一个string是类型的全路径名,第二个string是成员名,如果成员是一个方法,还需要从第三个string开始,把其参数的类型全路径全列出来。
如何配置?
[LuaCallCSharp]
public class TestBlackList
{public int a1;public int a2;public void Test(){}
}
若不加处理生成代码如下:
1.打标签
C#代码:
[LuaCallCSharp]
public class TestBlackList
{public int a1;[BlackList]public int a2;[BlackList]public void Test(){}
}
生成代码如下:
2.静态列表
列表中的第一个字符串是类型,第二个及以后是要屏蔽的字段
[BlackList]public static List<List<string>> BlackList = new List<List<string>>(){new List<string>(){ "TestBlackList", "a2"},new List<string>(){ "TestBlackList", "Test"}};
生成代码:
注:
DoNotGen和ReflectionUse的区别是:1、ReflectionUse指明的是整个类;2、当第一次访问一个函数(字段、属性)时,ReflectionUse会把整个类都wrap,而DoNotGen只wrap该函数(字段、属性);
DoNotGen和BlackList的区别是:1、BlackList配了就不能用;2、BlackList能指明某重载函数,DoNotGen不能;
2.静态列表
有时我们无法直接给一个类型打标签,比如系统api,没源码的库,或者实例化的泛化类型,这时可以在一个静态类里声明一个静态字段,然后为这字段加上标签,并且这个字段需要放到一个静态类里,建议放到 Editor目录。
如何配置?
public static class ExportCfg
{[DoNotGen]public static Dictionary<Type, List<string>> dontGenDic = new Dictionary<Type, List<string>>{[typeof(TestDontGen)] = new List<string> { "a2" },};[BlackList]public static List<List<string>> BlackList = new List<List<string>>(){new List<string>(){ "TestBlackList", "a2"},new List<string>(){ "TestBlackList", "Test"}};[AdditionalProperties]public static Dictionary<Type, List<string>> AdditionalProperties = new Dictionary<Type, List<string>>{[typeof(TestGCOptimizeValue)] = new List<string> { "F" },};[LuaCallCSharp]public static List<Type> luaCallCsharp = new List<Type>{typeof(TestObj),};[CSharpCallLua]public static List<Type> CsharpCallLua = new List<Type> { typeof(IPlayerPosition),typeof(AddMethod),typeof(addAc),typeof(test1),typeof(test2),};
}
3.动态列表
声明一个静态属性,打上相应的标签即可。
见HotFix。
2.HotFix
1.标识要热更新的类型
1.打标签:(不建议,在高版本 Unity 不支持)
[Hotfix]
public class TestHotFix
{public void TestPrint(){Debug.Log("Before Hotfix");}
}
2.static列表:(高版本 Unity 需要把配置文件放 Editor 目录下)
[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]
public static List<Type> by_field = new List<Type>()
{typeof(HotFixSubClass),typeof(GenericClass<>),
};
2.API
1.xlua.hotfix(class, [method_name], fix)
class :C#类,CS.Namespace.TypeName或者字符串方式"Namespace.TypeName"表示,字符串格式和C#的Type.GetType要求一致,如果是内嵌类型(Nested Type)是非Public类型,只能用字符串方式表示"Namespace.TypeName+NestedTypeName";
method_name : 方法名,可选;
fix : 如果传了method_name,fix将会是一个function,否则通过table提供一组函数。table的组织按key是method_name,value是function的方式。
2.base(csobj)
子类override函数通过base调用父类实现。
csobj : 对象
返回值 : 新对象,可以通过该对象base上的方法
3.util.hotfix_ex(class, method_name, fix)
可以在fix函数里头执行原来的函数,缺点是fix的执行会略慢。
method_name : 方法名;
fix : 用来替换C#方法的lua function
Lua侧示例:
xlua.hotfix(CS.BaseTest, 'Foo', function(self, p)print('BaseTest', p)base(self):Foo(p)
end)
3.Hotfix Flag
使用方式:
[Hotfix(HotfixFlag.xxxx)]
public class TestHotFixClass
{}
1.Stateless、Stateful
遗留设置,Stateful 方式在新版本已经删除,使用xlua.util.state代替,默认为Stateless。
这个东西不知道是个啥玩意,从utile.state的代码和Xlua例子的代码来看,就是可以为热修复类新增一些属性事件类似的东西,在构造函数中指给Lua侧的当前类(table)一个父类(metatable)。
debug.setmetatable:
rawget,rawset:获取或设置table的值,不从原表获取或者设置
Lua 5.4 Reference Manual
使用方式:
xlua.hotfix(CS.TestHotFixClass, { ['.ctor'] = function(self)util.state(self, {evt = {}})end;
})
2.ValueTypeBoxing
值类型的适配delegate会收敛到object(这是个啥?),好处是代码量更少,不好的是值类型会产生装箱,适用于对字段敏感的业务。
就是在生成delegate的时候,
如果是HotFix标签中包含ValueTypeBoxing,就会将方法和参数的类型转换为object类型
3.IgnoreProperty
不对属性注入及生成适配代码。
4.IgnoreNotPublic
不对非public的方法注入及生成适配代码。
5.Inline
不生成适配 delegate,直接在函数体注入处理代码。
6.IntKey
不生成静态字段,而是把所有注入点放到一个数组集中管理。
好处:对字段影响小。
坏处:使用不像默认方式那么方便,需要通过id来指明hotfix哪个函数,而这个id是代码注入工具时分配的,函数到id的映射会保存在 Gen/Resources/hotfix_id_map.lua.txt,并且自动加时间戳备份到 hotfix_id_map.lua.txt 同级目录,发布手机版本后请妥善保存该文件。
C#代码:
[Hotfix(HotfixFlag.IntKey)]
public class TestHotFixInkey
{public int A;public bool B; public string C;public int this[string field]{get { return 1; }set { }}public void EventCall(){Debug.Log("EventCall");}public void EventCall(int a){Debug.Log("EventCall a");}public void test(){Debug.Log("cccccccccc");}
}
生成的如下:重载函数,将会一个函数名对应多个 id
要替换掉test函数
local a = function(self)print("Lua EventCall2")
endCS.XLua.HotfixDelegateBridge.Set(4, a)local tik = CS.TestHotFixInkey()
tik:test()
结果:
xlua.util 提供了 auto_id_map 函数,执行一次后你就可以像以前那样直接用类,方法名去指明修补的函数。
local a = function(self)print("Lua EventCall2")
endutil.auto_id_map()
xlua.hotfix(CS.TestHotFixInkey, 'test', a)local tik = CS.TestHotFixInkey()
tik:test()
结果:
注意:
auto_id_map函数会require hot_id_map
一定要确保生成的hot_id_map能被加载到,如果你的自定义loader加载的文件后缀是.txt,就要确保hot_id_map.lua.txt能被加载到,如果是.lua则要确保hot_id_map.lua能被加载到。否则就会报下面的错误:
4.打补丁
Xlua可以用lua函数替换 C# 的构造函数,函数,属性,事件。
1.函数
method_name 传函数名,支持重载,不同重载都是转发到同一个 lua 函数。
见Unity中 Xlua使用整理(一)_为xlua配置搜索路径-CSDN博客中的HotFix章节
2.构造函数
构造函数对应的 method_name 是 ".ctor"。和普通函数不一样的是,构造函数的热补丁是执行原有逻辑后调用lua。
C#侧代码:
[Hotfix]
[LuaCallCSharp]
public class TestHotFixClass
{public TestHotFixClass(){Debug.Log("c# .ctor");}public TestHotFixClass(int p, int c){Debug.Log($"C# .ctor {p} , {c}");}
}
调用代码:
//调用代码:
luaenv.DoString("require 'main'");
TestHotFixClass testHotFixClass = new TestHotFixClass();
TestHotFixClass testHotFixClass1 = new TestHotFixClass(10, 20);
Lua侧代码:
xlua.hotfix(CS.TestHotFixClass, '.ctor' , function(self,p,c,d)print("Lua .ctor",p,c,d)
end)
结果:
3.属性
method_name 等于 get_属性名,setter 的 method_name 等于 set_属性名。
C#侧代码:
[Hotfix]
[LuaCallCSharp]
public class TestHotFixClass
{private int testProperty;public int TestProperty{ get {return testProperty;} set {testProperty = value;Debug.Log($"c# set {testProperty}");} }
}
调用代码:
TestHotFixClass testHotFixClass = new TestHotFixClass();luaenv = new LuaEnv();luaenv.AddLoader(customLoader);testHotFixClass.TestProperty = 10;Debug.Log($"before get {testHotFixClass.TestProperty}");luaenv.DoString("require 'main'");Debug.Log($"after get {testHotFixClass.TestProperty}");testHotFixClass.TestProperty = 100;Debug.Log($"set after get {testHotFixClass.TestProperty}");
Lua侧代码:
xlua.hotfix(CS.TestHotFixClass, { set_TestProperty = function(self, v)self.testProperty = vprint('Lua set_TestProperty', v)end;get_TestProperty = function(self)print('Lua get_TestProperty')return self.testPropertyend
})
结果:
4.[]操作符
赋值对应 set_Item,取值对应 get_Item。第一个参数是 self,赋值后面跟 key,value,取值只有 key 参数,返回值是取出的值。
C#侧代码:
public int this[string field]{get{ return 1;}set{}}public string this[int index]{get{ return "aaabbbb";}set{}}
C#侧调用代码:
Debug.Log(testHotFixClass[1]);Debug.Log(testHotFixClass["cc"]);luaenv.DoString("require 'main'");Debug.Log(testHotFixClass[1]);Debug.Log(testHotFixClass["cc"]);
Lua侧代码:
xlua.hotfix(CS.TestHotFixClass, { get_Item = function(self, k)print('get_Item', k)return 1024end;set_Item = function(self, k, v)print('set_Item', k, v)end;
})
结果:
5.其他操作符
使用C#的内部操作符可以覆盖对应函数,比如+号的操作符函数名是 op_Addition,
操作符还有哪些可以看下边这篇文章
C#中如何利用操作符重载和转换操作符 (转载) - PowerCoder - 博客园 (cnblogs.com)
C#代码:
[Hotfix]
[LuaCallCSharp]
public class HotFixOtherOperator
{public static int operator +(HotFixOtherOperator a, HotFixOtherOperator b){Debug.Log("C# op_add");return 0;}
}
调用代码:
HotFixOtherOperator ho1 = new HotFixOtherOperator();HotFixOtherOperator ho2 = new HotFixOtherOperator();int a = ho1 + ho2;luaenv.DoString("require 'main'");
Lua代码:
xlua.hotfix(CS.HotFixOtherOperator,"op_Addition",function(a,b)print("Lua Op_add")return 0end)local ho1 = CS.HotFixOtherOperator()
local ho2 = CS.HotFixOtherOperator()
local a = ho1 + ho2
结果:
6.事件
+= 操作符是 add_事件名,-=对应的是 remove_事件名。这两个函数均是第一个参数是self,第二个参数是操作符后面跟的delegate。
C#代码:
[Hotfix]
[LuaCallCSharp]
public class TestHotFixClass
{public event Action myEvent;
}
调用代码:
luaenv.DoString("require 'main'");TestHotFixClass testHotFixClass = new TestHotFixClass();testHotFixClass.myEvent += TestHotfixEvent;testHotFixClass.myEvent -= TestHotfixEvent;
Lua代码:
-- 事件
xlua.hotfix(CS.TestHotFixClass, { ['.ctor'] = function(self)util.state(self, {evt = {}})end;add_myEvent = function(self, cb)print('add_Event', cb)table.insert(self.evt, cb)end;remove_myEvent = function(self, cb)print('remove_Event', cb)for i, v in ipairs(self.evt) doif v == cb thentable.remove(self.evt, i)breakendendend;
})
结果:
通过 xlua.private_accessible(版本号大于2.1.11不需要调用 xlua.private_accessible)来直接访问,事件对应的私有 delegate 的直接访问后,可以通过对象的"&事件名"字段直接触发事件,例如 self['&MyEvent'](),其中MyEvent是事件名。
7.析构函数
method_name 是 "Finalize",传一个 self 参数。析构函数的热补丁并不是替换,而是开头调用 lua 函数后继续原有逻辑。
C#侧代码:
[Hotfix]
[LuaCallCSharp]
public class TestHotFixClass
{~TestHotFixClass(){Debug.Log("执行析构函数");}
}
调用代码:
TestHotFixClass testHotFixClass = new TestHotFixClass();luaenv = new LuaEnv();luaenv.AddLoader(customLoader);System.GC.Collect();System.GC.WaitForPendingFinalizers();luaenv.DoString("require 'main'");TestHotFixClass testHotFixClass1 = new TestHotFixClass();luaenv.FullGc();System.GC.Collect();System.GC.WaitForPendingFinalizers();
Lua侧代码:
xlua.hotfix(CS.TestHotFixClass,"Finalize", function(self)print('Finalize', self)
end)
结果:
注:System.GC.Collect()
System.GC.WaitForPendingFinalizers
luaenv.FullGC():清理GC
8.泛型
其他规则一致,每个泛化类型实例化后都是一个独立的类型,只能针对实例化后的类型分别打补丁。
C#代码:
[Hotfix]
public class TestGeneric<T>
{public TestGeneric(){Debug.Log($"aaaaa,{typeof(T)}");}
}
调用代码:
TestGeneric<int> testGeneric = new TestGeneric<int>();
TestGeneric<float> testGeneric1 = new TestGeneric<float>();
luaenv.DoString("require 'main'");
TestGeneric<int> testGeneric2 = new TestGeneric<int>();
TestGeneric<float> testGeneric3 = new TestGeneric<float>();
Lua代码:
xlua.hotfix(CS.TestGeneric(CS.System.Int32),".ctor", function(self)print("LUA INT32")
end)xlua.hotfix(CS.TestGeneric(CS.System.Single),".ctor", function(self)print("LUA FLAOTG4")
end)
结果:
如果Lua侧没有泛型的单独函数不会报错,只会执行C#侧的泛型函数
TestGeneric<double> testGeneric2 = new TestGeneric<double>();
9.Unity协程
通过 util.cs_generator 可以用一个 function 模拟一个 IEnumerator,yield return使用在里头的 coroutine.yield模拟。
C#代码:
[Hotfix]
public class HotFixSubClass : MonoBehaviour
{IEnumerator Start(){while (true){yield return new WaitForSeconds(3);Debug.Log("C# Wait for 3 seconds");}}
}
调用代码:
Unity协程的使用请看3.1节。
C#代码:
[Hotfix]
public class HotFixCoroutine: MonoBehaviour
{private void Start(){StartCoroutine(CoroutineStart());}IEnumerator CoroutineStart(){while (true){yield return new WaitForSeconds(1);Debug.Log("c# Wait for 3 seconds");}}
}
调用代码:
new GameObject().AddComponent<HotFixCoroutine>();
luaenv.DoString("require 'main'");
new GameObject().AddComponent<HotFixCoroutine>();
Lua代码:
xlua.hotfix(CS.HotFixCoroutine,{CoroutineStart = function(self)return util.cs_generator(function()while true docoroutine.yield(CS.UnityEngine.WaitForSeconds(3))print('Wait for 3 seconds')endend)end;
})
结果:
10.整个类
如果要替换整个类,使用一个 table,按 method_name = function 组织即可。
注:将打补丁1-9小节挑选合适的填入table({})中
xlua.hotfix(CS.类名, { --放入要热修复的代码
})
3.其他
1.Unity协程的使用
C#代码:
新建一个MonoBehaviour,
public class TestCoroutine : MonoBehaviour{}
添加WaitForSeconds到LuaCallCSshrap的静态列表中,供Lua侧调用
Lua代码:
local gameobject = CS.UnityEngine.GameObject('CoroutineTest')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.TestCoroutine))local times = 5
cs_coroutine_runner:StartCoroutine(util.cs_generator(function()while (times > 0) doprint('coroutine aaaaa')coroutine.yield(CS.UnityEngine.WaitForSeconds(1))times = times - 1end
end))
结果:
参考链接:
介绍 — XLua (tencent.github.io)