在聊ulua、tolua之前,我們先來看看Unity熱更新相關知識。
什么是熱更新
舉例來說: 游戲上線后,玩家下載第一個版本(70M左右或者更大),在運營的過程中,如果需要更換UI顯示,或者修改游戲的邏輯,這個時候,如果不使用熱更新,就需要重新打包,然后讓玩家重新下載(浪費流量和時間,體驗不好)。 熱更新可以在不重新下載客戶端的情況下,更新游戲的內容。 熱更新一般應用在手機網游上。
為什么要用lua做熱更新
其實C#本身的反射機制可以實現熱更新,但是在ios平台上:
System.Reflection.Assembly.Load
System.Reflection.Emit
System.CodeDom.Compiler
無法使用,而動態載入dll或者cs的方法就這幾個,因此在ios下不能動態載入dll或者cs文件(已經編譯進去的沒事),就把傳統dotnet動態路徑封死了。
所以,只能通過把lua腳本打進ab包,玩家通過解壓ab包來更新游戲邏輯和游戲界面。
lua熱更技術
- ulua & tolua
- xlua
- slua
- …
lua熱更新流程
原理
使用assetbundle進行資源的更新,而由於lua運行時才編譯的特性,所以lua文件也可以被看成是一種資源文件(與fbx、Image等一樣)可以打進ab包中。
流程
- 對比files清單文件
- 更新文件
- 解壓AB包中的資源
- 初始化
游戲運行時從服務器下載files.txt清單文件,與本地的files.txt清單文件進行對比。如果新下載的files里面的md5值與本地files的md5值不一樣,或者本地清單里根本沒有這個文件就從服務器下載這個ab包到PersistentDataPath文件夾(可讀寫)中。下載完畢后解開AB包中的資源,然后完成初始化。
ulua&tolua原理解析
既然使用了lua作為熱更腳本,那肯定避免不了lua和C#之間的交互。
C#調用lua
C#調用lua的原理是lua的虛擬機,具體步驟可參見我的博客
也可以看看簡單的示例:
private string script = @"
function luaFunc(message)
print(message)
return 42
end
";
void Start () {
LuaState l = new LuaState();
l.DoString(script);
LuaFunction f = l.GetFunction("luaFunc");
object[] r = f.Call("I called a lua function!");
print(r[0]);
}
lua調用C#
反射
舊版本的ulua中lua調用C#是基於C#的反射。
C#中的反射使用Assembly定義和加載程序集,加載在程序集清單中列出模塊,以及從此程序集中查找類型並創建該類型的實例。
反射用到的命名空間:
System.Reflection
System.Type
System.Reflection.Assembly
反射用到的主要類:
- System.Type 類-通過這個類可以訪問任何給定數據類型的信息。
- System.Reflection.Assembly類-它可以用於訪問給定程序集的信息,或者把這個程序集加載到程序中。
ulua反射調用C#示例:
private string script = @"
luanet.load_assembly('UnityEngine')
luanet.load_assembly('Assembly-CSharp')
GameObject = luanet.import_type('UnityEngine.GameObject')
ParticleSystem = luanet.import_type('UnityEngine.ParticleSystem')
local newGameObj = GameObject('NewObj')
newGameObj:AddComponent(luanet.ctype(ParticleSystem))
";
//反射調用
void Start () {
LuaState lua = new LuaState();
lua.DoString(script);
}
可看到通過反射(System.Reflection.Assembly)把UnityEngine程序集加入到lua代碼中,通過反射(System.Type)把Unity.GameObject和Unity.ParticleSystem類型加入到lua代碼中,這樣我們便可以在lua中像在C#里一樣調用Unity定義的類。
去反射
現版本的ulua(tolua)中lua調用C#是基於去反射。
去反射的意思是:
把所有的c#類的public成員變量、成員函數,都導出到一個相對應的Wrap類中,而這些成員函數通過特殊的標記,映射到lua的虛擬機中,當在lua中調用相對應的函數時候,直接調用映射進去的c# wrap函數,然后再調用到實際的c#類,完成調用過程。
具體調用過程可參考: Unity3d ulua c#與lua交互+wrap文件理解
因為反射在效率上存在不足,所以通過wrap來提升性能。但是因為wrap需要自己去wrap,所以在大版本更新是可以用到的,小版本更新還是使用反射。
C#與Lua數據交互(lua虛擬棧)
C#與lua的數據交互是基於一個Lua先進后出的虛擬棧:
(1)若Lua虛擬機堆棧里有N個元素,則可以用 1 ~ N 從棧底向上索引,也可以用 -1 ~ -N 從棧頂向下索引,一般后者更加常用。
(2)堆棧的每個元素可以為任意復雜的Lua數據類型(包括table、function等),堆棧中沒有元素的空位,隱含為包含一個“空”類型數據
(3)TValue stack[max_stack_len] // 定義在 lstate.c 的stack_init函數
關於Lua虛擬棧入棧的具體操作做可以見下圖:
更詳細的可見: Lua初學者(四)–Lua調用原理展示(lua的堆棧)
C#與Lua通信(P/Invoke)
- 所有的通信都是基於P/Invoke模式(性能低)類似JNI
- P/Invoke:公共語言運行庫(CLR)的interop功能(稱為平台調用(P/Invoke))
- 命名空間:System.Runtime.InteropServices
示例:
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr luaL_newstate();
P/Invoke 要求方法被聲明為 static。
P/Invoke性能:
為啥P/Invoke看起來這么慢?
(1)尋址方式:調用時指定了CharSet=CharSet.Ansi 那么CLR首先在非托管的DLL中尋找,若找不到,就用帶A后綴的函數進行搜索造成開銷,可將ExactSpelling的值設為true防止CLR通過修改入口名稱進行搜索。
(2)類型轉換:在Managed Code和Native Code間傳遞參數和返回值的過程成為marshalling。托管函數每次調用非托管函數時,都要求執行以下幾項操作:
- 將函數調用參數從CLR封送到本機類型。
- 執行托管到非托管形式轉換。
- 調用非托管函數(使用參數的本機版本)
- Interop在進行封送時候,對bittable可以不進行拷貝,而是直接內存錨定。
- 將返回類型及任何“out”或“in,out”參數從本機類型封送到 CLR 類型。
(3)VC++ 提供自己的互操作性支持,這稱為 C++ Interop。 C++ Interop 優於 P/Invoke,因為 P/Invoke 不具有類型安全性,參數傳遞還需要做類型檢查。
Bittable類型(byte,int,uint)與非Bittable類型(char, boolean,array,class)
參考書: NET互操作 P_Invoke,C++Interop和COM Interop.pdf
ulua的優化方式匯總
- BinderLua太多wrap很慢(反射與去反射共存)
- Lua代碼打入AssetBundle為了繞過蘋果檢測
- 動態注冊Wrap文件到Lua虛擬機(tolua延伸)
- ToLuaExport. memberFilter的函數過濾
- 盡量減少c#調用lua的次數來做主題優化思想
- 盡量使用lua中的容器table取代c#中的所有容器
- 例子CallLuaFunction_02里附帶了no gc alloc調用方式
- Lua的bytecode模式性能要低於Lua源碼執行
- 取消動態參數:打開LuaFunction.cs文件,找到函數聲明:
public object[] Call(params object[] args){
return call(args, null);
}
取消動態參數args,可用較笨方法,就是定義6-7個默認參數,不夠再加。
- 安卓平台如果使用luajit的話,記得在lua最開始執行的地方請開啟 jit.off(),性能會提升N倍。
- 記得安卓平台上在加上jit.opt.start(3),相當於c++程序-O3,可選范圍0-3,性能還會提升。Luajit作者建議-O2