举个例子吧,比如说CS.UnityEngine.Input的调用流程
第一次调用CS.UnityEngine的时候,CS表里没有UnityEngine这个键,所以会触发__index方法。此时fqn为空,会调用local obj = import_type(‘UnityEngine’)。由于此时只是路径,所以返回的obj=nil,此时把obj={[‘.fqn’] = fqn},因为这个obj其实不是obj只是调用链上的一部分,但是肯定要记录下这个链前面是什么方便后面访问的。顺便还要设置元表(毕竟需要入口嘛,不然.Input没法访问到了)。最后rawset(self, key, obj)就是CS[‘UnityEngine’] = obj, 也就是说下一次再调用CS.UnityEngine就不需要走元表了直接走table的键值对了。
后续第一次调用CS.UnityEngine.Input的时候,其实是UnityEngine.Input,而UnityEngine也就是我们上述存的那个obj, 其值也就是obj={[‘.fqn’] = fqn},所以这里fqn就是UnityEngine.Input了,然后调用C#层的import_type(‘UnityEngine.Input’)获得了obj=true,(其实并且这个import_type会设置UnityEngine[Input] = real_obj,并return这个real_obj)
第二次调用CS.UnityEngine.Input的时候,就不会走任何元表了,直接走table了
[MonoPInvokeCallback(typeof(LuaCSFunction))]
public static int ImportType(RealStatePtr L)
{
try
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
string className = LuaAPI.lua_tostring(L, 1);
Type type = translator.FindType(className);
if (type != null)
{
if (translator.GetTypeId(L, type) >= 0) //导入wrap or reflection
{
LuaAPI.lua_pushboolean(L, true);
}
else
{
return LuaAPI.luaL_error(L, “can not load type ” + type);
}
}
else
{
LuaAPI.lua_pushnil(L);
}
return 1;
}
catch (System.Exception e)
{
return LuaAPI.luaL_error(L, “c# exception in xlua.import_type:” + e);
}
}
StatistiLuaCallbacks.cs
注释应该把整个lua栈情况写的比较详细了,此函数的主要作用就是把method, getter, setter写入元表,其中method和getter写入__index, setter写入__newindex。具体的设置方法写入了xlua.c,这里我猜测是因为对栈的操作比较多所以直接写C层了。
接下来具体看看C层的obj_indexer吧。
ObjectRegister和ClassRegister的区别是,顾名思义,object是对象,如果是gameobject.position,就调用的是object的逻辑。Class是类如果是CS.UnityEngine.GameObject.Find,则调用的是class的逻辑。
object将做好的含有indexer的metatable放入了注册表中,只有真正push_object的时候,才会将metatable以及其中的__index给对象(LuaAPI.xlua_pushcsobj这个API做的,Push函数的最后一行)
class则直接将信息直接存在了CS.XX.XX中(因为静态方法,直接使用CS.XX.XX就能调用而不需要产生object实例之后才能用)
还有不同就是class将方法直接写入了CS.XX.XX的表中(当然如果要访问基类的方法还是要走classindexer)。原因也简单,因为obj是很多属于相同class的obj可以共享这个__index,肯定不要复制很多份表的好。但是class就只有一个,所以直接设置会快一点。
BeginClassRegister和BeginObjectRegister有异曲同工之妙,创建cls_table, meta_table, static_getter, static_setter,设置meta的构造方法,并把cls_table插入到CS..路径中
有个小插曲,刚开始由于没有细看C层的obj_indexer所以产生了疑问。既然metatable[‘__index’] = cls_indexer这里面已经设置好了metatable,那不就访问的时候直接访问这个就好了为什么还要在注册表中存副本?我还全局搜了LuaIndexsFieldName发现除了注册的时候确实也没有用到这个字段。不过后来仔细的看了看才发现,obj_indexer的上值是baseType,所以这个东西存着是为了找父亲的indexer。在obj_indexer中,如果其他几个表都找不到索引,就会去查找baseType在registry[LuaNewIndexsFieldName][type]中存的副本。(并且如果找到了就缓存在这个upvalue中,并将upvalue base置空了)
然而疑问就来了,那如果是子类先register的怎么办?父类不就是空了吗?其实早在LuaEnv初始化,初始registry[Utils.LuaIndexsFieldName] = {},…,这四个表的时候,就已经为他们所有设置了通用的metatable的__index。(也就是找不到registry[Utils.LuaIndexsFieldName][baseType]的时候会调用),代码如下:
也是将四个表分别放入元表(CS.PATH.GameObject的元表)中,也是调用C层的函数。
这三个方法已经在前面wrap的注册里面提过了,值得注意的是这里都是objectRegister,并没有注册类。但是从lua层的CS表中,却可以发现CS.System.Array,应该是lua层调用的CS.System.Array访问到的吧。可是很奇怪,我在init_xlua里面加上了local a = CS.System.Array之后,去掉LuaEnv里面的translator.CreateArrayMetatable(rawL);却报错了,待我再研究研究。
创建:
创建一个cs层的vector3历程:
CS.UnityEngine.Vector3可以先缓存起来,查表(rawget)不计入时间。Vector3类的元表会调用CS层的__CreateInstance方法(lua元表调用+跨语言调用),随后从栈中取出三个元素(3次跨语言调用),然后调用CS方法在CS层新建个vector,随后将这个vector push到lua栈(2次跨语言调用一次新建udata),所以总费用是1次元表,6次跨语言调用,一次新建udata。
创建一个lua层的vector3历程:
调用函数,new_table,很简洁。费用是1次函数调用+1个新建table的花费。
对比:
cs层的费用主要在跨语言调用,lua层的费用主要就是newtable的费用。按照实验结果而言,二者速度相差不大(但是lua层占用的内存更大)
_add:
使用CS的Vector3调用_add历程:
local cs_vector3 = CS.UnityEngine.Vector3(1,2,3)生成一个cs vector。这时候的cs_vector3是一个C层的udata,元表指向的CS层的wrap(或反射)。占用内存不大,只有2个标记位和3个float值的内存,并且无cs层的占用。当使用属性时比如说cs_vector3_a+cs_vector3_b时,会先产生一个元表调用的费用,随后因为函数是在cs层,所以会产生一个跨语言调用的费用。这时候进入了CS层,CS层需要取udata的数据,是通过调用C API完成的,所以这时候又会产生跨语言的费用。to_userdata一个,unpack一个,两个向量就是4份。随后计算完还要打包回去又是2个。打包完就要切换成C/Lua,又是一份,所以一共的费用是:跨语言费用是8个,元表调用1个,新建udata 1个。
使用lua vector3调用_add历程:
local lua_vector3 = lua_vector(1,2,3)生成一个lua_vector3,这时候lua_vector3占用的是一个table的内存,也无cs层的占用,元表指向lua层的函数。当使用属性时比如说lua_vector3_a+lua_vector3_b时,会先产生一个元表调用的费用,随后直接在lua层计算,而后new一个table返回。所以一共的费用是:元表调用1个,新建lua表1个。
对比:
cs层的费用主要在跨语言调用,lua层的费用主要就是newtable的费用。其实对于越复杂的计算,cs表现会更好一点,因为cs的计算力要比lua强。实验结果:针对于_add来说,lua层的速度是cs层的1.5~2倍。
Push cs_vector到cs层:
to_userdata + unpack一共两次跨语言调用。
Push lua_vector到cs层:
一共16次跨语言调用。。。(取个table的单独的值要3次,还有什么pop的)
分开x,y,z push到cs层:
3次跨语言调用(取x,y,z)
对比:
分开xyz和cs_vector速度相差不大,而lua_vector的时间是前者的3-4倍(因为16次跨语言调用真的是太多了。)
取值:
lua_vector:
就是个表的键值对获取。
cs_vector:
先调用元表,然后跨语言调用到cs,然后cs再跨语言调用C api进行取值,费用是1次元表调用,5次元语言调用(元表1+读udata2+pushnumber1+返回1)
对比:
二者的差距相当之大,10多倍。lua表获取键值对还是很快的,虚拟机也没有多余的指令,表那边也就是个hash获取。
思考:
从前面的对比可以发现,cs_vector在计算,取值上要比lua_vector占劣势,而push到cs层,cs_vector要远远快于lua_vector,lua_vector已经没有优化空间了,有没有什么方法把cs_vector的速度加快呢?有!
之前所提到的cs_vector的花费,最常见的一个词就是”跨语言调用”。那么有没有可能不产生这个跨语言调用的费用?可以!vector就是一个简单的值类型,而xlua中的cs层在c层生成的这个udata,也只用到了非元表的部分,而且取值也都是在C层实现的。那么如果我在C层实现元表的?那么之前的所有的所有的跨语言调用都不存在了。而且可以直接无损push到cs层。