原文鏈接:Unity 游戲用XLua的HotFix實現熱更原理揭秘-CSDN博客
本文通過對XLua的HoxFix使用原理的研究揭示出來這樣的一套方法。這個方法的
- 第一步:通過對C#的類與函數設置Hotfix標簽。來標識需要支持熱更的類和函數。
- 第二步:生成函數連接器來連接LUA腳本與C#函數。
- 第三步:在C#腳本編譯結束后,使用Mono提供的一套C#的API函數,對已經編譯過的.Net體系生成的DLL文件進行修改。
- 第四步,通過LUA腳本修改C#帶有標簽的類中靜態變量,把代碼的執行路徑修改到LUA腳本中。通過這套方案可以實現對已經標識的C#代碼進行動態更新。
基礎準備
知識準備
CIL: 通用中間語言(Common Intermediate Language,簡稱CIL), 是一種屬于通用語言架構和 .NET 框架的低階(lowest-level)的人類可讀的編程語言。目標為 .NET 框架的語言被編譯成CIL(基于.NET框架下的偽匯編語言,原:MSIL),這是一組可以有效地轉換為本機代碼且獨立于 CPU 的指令。CIL類似一個面向對象的匯編語言,并且它是完全基于堆棧的。它運行在CLR上(類似于JVM),其主要支持的語言有C#、VisualBasic .NET、C++/CLI以及 J#(集成這些語言向CIL的編譯功能)。
在編譯.NET編程語言時,源代碼被翻譯成CIL碼,而不是基于特定平臺或處理器的目標代碼。CIL是一種獨立于具體CPU和平臺的指令集,它可以在任何支持.NET framework的環境下運行。CIL碼在運行時被檢查并提供比二進制代碼更好的安全性和可靠性。在Unity3D中,是用過Mono虛擬機來實現運行這些中間語言指令的。
之前寫一篇介紹過一篇使用微軟的API函數,利用中間語言生成或注入.NET支持下的DLL。這里就不在贅述,需要了解的請參考《使用MSIL采用Emit方式實現C#的代碼生成與注入》。
IL2CPP: 直接理解把IL中間語言轉換成CPP文件。根據官方的實驗數據,換成IL2CPP以后,程序的運行效率有了1.5-2.0倍的提升。引用地址:Unity將來時:IL2CPP是什么?有了Mono為什么還需要IL2CPP?_unity windows il2cpp什么時候用-CSDN博客
使用Mono的時候,腳本的編譯運行如下圖所示:
簡單的來說,3大腳本被編譯成IL,在游戲運行的時候,IL和項目里其他第三方兼容的DLL一起,放入Mono VM虛擬機,由虛擬機解析成機器碼,并且執行
IL2CPP做的改變由下圖紅色部分標明:
在得到中間語言IL后,使用IL2CPP將他們重新變回C++代碼,然后再由各個平臺的C++編譯器直接編譯成能執行的原生匯編代碼。
一,Lua基礎之熱更新
首先,什么是熱更新
- 字面意思就是對lua的一些代碼進行更新,在介紹熱更新之前,我想要和大家分享一下lua的require的機制
- 我們知道lua加載一個文件的方式可以有:dofile,loadfile以及 require。其中loadfile是只編譯不執行,dofile和require是同時編譯和執行。而dofile和require的區別是dofile同一個文件每次都要加載,也就是說,dofile兩次返回來的是兩個不同的地址。而require同一個文件,不管多少次都是都返回同一個地址,其原因是lua的地址緩存在了package.load()中。所以效率比dofile要高許多,因而現在一般都是用require加載文件。
- 那么問題來了,如果我在lua文件中改變了一些數值(產生了新的地址),結果你卻用之前的地址,那豈不是白給了嗎?
于是熱更新機制應運而生。其實現方式有兩種:
(1)簡單版但是有缺陷
package.load(“modelname”) = nil-- 修改modelname.lua的數據require(“modelname”)
- 既然你有緩存,我直接置為空不就好了嗎?然后重新require一次把修改好的加進來。這樣子做的話第二次require的數據可能是正確的,但是之前require過一次的數值卻仍然錯誤,所以說程序除非在之前沒有加載過這個文件,否則得到的結果不完善。
(2)復雜版但是很有用
function reload_module(module_name)local old_module = package.loaded[module_name] or {}package.loaded[module_name] = nilrequire (module_name)local new_module = package.loaded[module_name]for k, v in pairs(new_module) doold_module[k] = vendpackage.loaded[module_name] = old_modulereturn old_module
end
-
簡單來說就是使用一個全局表存儲了新修改后的所有數值,然后循環賦值給舊的值,這樣就可以確保同一個舊地址也可以得到正確的數據。
要點分析
Lua 語言
-
再熱更新功能開發過程中,我們需要用到一款新的語言:Lua語言。
-
Lua和C#對比:C#是編譯型語言,Lua是解析型語言
-
Lua語言不可以單獨完成一個項目的開發,Lua語言出現的目的是“嵌入式”,為其他語言開發出來的項目進行功能的擴展和補丁的更新。
2.Lua語言與C#語言交互
- Unity項目是使用C#開發的,后續熱更新的功能需要使用Lua語言實現。而我們在最開始使用C#開發項目的時候,需要預留和Lua代碼的“交互接口”,這就涉及到兩門語言的代碼相互調用訪問。
3.AssetBundle
- AssetBundle是Unity內資源的一種打包格式,和電腦上的rar、zip壓縮包比較類似,客戶端熱更新過程中,從服務器上下載下來的資源,都是AssetBundle打包過的資源。
4.ULua和XLua熱更新框架
- ULua和XLua是兩個熱更新框架,專門用于Unity客戶端項目熱更新開發。其實就是兩個“資源包”,導入到我們的項目中,在框架的基礎之上,完成我們項目需要的熱更新邏輯。
3.Lua熱更新的實現
- 1.將模塊中舊的函數替換成新的函數,這個新的函數可以放到一個lua文件中,或者以字符串的形式給出。
- 2.將模塊中舊的函數,當前用到的所有上值,(什么是上值,后面有講到)保存到起來,用于新函數引用,保證新函數作為模塊中的一部分能夠正確運行。
下面以一個demo為例,這也是抽取 snax 模塊中熱更新部分:
./main.lua ? ? ? ? ? ? ? ?調用 test.lua,做為運行文件,顯示最終運行效果
./test.lua ? ? ? ? ? ? ? ?一個簡單模塊文件,用于提供熱更新的來源
./test_hot.lua ? ? ? ? ? ?用于更新替換 test 模塊中的某些函數,更新文件
./hotfix.lua ? ? ? ? ? ? ?實現熱更新機制
通過這幅關系圖,可以了解到,test 模塊和 test_hot 之間的關系,test_hot 負責更新 test 模塊中的某些函數,但更新后的這些函數依然屬于 test 模塊中的一部分,并沒有脫離 test 模塊的掌控,而獨立出來。
- 現在我們看看 test.lua 包含了哪些內容,分別有 一個局部變量 index,兩個函數 print_index,show ,函數體分別是圓圈1和2,兩個函數都引用到了這個局部變量 index。
- 假設當前,我們想更新替換掉 print_index 函數,讓其 index 加1 操作,并打印 index 值,那么我們可以在 test_hot.lua 文件中這么寫,見下圖黃色框部分:
?
- 我們希望在 print_index 更新后, index 加 1 后,show 函數獲取到的 index 值是 1,即把更新函數也看作是 test.lua 模塊中的一部分。而不應該是 index 加 1 后,show 函數獲取到的還是原值 0。
- 假設我們希望更新 print_index 后,再一次更新,把 index 值直接設置為 100,那么它又應該是這樣子的,見下圖最左側黃色部分:
4._ENV 環境變量
- 在 lua 程序設計一書中有過這樣的解釋,lua 語言并沒有全局變量,所謂的全局變量都是通過某種手段模擬出來的。
Lua 語言是在一個名為 _ENV 的預定義上值(一個外部的局部變量,upvalue)存在的情況下編譯所有的代碼段的。因此,所有的變量要么綁定到一個名稱的局部變量,要么是 _ENV 中的一個字段,而 _ENV 本身是一個局部變量。
例如:
local z = 10
x = 0
y = 1
x = y + z
等價于
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y + z
- x,y 都是不用 local 聲明,z 是 local 聲明。
- 所以,我們用到的全局變量其實是保存到 _ENV 變量中。lua 語言在內部維護了一個表來作用全局環境(_G),通常,我們在 load 一個代碼段,一個模塊時,lua 會用這個表(_G)來初始化 _ENV。如果上面的幾行代碼是寫在一個文件中,那么當 load 調用它時,又會等價于:
-- xxx.lua 文件
local _ENV = the global environment (全局環境)
return function(...)
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y +z
end
5.上值 upvalue
從這里開始不是很懂,之后再復盤吧 ------------------------------------------------------------------------------
當一個局部變量被內層的函數中使用的時候, 它被內層函數稱作上值,或是外部局部變量。引用 Lua 5.3 參考手冊
例如:
local x = 10
function hello(a, b)
local c = a + b + x
print(c)
end
那么在這段代碼中,hello 函數的上值有 變量 x,_ENV,而我們剛剛講到,print 沒有經過聲明,就可以直接使用,那么它肯定是保存于 _ENV 表中,print(c) 等價于 _ENV.print(c),而變量 a、b、c 都是做為 hello 函數的局部變量。
6.熱更新函數Lua的require函數
- Lua的require(modelname)把一個lua文件加載存放到package.loaded[modelname]中,重復require同一個模塊實際還是沿用第一次加載的chunk。因此,很容易想到,第一個版本的熱更新模塊可以寫成這樣:
--強制重新載入module
function require_ex( _mname ) log( string.format("require_ex = %s", _mname) ) if package.loaded[_mname] then log( string.format("require_ex module[%s] reload", _mname)) end package.loaded[_mname] = nil require( _mname )
end
- 可以看到,強制地require新的模塊來更新新的代碼,非常簡單暴力。但是,顯然問題很多,舊的引用住的模塊無法得到更新,全局變量需要用"a = a or 0"這種約定來保留等等。這種程度的熱更新顯然不能滿足現在的游戲開發需求。
7.熱更新函數Lua的setenv函數
setenv是Lua 5.1中可以改變作用域的函數,或者可以給函數的執行設置一個環境表,如果不調用setenv的話,一段lua chunk的環境表就是_G,即Lua State的全局表,print,pair,require這些函數實際上都存儲在全局表里面。那么這個setenv有什么用呢?我們知道loadstring一段lua代碼以后,會經過語法解析返回一個Proto,Lua加載任何代碼chunk或function都會返回一個Proto,執行這個Proto就可以初始化我們的lua chunk。為了讓更新的時候不污染_G的數據,我們可以給這個Proto設置一個空的環境表。同時,我們可以保留舊的環境表來保證之前的引用有效。
local Old = package.loaded[PathFile]
local func, err = loadfile(PathFile)
--先緩存原來的舊內容
local OldCache = {}
for k,v in pairs(Old) do OldCache[k] = v Old[k] = nil
end
--使用原來的module作為fenv,可以保證之前的引用可以更新到
setfenv(func, Old)()
8.熱更新函數Lua的debug庫函數
Lua的函數是帶有詞法定界的first-class value,即Lua的函數與其他值(數值、字符串)一樣,可以作為變量、存放在表中、作為傳參或返回。通過這樣實現閉包的功能,內嵌的函數可以訪問外部的局部變量。這一特性給Lua帶來強大的編程能力同時,其函數也不再是單一無狀態的函數,而是連同外部局部變量形成包含各種狀態的閉包。如果熱更新缺少了對這種閉包的更新,那么可用性就大打折扣。
下面講一下熱更新如何處理舊的數據,還有閉包的upvalue的有效性問題怎么解決。這時候強大的Lua debug api上場了,調用debug庫的getlocal函數可以訪問任何活動狀態的局部變量,getupvalue函數可以訪問Lua函數的upvalues,還有相對應的修改函數。
例如,這是查詢和修改函數局部變量寫的debug函數:
-- 查找函數的local變量
function get_local( func, name ) local i=1 local v_name, value while true do v_name, value = debug.getlocal(func,i) if not v_name or v_name == name then break end i = i+1 end if v_name and v_name == name then return value end return nil
end
-- 修改函數的local變量
function set_local( func, name, value ) local i=1 local v_name while true do v_name, _ = debug.getlocal(func,i) if not v_name or v_name == name then break end i = i+1 end if not v_name then return false end debug.setlocal(func,i,value) return true
end
一個函數的局部變量的位置實際上在語法解析階段就已經能確定下來了,這時候生成的opcode就是通過寄存器的索引來找到局部變量的,了解這一點應該很容易理解上面的代碼。
9.深度遞歸替換所有的upvalue
- 接下來要做的事情很清晰了,遞歸所有的upvalue,根據一定的替換規則替換就可以,注意新的upvalue需要設置回原來的環境表。
function UpdateUpvalue(OldFunction, NewFunction, Name, Deepth) local OldUpvalueMap = {} local OldExistName = {} -- 記錄舊的upvalue表 for i = 1, math.huge do local name, value = debug.getupvalue(OldFunction, i) if not name then break end OldUpvalueMap[name] = value OldExistName[name] = true end -- 新的upvalue表進行替換 for i = 1, math.huge do local name, value = debug.getupvalue(NewFunction, i) if not name then break end if OldExistName[name] then local OldValue = OldUpvalueMap[name] if type(OldValue) ~= type(value) then -- 新的upvalue類型不一致時,用舊的upvalue debug.setupvalue(NewFunction, i, OldValue) elseif type(OldValue) == "function" then -- 替換單個函數 UpdateOneFunction(OldValue, value, name, nil, Deepth.." ") elseif type(OldValue) == "table" then -- 對table里面的函數繼續遞歸替換 UpdateAllFunction(OldValue, value, name, Deepth.." ") debug.setupvalue(NewFunction, i, OldValue) else debug.setupvalue(NewFunction, i, OldValue) -- 其他類型數據有改變,也要用舊的 end else ResetENV(value, name, "UpdateUpvalue", Deepth.." ") -- 對新添加的upvalue設置正確的環境表 end end
end
10.實例分析
- 下面就來看下具體 demo 的實現。
-- main.lua
local hotfix = require "hotfix"
local test = require "test"
local test_hot = require "test_hot"print("before hotfix")
for i = 1, 5 do test.print_index() -- 熱更前,調用 print_index,打印 index 的值
end hotfix.update(test.print_index, test_hot) -- 收集舊函數的上值,用于新函數的引用,這個對應之前說的歸納第2小點
test.print_index = test_hot -- 新函數替換舊的函數,對應之前說的歸納第1小點print("after hotfix")
for i = 1, 5 do test.print_index() -- 打印更新后的 index 值
end test.show() -- show 函數沒有被熱更,但它獲取到的 index 值應該是 最新的,即 index = 5。
- 接下來看看 test.lua 模塊內容:
-- test.lua
local test = {}
local index = 0 function test.print_index()print(index)
end function test.show( )print("show:", index)
endreturn test
- 再看看 熱更文件 test_hot.lua 內容:
-- test_hot.lua
local index -- 這個 index 必須聲明,不用賦值,才能夠引用到 test 模塊中的局部變量 indexreturn function () -- 返回一個閉包函數,這個就是要更新替換后的原型index = index + 1print(index)
end
- 最后,再看看 hotfix.lua:
-- hotfix.lua
local hotfix = {}local function collect_uv(f, uv)local i = 1while true dolocal name, value = debug.getupvalue(f, i)if name == nil then -- 當所有上值收集完時,跳出循環breakendif not uv[name] thenuv[name] = { func = f, index = i } -- 這里就會收集到舊函數 print_index 所有的上值,包括變量 indexif type(value) == "function" thencollect_uv(value, uv)endendi = i + 1end
endlocal function update_func(f, uv) local i = 1while true dolocal name, value = debug.getupvalue(f, i)if name == nil then -- 當所有上值收集完時,跳出循環breakend-- value 值為空,并且這個 name 在 舊的函數中存在if not value and uv[name] then local desc = uv[name]-- 將新函數 f 的第 i 個上值引用舊模塊 func 的第 index 個上值debug.upvaluejoin(f, i, desc.func, desc.index)end-- 只對 function 類型進行遞歸更新,對基本數據類型(number、boolean、string) 不管if type(value) == "function" thenupdate_func(value, uv)endi = i + 1end
endfunction hotfix.update(old, new)local uv = {}collect_uv(old, uv)update_func(new, uv)
endreturn hotfix
- 這個用到了 lua 的兩個 api 函數,在 Lua 5.3 參考手冊 中有介紹。
debug.getupvalue (f, up)
此函數返回函數 f 的第 up 個上值的名字和值。 如果該函數沒有那個上值,返回 nil 。
debug.upvaluejoin (f1, n1, f2, n2)
讓 Lua 閉包 f1 的第 n1 個上值 引用 Lua 閉包 f2 的第 n2 個上值。
- 我們可以看到, hotfix.lua 做的事也是比較簡單的,主要是收集 舊函數的所有上值,更新到新函數中。最后一步替換舊函數是在 main.lua 中完成。
- 最后看看運行結果:
[root@instance test]# lua main.lua
before hotfix
0
0
0
0
0
after hotfix
1
2
3
4
5
-------------
show: 5
四、Lua腳本熱更新方案
-
熱更新,通俗點說就是補丁,玩家那邊知道重啟客戶端就可以更新到了的,不用卸載重新安裝app,相對于單機游戲,這也是網絡游戲用得比較多的一個東西吧。
-
首先,大概流程如下:
- luaFileList.json文件內容一般是lua文件的鍵值對,key為lua文件路徑+文件名,value為MD5值:
五、lua熱更新
1.什么是熱更新
- 熱更新也叫不停機更新,是在游戲服務器運行期間對游戲進行更新。實現不停機修正bug、修改游戲數據等操作
2.熱更新原理第一種:
- lua中的require會阻止多次加載相同的模塊。所以當需要更新系統的時候,要卸載掉響應的模塊。(把package.loaded里對應模塊名下設置為nil,以保證下次require重新加載)并把全局表中的對應的模塊表置 nil 。同時把數據記錄在專用的全局表下,并用 local 去引用它。初始化這些數據的時候,首先應該檢查他們是否被初始化過了。這樣來保證數據不被更新過程重置。
原文鏈接:Unity將來時:IL2CPP是什么? - 知乎 (zhihu.com)
IL
啰 嗦完了C#,.Net Framework和Mono,引出了我們很重要的一個概念”IL“。IL的全稱是 Intermediate Language,很多時候還會看到CIL(Common Intermediate Language,特指在.Net平臺下的IL標準)。在Unity博客和本文中,IL和CIL表示的是同一個東西:翻譯過來就是中間語言。它是一種屬于 通用語言架構和.NET框架的低階(lowest-level)的人類可讀的編程語言。目標為.NET框架的語言被編譯成CIL,然后匯編成字節碼。 CIL類似一個面向對象的匯編語言,并且它是完全基于堆棧的,它運行在虛擬機上(.Net Framework, Mono VM)的語言。
具體過程是:C#或者VB這樣遵循CLI規范的高級語言,被先被各自的編譯器編譯成中間語言:IL(CIL),等到需要真正執行的時候,這些IL會被加載到運行時庫,也就是VM中,由VM動態的編譯成匯編代碼(JIT)然后在執行。
正是由于引入了VM,才使得很多動態代碼特性得以實現。通過VM我們甚至可以由代碼在運行時生成新代碼并執行。這個是靜態編譯語言所無法做到的。回到上一 節我說的Boo和Unity Script,有了IL和VM的概念我們就不難發現,這兩者并沒有對應的VM虛擬機,Unity中VM只有一個:Mono VM,也就是說Boo和Unity Script是被各自的編譯器編譯成遵循CLI規范的IL,然后再由Mono VM解釋執行的。這也是Unity Script和JavaScript的根本區別。JavaScript是最終在瀏覽器的JS解析器中運行的(例如大名鼎鼎的Google Chrome V8引擎),而Unity Script是在Mono VM中運行的。本質上說,到了IL這一層級,它是由哪門高級語言創建的也不是那么重要了,你可以用C#,VB,Boo,Unity Script甚至C++,只要有相應的編譯器能夠將其編譯成IL都行!
IL2CPP, IL2CPP VM
- 1.Mono VM在各個平臺移植,維護非常耗時,有時甚至不可能完成
- 2.Mono版本授權受限
- 3.提高運行效率
幾點注意:
1.將IL變回CPP的目的除了CPP的執行效率快以外,另一個很重要的原因是可以利用現成的在各個平臺的C++編譯器對代碼執行編譯期優化,這樣可以進一步減小最終游戲的尺寸并提高游戲運行速度。
2. 由于動態語言的特性,他們多半無需程序員太多關心內存管理,所有的內存分配和回收都由一個叫做GC(Garbage Collector)的組件完成。雖然通過IL2CPP以后代碼變成了靜態的C++,但是內存管理這塊還是遵循C#的方式,這也是為什么最后還要有一個 IL2CPP VM的原因:它負責提供諸如GC管理,線程創建這類的服務性工作。但是由于去除了IL加載和動態解析的工作,使得IL2CPP VM可以做的很小,并且使得游戲載入時間縮短。
3.由于C++是一門靜態語言,這就意味著我們不能使用動態語言的那些酷炫特性。運行時生 成代碼并執行肯定是不可能了。這就是Unity里面提到的所謂AOT(Ahead Of Time)編譯而非JIT(Just In Time)編譯。其實很多平臺出于安全的考慮是不允許JIT的,大家最熟悉的有iOS平臺,在Console游戲機上,不管是微軟的Xbox360, XboxOne,還是Sony的PS3,PS4,PSV,沒有一個是允許JIT的。使用了IL2CPP,就完全是AOT方式了,如果原來使用了動態特性的 代碼肯定會編譯失敗。這些代碼在編譯iOS平臺的時候天生也會失敗,所以如果你是為iOS開發的游戲代碼,就不用擔心了。因此就這點而言,我們開發上幾乎 不會感到什么問題。
原文鏈接:【Unity游戲開發】Mono和IL2CPP的區別 - 知乎 (zhihu.com)
二、Mono介紹
Mono是一個由 Xamarin公司所主持的自由開放源碼項目。
Mono的目標是在盡可能多的平臺上使.net標準的東西能正常運行的一套工具,核心在于“跨平臺的讓.net代碼能運行起來“。
Mono組成組件:C# 編譯器,CLI虛擬機,以及核心類別程序庫。
Mono的編譯器 負責生成符合公共語言規范的映射代碼,即公共中間語言(Common Intermediate Language, CIL),我的理解就是工廠方法實現不同解析。
IL科普
IL的全稱是 Intermediate Language,很多時候還會看到 CIL(特指在.Net平臺下的IL標準)。翻譯過來就是中間語言。
它是一種屬于通用語言架構和.NET框架的低階的人類可讀的編程語言。
CIL類似一個面向對象的匯編語言,并且它是完全基于堆棧的,它運行在虛擬機上(.Net Framework, Mono VM)的語言。
2.1 工作流程
- 通過C#編譯器mcs,將C#編譯為IL(中間語言,byte code)
- 通過Mono運行時中的編譯器將IL編譯成對應平臺的原生碼
2.2 知識點
2.2.1. 編譯器
C#編譯器mcs:將C#編譯為 IL
Mono Runtime編譯器:將IL轉移為 原生碼。
2.2.2. 三種轉譯方式
即時編譯(Just in time,JIT):程序運行過程中,將CIL的byte code轉譯為目標平臺的原生碼。
提前編譯(Ahead of time,AOT):程序運行之前,將.exe或.dll文件中的CIL的byte code部分轉譯為目標平臺的原生碼并且存儲,程序運行中仍有部分CIL的byte code需要JIT編譯。
完全靜態編譯(Full ahead of time,Full-AOT):程序運行前,將所有源碼編譯成目標平臺的原生碼。
2.2.3 Unity跨平臺的原理
Mono運行時編譯器支持將IL代碼轉為對應平臺原生碼
IL可以在任何支持CLI,通用語言環境結構)中運行,IL的運行是依托于Mono運行時。
2.2.4 IOS不支持jit編譯原因
機器碼被禁止映射到內存,即封存了內存的可執行權限,變相的封鎖了jit編譯方式. 詳情見
2.2.5 JIT編譯
將IL代碼轉為對應平臺原生碼并且將原生碼映射到虛擬內存中執行。JIT編譯的時候IL是在依托Mono運行時,轉為對應的原生碼后在依托本地運行。
2.3 優點
- 構建應用非常快
- 由于Mono的JIT(Just In Time compilation ) 機制, 所以支持更多托管類庫
- 支持運行時代碼執行
三、IL2CPP【AOT編譯】
IL2CPP分為兩個獨立的部分:
1. AOT(靜態編譯)編譯器:把IL中間語言轉換成CPP文件
2. 運行時庫:例如 垃圾回收、線程/文件獲取(獨立于平臺,與平臺無關)、內部調用直接修改托管數據結構的原生代碼的服務與抽象
3.1 AOT編譯器
IL2CPP AOT編譯器名為il2cpp.exe。
在Windows上,您可以在Editor \ Data \ il2cpp
目錄中找到它。
在OSX上,它位于Unity安裝的Contents / Frameworks / il2cpp / build
目錄中
il2cpp.exe 是由C#編寫的受托管的可執行程序,它接受我們在Unity中通過Mono編譯器生成的托管程序集,并生成指定平臺下的C++代碼。
IL2CPP工具鏈:
3.2 運行時庫
IL2CPP技術的另一部分是運行時庫(libil2cpp),用于支持IL2CPP虛擬機的運行。
這個簡單且可移植的運行時庫是IL2CPP技術的主要優勢之一!
通過查看我們隨Unity一起提供的libil2cpp的頭文件,您可以找到有關libil2cpp代碼組織方式的一些線索
您可以在Windows的Editor \ Data \ PlaybackEngines \ webglsupport \ BuildTools \ Libraries \ libil2cpp \ include
目錄中找到它們
或OSX上的Contents / Frameworks / il2cpp / libil2cpp
目錄。
3.3 為啥要轉成CPP呢?
- 運行效率快
根據官方的實驗數據,換成IL2CPP以后,程序的運行效率有了1.5-2.0倍的提升。
2. Mono VM在各個平臺移植,維護非常耗時,有時甚至不可能完成
Mono的跨平臺是通過Mono VM實現的,有幾個平臺,就要實現幾個VM,像Unity這樣支持多平臺的引擎,Mono官方的VM肯定是不能滿足需求的。所以針對不同的新平臺,Unity的項目組就要把VM給移植一遍,同時解決VM里面發現的bug。這非常耗時耗力。這些能移植的平臺還好說,還有比如WebGL這樣基于瀏覽器的平臺。要讓WebGL支持Mono的VM幾乎是不可能的。
3. 可以利用現成的在各個平臺的C++編譯器對代碼執行編譯期優化,這樣可以進一步減小最終游戲的尺寸并提高游戲運行速度。
4. 由于動態語言的特性,他們多半無需程序員太多關心內存管理,所有的內存分配和回收都由一個叫做GC(Garbage Collector)的組件完成。
雖然通過IL2CPP以后代碼變成了靜態的C++,但是內存管理這塊還是遵循C#的方式,這也是為什么最后還要有一個?IL2CPP VM的原因:它負責提供諸如GC管理,線程創建這類的服務性工作。
但是由于去除了IL加載和動態解析的工作,使得IL2CPP VM可以做的很小,并且使得游戲載入時間縮短。
3.5 優點
- 相比Mono, 代碼生成有很大的提高
- 可以調試生成的C++代碼
- 可以啟用引擎代碼剝離(Engine code stripping)來減少代碼的大小
- 程序的運行效率比Mono高,運行速度快
- 多平臺移植非常方便
- 相比Mono構建應用慢
- 只支持AOT(Ahead of Time)編譯
四、Mono與IL2CPP的區別
IL2CPP比較適合開發和發布項目 ,但是為了提高版本迭代速度,可以在開發期間切換到Mono模式(構建應用快)。