375 lines
12 KiB
Markdown
375 lines
12 KiB
Markdown
## 简介
|
||
|
||
热补丁允许你使用 xLua 的代码逻辑, 替换掉原有的 C# 程序逻辑, 以实现热补丁.
|
||
|
||
## 使用方式
|
||
|
||
1. 打开该特性
|
||
|
||
添加 `HOTFIX_ENABLE` 宏,(在 Unity3D 的 "File->Build Setting->Scripting Define Symbols" 下添加)。编辑器、各手机平台这个宏要分别设置!如果是自动化打包,要注意在代码里头用 API 设置的宏是不生效的,需要在编辑器设置。
|
||
|
||
(建议平时开发业务代码不打开 `HOTFIX_ENABLE`,只在构建手机版本或者要在编译器下开发补丁时打开 `HOTFIX_ENABLE`)
|
||
|
||
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 __Hitfix0_Update” 的错误,要么是该类没配置到 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 : 如果传了method_name,fix将会是一个function,否则通过table提供一组函数。table的组织按key是method_name,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.hotfix_ex(class, method_name, fix)`
|
||
|
||
* 描述 : xlua.hotfix的增强版本,可以在fix函数里头执行原来的函数,缺点是fix的执行会略慢。
|
||
* method_name : 方法名;
|
||
* fix : 用来替换C#方法的lua function。
|
||
|
||
## 标识要热更新的类型
|
||
|
||
和其它配置一样,有两种方式
|
||
|
||
方式一:直接在类里头打Hotfix标签(不建议,示例只是为了方便演示采取这种方式);
|
||
|
||
!!注意,方式一在高版本 Unity 不支持
|
||
|
||
方式二:在一个静态类的静态字段或者属性里头配置一个列表。属性可以用于实现的比较复杂的配置,比如根据命名空间做白名单。
|
||
|
||
!!注意,高版本 Unity 需要把配置文件放 Editor 目录下
|
||
|
||
```csharp
|
||
//如果涉及到Assembly-CSharp.dll之外的其它dll,如下代码需要放到Editor目录
|
||
public static class HotfixCfg
|
||
{
|
||
[Hotfix]
|
||
public static List<Type> by_field = new List<Type>()
|
||
{
|
||
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/hotfix_id_map.lua.txt`,并且自动加时间戳备份到 `hotfix_id_map.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` 提供了 `auto_id_map` 函数,执行一次后你就可以像以前那样直接用类,方法名去指明修补的函数。
|
||
|
||
```lua
|
||
(require 'xlua.util').auto_id_map()
|
||
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)
|
||
```
|
||
|
||
前提是 `hotfix_id_map.lua.txt` 放到可以通过 `require 'hotfix_id_map'` 引用到的地方。
|
||
|
||
## 使用建议
|
||
|
||
* 对所有较大可能变动的类型加上 `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,`method_name` 等于 `get_AProp`,setter 的 `method_name` 等于 `set_AProp`。
|
||
|
||
* []操作符
|
||
|
||
赋值对应 `set_Item`,取值对应 `get_Item`。第一个参数是 self,赋值后面跟 key,value,取值只有 key 参数,返回值是取出的值。
|
||
|
||
* 其它操作符
|
||
|
||
C#的操作符都有一套内部表示,比如 `+` 号的操作符函数名是 `op_Addition`(其它操作符的内部表示可以去请参照相关资料),覆盖这函数就覆盖了 C# 的 `+` 号操作符。
|
||
|
||
* 事件
|
||
|
||
比如对于事件“AEvent”,+= 操作符是 add_AEvent,-=对应的是 remove_AEvent。这两个函数均是第一个参数是self,第二个参数是操作符后面跟的delegate。
|
||
|
||
通过 `xlua.private_accessible`(版本号大于2.1.11不需要调用 `xlua.private_accessible`)来直接访问事件对应的私有 delegate 的直接访问后,可以通过对象的"&事件名"字段直接触发事件,例如 `self['&MyEvent']()`,其中MyEvent是事件名。
|
||
|
||
* 析构函数
|
||
|
||
method_name 是 "Finalize",传一个 self 参数。
|
||
|
||
和普通函数不一样的是,析构函数的热补丁并不是替换,而是开头调用 lua 函数后继续原有逻辑。
|
||
|
||
* 泛化类型
|
||
|
||
其它规则一致,需要说明的是,每个泛化类型实例化后都是一个独立的类型,只能针对实例化后的类型分别打补丁。比如:
|
||
|
||
```csharp
|
||
public class GenericClass<T>
|
||
{
|
||
}
|
||
```
|
||
|
||
你只能对 `GenericClass<double>`,`GenericClass<int>` 这些类,而不是对 `GenericClass` 打补丁。
|
||
|
||
对 `GenericClass<double>` 打补丁的实例如下:
|
||
|
||
```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;
|
||
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
|
||
})
|
||
|
||
```
|