Unity Ulua1.03优化记录

现在项目的框架是在2015年设计的,那时候Ulua还处于1.03版本,现在回头再看,Ulua已经迭代到1.25版本,中间引入带有wraper的cstolua,而后转向现有的toLua#版本。

随着版本更新性能提升了很多(当然也看人为因素),代码的清晰度也越来越清晰,但是引入的项目本身可能不需要的东西越来越多,比如Gameobject、vector等等,这些并不在我们现有框架需求的范围内。所以在这次优化过程中并没有直接拿最新的toLua#来替换现在的Lua交互模块,而是将后续版本中能够改善现有性能的部分逐步添加进来,保证框架本身的稳定性。下文主要介绍cstolua版本优化之后的实现方式以及与当前1.03对比。

C#调用 Lua Function--缓存机制

每个版本中对Lua table的均支持通过"."分割操作。

 /*
  * Navigates a table in the top of the stack, returning
  * the value of the specified field
  */
 internal object getObject(string[] remainingPath)
 {
     object returnValue = null;
     for (int i = 0; i < remainingPath.Length; i++)
     {
         LuaDLL.lua_pushstring(L, remainingPath[i]);
         LuaDLL.lua_gettable(L, -2);
         returnValue = translator.getObject(L, -1);
         if (returnValue == null) break;
     }
     return returnValue;
 }

如果你使用的Lua函数层级比较深,同时还要处理一些堆栈操作和校验,获取效率是没有保证的,当然也可以通过保存需要的LuaFunction避免这个问题。在后续版本中增加了LuaTable和LuaFunction缓冲的功能,对使用多的函数或者table保存下来,避免后续重复的查找。

public LuaFunction GetLuaFunction(string name)
 {
     LuaBase func = null;

     if (!dict.TryGetValue(name, out func))
     {
         IntPtr L = lua.L;
         int oldTop = LuaDLL.lua_gettop(L);

         if (PushLuaFunction(L, name))
         {
             int reference = LuaDLL.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);                
             func = new LuaFunction(reference, lua);                
             func.name = name;
             dict.Add(name, func);
         }
         else
         {
             Debugger.LogError("Lua function {0} not exists", name);
         }

         LuaDLL.lua_settop(L, oldTop);            
     }
     else
     {
         func.AddRef();
     }

     return func as LuaFunction;
 }

LuaTable获取采用类似的方案,代码不在贴出。

Lua调用C# Function -- 干掉反射

Lua1.03版本中主要使用反射机制实现Lua对函数的调用,主要流程:

  1. Lua使用时通过luanet.import_type注册需要的对象类型,在C#层通过反射获得Assembly对象,并记录下来
 public static int loadAssembly(IntPtr luaState) 
 {
	   ObjectTranslator translator = ObjectTranslator.FromState(luaState);
       string assemblyName = LuaDLL.lua_tostring(luaState, 1);
       Assembly assembly = null;
       assembly = Assembly.Load(assemblyName);
       if (assembly != null && !translator.assemblies.Contains(assembly)) 
       {
           translator.assemblies.Add(assembly);
       }
     }
     return 0; 
 }
  1. 在使用到注册对象的类型时,通过反射获取该方法并执行 ,具体可以查看 MetaFunctions.getMember()函数的实现。

C#的反射机制为什么慢,可考:C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题,慢可以接受,但还有一点是产生一堆临时对象,也就是会有GC存在。如果在框架层次严格的控制Lua和C#的交互时机和接口标准,完全也可以满足游戏需求。但是在多人开发中慢慢的就会走向另外一个方向,接口变多,标准不在。对于在大量在Lua层操作Unity对象的框架,在加上快速开发中的不关注,后期游戏的交互的性能估计更严重。

Ulua的后续版本中就增加了可以避免取消反射机制,即采用Lua全局注册的方式使用C#方法。具体流程:

  1. 定义需要导出的类
public static BindType[] binds = new BindType[]
{
     _GT(typeof(DebugExport)),
}
  1. 通过自动化工具生成需要的注册文件(wraper file),注册文件中实现每个类方法与Lua堆栈的交互过程;类似与下面:
//注册方法,关键在LuaScriptMgr.RegisterLib
public static void Register(IntPtr L)
{
	LuaMethod[] regs = new LuaMethod[]
	{
		new LuaMethod("RegisterLog", RegisterLog),
		new LuaMethod("LogError", LogError),
		new LuaMethod("LogWarning", LogWarning),
		new LuaMethod("Log", Log),
		new LuaMethod("ClearLog", ClearLog),
	};
	
	LuaField[] fields = new LuaField[]
	{
	};
	
	LuaScriptMgr.RegisterLib(L, "DebugExport", regs,1);
}

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int LogError(IntPtr L)
{
	LuaScriptMgr.CheckArgsCount(L, 1); //检测参数个数
	string arg0 = LuaScriptMgr.GetLuaString(L, 1); //获取第一个参数
	DebugExport.LogError(arg0);  //调用Unity 方法
	return 0;
}

  1. 游戏启动时首先将注册文件中的方法全部注册到Lua全局表中(也就是调用Wraper中Register),这样就后面在Lua中可以直接使用这些方法。

对象 -- 尽量避免开箱和装箱操作

不管是C#调用Lua还是Lua调用C#,不可避免的都会涉及到参数问题。
类型转换 c#中不可绕过的就是对象转换的代价,参考文章:深入理解C#的装箱和拆箱 , OK,频繁的开箱和装箱操作会额外耗费CPU和内存资源,所以在1.03后续版本中引入重载,对不同的数据类型提供不同的接口,同时在函数调用层次也提供一些优化方式来避免,比如讲函数调用开放给编码的人,比如C#层调用Lua层Update函数,带有两个参数,可以这样修改:

void CallUpdate(LuaFunction updateFunc, float deltaTime, float unscaledDeltaTime)
{
    int top = updateFunc.BeginPCall();
    IntPtr L = updateFunc.GetLuaState();
    //带有重载
    LuaScriptMgr.Push(L, deltaTime);
    //带有重载
    LuaScriptMgr.Push(L, unscaledDeltaTime);
    updateFunc.PCall(top, 2);
    updateFunc.EndPCall(top);
}

当然对于带有返回数据的函数也会存在这样的问题,优化方式类似,提供足够的重载函数来满足需求。当然对于要使用Gameobject、自定义类、委托等等的肯定还是避免不了.

对象生命周期管理方式

tolua#中 对象的生命周期管理和垃圾回收策略做了很大的优化,不过鉴于当前的框架中并没有对象作为参数,所有没有做进一步的修改。

结束语

后续版本中做的优化可能不止这些,但是通过上面的改善以及在逻辑层次注意和规范,应该可以满足现有要求,前提是在现有框架下。得益于现有框架对接口的定义,不存在使用GameObject等Unity内置类型或者委托之类的情况,所以避免了很多比如对象生命周期等问题。

参考:

[1]用好lua+unity,让性能飞起来——lua与c#交互篇 :http://www.cnblogs.com/zwywilliam/p/5999924.html

[2]:ulua中lua代码使用反射调用c#详解 http://blog.csdn.net/wdsdsdsds/article/details/54409936?utm_source=itdadao&utm_medium=referral

posted @ 2017-02-22 14:34  RubbyZhang  阅读(782)  评论(0编辑  收藏  举报