30.总结
30.1 知识点
系列模块
- 概述与环境搭建
- C# 调用 Lua:解析器、加载、管理器、变量与函数、表与多种映射
- Lua 调用 C#:类、值类型、集合、扩展方法、重载、委托与事件等互操作细节
- xLua 热补丁:单函数到泛型类、属性、事件等替换维度
30.2 核心要点速览
热更场景与前置
常见拆法:引擎侧 C# 尽量稳定,业务逻辑放 Lua;资源侧「包内 + 远端 AB」并进。下表按客户端 / 服务端分工收起要点。
| 块 | 说明 |
|---|---|
| 客户端 | C# 做引擎与宿主;Lua 承载希望线上可换的逻辑;Resources 多在包内 |
| 服务端 | 资源 AB + Lua 脚本 AB,检查版本后下载、进内存执行 |
| 前置 | AssetBundle 会打包与下发;Lua 语法 会写脚本——缺一则「脚本进包 + 互操作」都别扭 |
学习目标
整条线按「能跑起来 → 双向互操作 → 热补丁兜底」排;热补丁与普通 Lua 逻辑解决的问题不同,复习时别混成一条链。
| 环节 | 要交付的能力 |
|---|---|
| 导入 xLua | 工程里框架就绪,LuaEnv 与 Unity 正确链接 |
| C# 调用 Lua | Global、表映射、require 与 Loader,从 C# 驱动脚本 |
| Lua 调用 C# | CS、集合规矩、委托与扩展,脚本里直接用引擎 API |
| xLua 热补丁 | IL 侧把标靶方法转进 Lua,修 已进包 的 C# 行为 |
- 前四项是日常互操作;热补丁单独对付商店包内已编译、不便整包更新的 C# 行为。
环境与工程搭设
- 源码 Tencent/xLua,Code → Download ZIP;顶层含
Assets、Tools等,本质是完整 Unity 工程目录。 - 新项目只拷
Assets/Plugins(各平台 native)+Assets/XLua(Editor、Gen、Src等)到自家 Assets。 - 首次:XLua → Clear Generated Code → Generate Code;同级还有 Hotfix Inject In Editor、Examples。
- Asset Bundle Browser(Package Manager)→ Window → Asset Bundle Browser:Configure / Build / Inspect;新 Unity 若主用 Addressables,仍属「分包 + 远端」同一类问题。
- 示例工程 BaseFramework(普通 /
MonoBehaviour单例)+ AssetBundleManager,把 AB 与 Lua 管线收拢,避免管理器散落。
C# 侧:虚拟机与 require
LuaEnv 即虚拟机外壳;chunkName 进异常栈,排错应填。
LuaEnv env = new LuaEnv();
env.DoString("print('ok')", "MyChunk");
env.Tick();
env.Dispose();
默认 require('模块名') 走 Resources.Load,故 Resources 里常用 模块名.lua.txt,避免裸 .lua 扩展名加载不到。
自定义 Loader
AddLoader(byte[] Loader(ref string filePath)):filePath即require括号内模块名。- 顺序:先入先试;首个返回非 null 的字节块即命中并停止 → 再 内置 Resources → 仍失败则抛错。
- 磁盘示例:
Application.dataPath + "/LuaScripts/" + filePath + ".lua"+File.ReadAllBytes。
luaEnv.AddLoader((ref string fp) => {
string p = Application.dataPath + "/LuaScripts/" + fp + ".lua";
return File.Exists(p) ? File.ReadAllBytes(p) : null;
});
LuaManager、双 Loader 与后缀
工程里常在 Init 里叠磁盘 Loader 与 AB Loader,再统一用 DoLuaFile 包一层 require,和上一节「自定义 Loader」是同一机制的组合用法。
| 项 | 说明 |
|---|---|
Init |
LuaEnv + 磁盘 Loader + AB Loader(disk miss 再 TextAsset) |
DoLuaFile |
DoString(require('name')) |
Global |
_G 上的 Get<T> / Set |
- 后缀:Resources →
.lua.txt;自管目录 →.lua;打进 AB 常改.txt当 TextAsset,LoadAsset名与打包配置一致。
C# 取 Lua:全局量与函数
Global.Get/Set只对_G;local取不到,硬转int易InvalidCastException。- 简单签名可用 **
Action/Func**;复杂签名 → 自定义delegate+[CSharpCallLua]+ Generate;多返回用out/ref位;LuaFunction.Call万能但 GC 重。 - 变长参走委托时,
pairs可能不稳,同逻辑可用LuaFunction.Call兜底。
表映射到 List / Dictionary / 类 / 接口 / LuaTable
| 目标 | 改 C# 后 Lua 原表 |
|---|---|
List<T>、List<object> |
不动 |
Dictionary<…> |
不动;遍历无序正常 |
class + public 字段 |
不动;private/protected 不进;多键可弃、多字段默认 |
[CSharpCallLua] interface |
写属性会回写表 |
LuaTable |
Set 改表;缺键 Get 抛;须 Dispose |
Lua 调 C#:CS、. / :、MonoBehaviour
local go = CS.UnityEngine.GameObject("Demo")
local colType = typeof(CS.MyBehaviour)
go:AddComponent(colType)
go:GetComponent(colType):SomeMethod()
local x = CS.UnityEngine.GameObject.Find("Demo").transform.position -- 字段用 .
- 静态成员:
Type.Member;实例方法:obj:Method();Lua 侧没有 C# 的new,CS.Foo(...)即构造。 MonoBehaviour不能当普通类直接CS.MyMb(),须AddComponent(typeof(CS.X))。
集合:在 Lua 里按 C# 规则
- 下标 从 0;长度
Length/Count;不用#。 - 新建数组:
CS.System.Array.CreateInstance(typeof(CS.System.Int32), n)。 - List:
list:Add(v)、list:Remove(v)。老版本泛型类型名字符串形如:
List`1[System.String]
新版本常用 CS.System.Collections.Generic.List(CS.System.String)(),以工程 xLua 版本为准。
- Dictionary:
pairs(d);字符串键d["k"]可能 nil,用 **get_Item/set_Item/TryGetValue**。
扩展方法、ref/out、重载、委托与事件
扩展方法
承载扩展的 static 类 标 [LuaCallCSharp] 并 Generate;Lua:实例:扩展名()。
ref / out
多返回值:第一个 = C# 返回值,其后按形参顺序对应 ref/out 出参;**ref 要占位初值**,out 不占 Lua 实参位。
重载
Lua 只有 number,10 与 10.0 可能都贴 int;要 float 重载:
local m = typeof(CS.MyType):GetMethod("Foo", { typeof(CS.System.Single) })
local f = xlua.tofunction(m)
f(obj, 10.2)
委托与事件
委托 **nil 时不能 +**,先 =;事件用 obj:event("+", fn),清理由 C# 提供方法再在 Lua 调。
二维数组、Unity 判空
- 二维:
GetLength(维)、GetValue(i,j),不能写[i,j]。 - Object 假 null:Lua 里
== nil不可靠;真nil不能:Equals。可自定IsNull扩展([LuaCallCSharp])+ 短路:obj == nil or obj:Equals(nil) or obj:IsNull()。
系统委托清单、协程、get_generic_method
[CSharpCallLua] static List<Type>登记UnityAction<float>等系统委托,才能在 Lua 里AddListener;[LuaCallCSharp]另一张表登记希望 Lua 高效调 的 C# 类型。- 协程:
require("xlua.util"),StartCoroutine(util.cs_generator(fn)),体内coroutine.yield(WaitForSeconds(...))。 - 泛型方法推断失败:
xlua.get_generic_method(CS.Type, "Name"),再(类型实参…);成员调用 首参self。IL2CPP + 值类型实参 常需 C# 侧 已具现化调用过。
xLua 热补丁
| 步骤 | 内容 |
|---|---|
| 标记 | 类 [Hotfix] |
| 宏 | HOTFIX_ENABLE → Scripting Define Symbols |
| 菜单 | Generate Code → Hotfix Inject In Editor |
| 排错 | Tools 与 Assets 同级;路径忌中文;改热补 C# 后 再 Generate + Inject |
-- 实例方法:首参 self
xlua.hotfix(CS.Foo, "Add", function(self, a, b) return a + b end)
-- 静态:无 self
xlua.hotfix(CS.Foo, "Speak", function(s) print(s) end)
-- 批量
xlua.hotfix(CS.Foo, { Update = function(self) end, Add = function(self,a,b) return a+b end })
.ctor/Finalize:特殊键;正文语义为 先 C# 再叠 Lua;Finalize 要求类 有析构。IEnumerator:补丁return util.cs_generator(function() … coroutine.yield(CS.UnityEngine.WaitForSeconds(1)) … end)。- 属性 / 索引器:**
get_Age/set_Age(self,v),get_Item/set_Item(self,i,v)**。 - 事件:
add_事件名/remove_事件名;补丁里不要用obj:event("+", d)再挂 listener,容易和拦截逻辑形成递归;C# 侧事件字段可看似一直为 null,实际订阅记录在 Lua 侧表里。 - 泛型类:
xlua.hotfix(CS.MyGeneric(CS.System.String), { … }),每个T一套。
30.3 面试题精选
基础题
1. LuaEnv 的 Tick、Dispose 一般在什么时机调用?DoString 第二个参数为什么要填?
题目
长时间持有 LuaEnv 却从不调 Tick 会有什么风险?
深入解析
- 正文里
Tick用来 做 Lua 侧 GC、清掉不再要的 Lua 对象,常见做法是 每帧或切场景 里打一打。 Dispose在 不再使用该虚拟机 时释放资源。DoString的第二个参数是 chunkName,Lua 报错栈里会带这个名字,用来对照到是哪段 C# 里注入的脚本。
答题示例
长生命周期
LuaEnv要按项目节奏Tick,否则 Lua 垃圾回收堆着不跑,对象可能清理不及时。不用了整个Dispose。DoString(code, "可读名字")让异常栈能指回业务而不是「string chunk」。
参考文章
- 3.CSharp调用Lua-Lua解析器
2. require 时 Loader 与 Resources 的先后顺序是什么?
题目
filePath 是什么?全部找不到会怎样?
深入解析
- 先按
AddLoader注册顺序 返回字节;再 内置 Resources;再失败 抛异常。
答题示例
自定义 Loader 谁先返回
byte[]谁赢;都没了才Resources;仍无模块名则报错。filePath就是require('xxx')里的xxx。
参考文章
- 4.CSharp调用Lua-Lua文件加载重定向
- 5.CSharp调用Lua-Lua管理器
3. Lua 里 : 和 . 何时用?MonoBehaviour 为什么配合 typeof 与 AddComponent?
题目
CS.MyMb() 为什么不成立?
深入解析
:传self调实例方法;.取字段、调静态。MonoBehaviour必须挂GameObject;typeof走 Type 重载,避开 Lua 侧不便写的泛型AddComponent<T>。
答题示例
实例方法
go:Foo(),静态Type.Bar()。脚本用go:AddComponent(typeof(CS.X));不能CS.MyMb()当普通类构造。
参考文章
- 12.Lua调用CSharp-类
4. 热补丁从工程到 xlua.hotfix:宏、类标记、菜单顺序?实例与静态顶替函数签名差在哪?
题目
HOTFIX_ENABLE 写在哪?改过热补 C# 之后要做什么?
深入解析
- 宏、
[Hotfix]、Generate、Inject;Tools 与 Assets 同级、路径忌中文。 - 实例
function(self,...),静态function(...);改 C# 后 再 Generate + Inject。
答题示例
Scripting Define Symbols 加
HOTFIX_ENABLE,类标[Hotfix],Generate 后 Inject。实例补丁带self,静态不带。
参考文章
- 24.xLua热补丁-单函数替换
进阶题
1. Generate Code、Clear Generated Code、Hotfix Inject 各自解决什么问题?工程上要按什么顺序记?
题目
空白工程接入时最少要把哪两个目录拷进 Assets?为什么要 Clear 再 Generate?只做「下发补丁 Lua」、从未 Inject 过,能改掉包内 C# 吗?
深入解析
- 最少
Assets/Plugins(Lua 与各平台 native)+Assets/XLua(框架与编辑器脚本)。 - Generate:生成 C#↔Lua 互操作桩,把 Lua 函数、表映射绑到托管委托上,少踩反射。
- Clear:清旧桩;改
[LuaCallCSharp]/[CSharpCallLua]列表或生成异常时 先清再生 ,避免新旧桩 半套混用。 - Inject:改 IL,让标了
[Hotfix]的方法 运行时跳进 Lua;和「能不能从 C# 调 Lua」不是一回事。 - 客户端包若 没注入过,只丢
.lua上去 改不了 已编译程序集里的 C#。
答题示例
拷贝 Plugins 与 XLua。互操作改配置:Clear → Generate。热修还要 Inject,把
[Hotfix]方法接到 Lua;出包用的程序集必须带注入结果,之后线上多更补丁 Lua。改过热补相关 C# 要 再 Generate + Inject 再发版。从没注入过的包,光下 Lua 换不了包内 C#。
参考文章
- 2.环境准备
- 24.xLua热补丁-单函数替换
2. 同一张 Lua 表映射成 class、带 [CSharpCallLua] 的接口、LuaTable,写回与释放上差在哪?
题目
private 字段为何映射不进来?
深入解析
- class + public:快照式填充,改 C# 不回写 Lua。
- interface:代理 写属性会改表。
LuaTable:**Set直接改表;Get缺键抛**;须Dispose。
答题示例
类映射是拷贝,接口映射可双向;LuaTable 动态访问要释放。只有 public 参与类字段映射。
参考文章
- 9.CSharp调用Lua-表映射到类
- 10.CSharp调用Lua-表映射到接口
- 11.CSharp调用Lua-表映射到LuaTable
3. Lua 调带 ref、out 的 C# 方法:实参与多返回值如何对齐?
题目
少传参数会怎样?
深入解析
ref要 初值占位;out不占 Lua 实参槽;第一返回值 = C# return,其后 =ref/out出参。- 少传会按正文规则 用默认填,易 静默错位。
答题示例
ref占位,out跳过;第一个返回值是函数返回值,后面按声明接ref/out。生产侧写满参数。
参考文章
- 16.Lua调用CSharp-ref和out函数
4. 热补丁里 [".ctor"]、Finalize、IEnumerator 方法各注意什么?
题目
构造补丁会不会「完全不要 C# 构造体」?
深入解析
- 正文:先 C# 再 Lua;Finalize 要类 有析构。
- 协程顶替:
return util.cs_generator(...),coroutine.yield对接yield return。
答题示例
.ctor/Finalize是特殊键;构造是 叠加语义(以正文为准)。协程补丁返回cs_generator包好的IEnumerator。
参考文章
- 25.xLua热补丁-多函数替换
- 26.xLua热补丁-协程函数替换
深度题
1. 从 Lua Global.Get 取「非 Action/Func 能表达」的函数,为什么要自定义 delegate、[CSharpCallLua] 和 Generate?
题目
漏生成典型现象?多返回、变长参怎么铺平?
深入解析
- 自定义
delegate+[CSharpCallLua]声明 允许 Lua 函数绑到的 CLR 签名;Generate 把绑定编译进桩。 - 漏生成:Get 失败或调用期异常。多返回:**返回值 +
out/ref**;变长参可LuaFunction.Call。
答题示例
复杂签名靠 自声明 delegate + [CSharpCallLua] + Generate;否则只能
LuaFunction硬 Call。改完特性要 Clear + Generate。
参考文章
- 7.CSharp调用Lua-函数的获取和调用
2. 改不了 UnityAction<float> 源码时,怎么让 Slider.onValueChanged.AddListener 接受 Lua 函数?get_generic_method 在 IL2CPP 下多什么坑?
题目
List<Type> 上两个特性各管哪类需求?
深入解析
[CSharpCallLua] List<Type>:登记 要从 Lua 当委托塞进 C# 的系统类型(如UnityAction<float>),再 Generate。get_generic_method:具现化 推断失败 的泛型方法;IL2CPP 下 值类型实参 常需 C# 已调用过同类型组合 做 AOT 预热。
答题示例
系统委托进
CSharpCallLua静态列表 + Generate。泛型方法裸调失败用xlua.get_generic_method;真机 IL2CPP 对 值类型泛型实参 要防 未具现。
参考文章
- 21.Lua调用CSharp-系统代码交互
- 23.Lua调用CSharp-泛型
3. 热补丁中 add_事件 / remove_事件 拦截的是什么?为何 C# 里事件字段仍像 null?泛型类如何写 hotfix 目标?
题目
补丁里为何不能 self:myEvent("+", delegate)?属性与默认索引器键名?
深入解析
- add/remove 拦截 **
+=/-=**,委托列表 落 Lua;补丁里再写self:myEvent("+", fn)一类语法,容易和拦截逻辑形成 递归。 - C#:
get_Age/set_Age,get_Item/set_Item。泛型类:CS.Foo(CS.System.Int32)等 逐T注册。
答题示例
加减事件由 add/remove 拦到 Lua 表;补丁里别再写
obj:event("+", fn)叠订阅,容易递归。属性/索引器用 getter/setter 方法名顶替。泛型CS.MyType(具体T)逐个 **T调xlua.hotfix**。
参考文章
- 27.xLua热补丁-属性和索引器替换
- 28.xLua热补丁-事件加减操作替换
- 29.xLua热补丁-泛型类替换
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com