Unity3D为何能跨平台?聊聊CIL(MSIL)
前言
其實(shí)小匹夫在U3D的開發(fā)中一直對U3D的跨平臺能力很好奇。到底是什么原理使得U3D可以跨平臺呢?后來發(fā)現(xiàn)了Mono的作用,并進(jìn)一步了解到了CIL的存在。所以,作為一個對Unity3D跨平臺能力感興趣的U3D程序猿,小匹夫如何能不關(guān)注CIL這個話題呢?那么下面各位看官就拾起語文老師教導(dǎo)我們的作文口訣(Why,What,How),和小匹夫一起走進(jìn)CIL的世界吧~
Why?
回到本文的題目,U3D或者說Mono的跨平臺是如何做到的?
如果換做小匹夫或者看官你來做,應(yīng)該怎么實(shí)現(xiàn)一套代碼對應(yīng)多種平臺呢?
其實(shí)原理想想也簡單,生活中也有很多可以參考的例子,比如下圖(誰讓小匹夫是做移動端開發(fā)的呢,只能物盡其用從自己身邊找例子了T.T):
像這樣一根線,管你是安卓還是ios都能充電。所以從這個意義上,這貨也實(shí)現(xiàn)了跨平臺。那么我們能從它身上學(xué)到什么呢?對的,那就是從一樣的能源(電)到不同的平臺(ios,安卓)之間需要一個中間層過度轉(zhuǎn)換一下。
那么來到U3D為何能跨平臺,簡而言之,其實(shí)現(xiàn)原理在于使用了叫CIL(Common Intermediate Language通用中間語言,也叫做MSIL微軟中間語言)的一種代碼指令集,CIL可以在任何支持CLI(Common Language Infrastructure,通用語言基礎(chǔ)結(jié)構(gòu))的環(huán)境中運(yùn)行,就像.NET是微軟對這一標(biāo)準(zhǔn)的實(shí)現(xiàn),Mono則是對CLI的又一實(shí)現(xiàn)。由于CIL能運(yùn)行在所有支持CLI的環(huán)境中,例如剛剛提到的.NET運(yùn)行時以及Mono運(yùn)行時,也就是說和具體的平臺或者CPU無關(guān)。這樣就無需根據(jù)平臺的不同而部署不同的內(nèi)容了。所以到這里,各位也應(yīng)該恍然大了。代碼的編譯只需要分為兩部分就好了嘛:
從代碼本身到CIL的編譯(其實(shí)之后CIL還會被編譯成一種位元碼,生成一個CLI assembly)
運(yùn)行時從CIL(其實(shí)是CLI assembly,不過為了直觀理解,不必糾結(jié)這種細(xì)節(jié))到本地指令的即時編譯(這就引出了為何U3D官方?jīng)]有提供熱更新的原因:在iOS平臺中Mono無法使用JIT引擎,而是以Full AOT模式運(yùn)行的,所以此處說的即時編譯不包括IOS)
What?
上文也說了CIL是指令集,但是不是還是太模糊了呢?所以語文老師教導(dǎo)我們,描述一個東西時肯定要先從外貌寫起。遵循老師的教導(dǎo),我們不妨先通過工具來看看CIL到底長什么樣。
工具就是ildasm了。下面小匹夫?qū)懸粋€簡單的.cs看看生成的CIL代碼長什么樣。
C#代碼:
class Class1 {public static void Main(string[] args){System.Console.WriteLine("hi");} }CIL代碼:
.class private auto ansi beforefieldinit Class1extends [mscorlib]System.Object {.method public hidebysig static void Main(string[] args) cil managed{.entrypoint// 代碼大小 13 (0xd).maxstack 8IL_0000: nopIL_0001: ldstr "hi"IL_0006: call void [mscorlib]System.Console::WriteLine(string)IL_000b: nopIL_000c: ret} // end of method Class1::Main.method public hidebysig specialname rtspecialname instance void .ctor() cil managed{// 代碼大小 7 (0x7).maxstack 8IL_0000: ldarg.0IL_0001: call instance void [mscorlib]System.Object::.ctor()IL_0006: ret} // end of method Class1::.ctor} // end of class Class1好啦。代碼雖然簡單,但是也能說明足夠多的問題。那么和CIL的第一次親密接觸,能給我們留下什么直觀的印象呢?
以“.”一個點(diǎn)號開頭的,例如上面這份代碼中的:.class、.method 。我們稱之為CIL指令(directive),用于描述.NET程序集總體結(jié)構(gòu)的標(biāo)記。為啥需要它呢?因?yàn)槟憧偟酶嬖V編譯器你處理的是啥吧。
貌似CIL代碼中還看到了private、public這樣的身影。姑且稱之為CIL特性(attribute)。它的作用也很好理解,通過CIL指令并不能完全說明.NET成員和類,針對CIL指令進(jìn)行補(bǔ)充說明成員或者類的特性的。市面上常見的還有:extends,implements等等。
每一行CIL代碼基本都有的,對,那就是CIL操作碼咯。小匹夫從網(wǎng)上找了一份漢化的操作碼表放在附錄部分,當(dāng)然英文版的你的vs就有。
直觀的印象有了,但是離我們的短期目標(biāo),說清楚(或者說介紹個大概)CIL是What,甚至是終極目標(biāo),搞明白Mono為何能跨平臺還有2萬4千9百里的距離。
好啦,話不多說,繼續(xù)亂侃。
參照附錄中的操作碼表,對照可以總結(jié)出一份更易讀的表格。那就是如下的表啦。
在此,小匹夫想請各位認(rèn)真讀表,然后心中默數(shù)3個數(shù),最后看看都能發(fā)現(xiàn)些什么。
基于堆棧
如果是小匹夫的話,第一感覺就是基本每一條描述中都包含一個“棧”。不錯,CIL是基于堆棧的,也就是說CIL的VM(mono運(yùn)行時)是一個棧式機(jī)。這就意味著數(shù)據(jù)是推入堆棧,通過堆棧來操作的,而非通過CPU的寄存器來操作,這更加驗(yàn)證了其和具體的CPU架構(gòu)沒有關(guān)系。為了說明這一點(diǎn),小匹夫舉個例子好啦。
大學(xué)時候?qū)W單片機(jī)的時候記得做加法大概是這樣的:
add eax,-2其中的eax是啥?寄存器。所以如果CIL處理數(shù)據(jù)要通過cpu的寄存器的話,那也就不可能和cpu的架構(gòu)無關(guān)了。
當(dāng)然,CIL之所以是基于堆棧而非CPU的另一個原因是相比較于cpu的寄存器,操作堆棧實(shí)在太簡單了。回到剛才小匹夫說的大學(xué)時候曾經(jīng)學(xué)過的單片機(jī)那門課程上,當(dāng)時記得各種寄存器,各種標(biāo)志位,各種。。。,而堆棧只需要簡單的壓棧和彈出,因此對于虛擬機(jī)的實(shí)現(xiàn)來說是再合適不過了。所以想要更具體的了解CIL基于堆棧這一點(diǎn),各位可以去看一下堆棧方面的內(nèi)容。這里小匹夫就不拓展了。
面向?qū)ο?/h4>
那么第二感覺呢?貌似附錄的表中有new對象的語句呀。嗯,的確,CIL同樣是面向?qū)ο?/strong>的。
這意味著什么呢?那就是在CIL中你可以創(chuàng)建對象,調(diào)用對象的方法,訪問對象的成員。而這里需要注意的就是對方法的調(diào)用。
回到上表中的右上角。對,就是對參數(shù)的操作部分。靜態(tài)方法和實(shí)例方法是不同的哦~
靜態(tài)方法:ldarg.0沒有被占用,所以參數(shù)從ldarg.0開始。
實(shí)例方法:ldarg.0是被this占用的,也就是說實(shí)際上的參數(shù)是從ldarg.1開始的。
舉個例子:假設(shè)你有一個類Murong中有一個靜態(tài)方法Add(int32 a, int32 b),實(shí)現(xiàn)的內(nèi)容就如同它的名字一樣使兩個數(shù)相加,所以需要2個參數(shù)。和一個實(shí)例方法TellName(string name),這個方法會告訴你傳入的名字。
class Murong {public void TellName(string name){System.Console.WriteLine(name);}public static int Add(int a, int b){return a + b;} }靜態(tài)方法的處理:
那么其中的靜態(tài)方法Add的CIL代碼如下:
//小匹夫注釋一下。 .method public hidebysig static int32 Add(int32 a,int32 b) cil managed {// 代碼大小 9 (0x9).maxstack 2.locals init ([0] int32 CS$1$0000) //初始化局部變量列表。因?yàn)槲覀冎环祷亓艘粋€int型。所以這里聲明了一個int32類型。索引為0IL_0000: nopIL_0001: ldarg.0 //將索引為 0 的參數(shù)加載到計(jì)算堆棧上。IL_0002: ldarg.1 //將索引為 1 的參數(shù)加載到計(jì)算堆棧上。IL_0003: add //計(jì)算IL_0004: stloc.0 //從計(jì)算堆棧的頂部彈出當(dāng)前值并將其存儲到索引 0 處的局部變量列表中。IL_0005: br.s IL_0007IL_0007: ldloc.0 //將索引 0 處的局部變量加載到計(jì)算堆棧上。IL_0008: ret //返回該值 } // end of method Murong::Add那么我們調(diào)用這個靜態(tài)函數(shù)應(yīng)該就是這樣咯。
Murong.Add(1, 2);對應(yīng)的CIL代碼為:
IL_0001: ldc.i4.1 //將整數(shù)1壓入棧中IL_0002: ldc.i4.2 //將整數(shù)2壓入棧中IL_0003: call int32 Murong::Add(int32,int32) //調(diào)用靜態(tài)方法可見CIL直接call了Murong的Add方法,而不需要一個Murong的實(shí)例。
實(shí)例方法的處理:
Murong類中的實(shí)例方法TellName()的CIL代碼如下:
.method public hidebysig instance void TellName(string name) cil managed {// 代碼大小 9 (0x9).maxstack 8IL_0000: nopIL_0001: ldarg.1 //看到和靜態(tài)方法的區(qū)別了嗎?IL_0002: call void [mscorlib]System.Console::WriteLine(string)IL_0007: nopIL_0008: ret } // end of method Murong::TellName看到和靜態(tài)方法的區(qū)別了嗎?對,第一個參數(shù)對應(yīng)的是ldarg.1中的參數(shù)1,而不是靜態(tài)方法中的0。因?yàn)榇藭r參數(shù)0相當(dāng)于this,this是不用參與參數(shù)傳遞的。
那么我們再看看調(diào)用實(shí)例方法的C#代碼和對應(yīng)的CIL代碼是如何的。
C#:
//C# Murong murong = new Murong(); murong.TellName("chenjiadong");CIL:
.locals init ([0] class Murong murong) //因?yàn)镃#代碼中定義了一個Murong類型的變量,所以局部變量列表的索引0為該類型的引用。 //.... IL_0009: newobj instance void Murong::.ctor() //相比上面的靜態(tài)方法的調(diào)用,此處new一個新對象,出現(xiàn)了instance方法。 IL_000e: stloc.0 IL_000f: ldloc.0 IL_0010: ldstr "chenjiadong" //小匹夫的名字入棧 IL_0015: callvirt instance void Murong::TellName(string) //實(shí)例方法的調(diào)用也有instance到此,受制于篇幅所限(小匹夫不想寫那么多字啊啊啊!)CIL是What的問題大致介紹一下。當(dāng)然沒有再拓展,以后小匹夫可能會再詳細(xì)寫一下這塊。
How?
記得語文老師說過,寫作文最重要的一點(diǎn)是要首尾呼應(yīng)。既然咱們開篇就提出了U3D為何能跨平臺的問題,那么接近文章的結(jié)尾咱們就再來
提問:
Q:上面的Why部分,咱們知道了U3D能跨平臺是因?yàn)榇嬖谥粋€能通吃的中間語言CIL,這也是所謂跨平臺的前提,但是為啥CIL能通吃各大平臺呢?當(dāng)然可以說CIL基于堆棧,跟你CPU怎么架構(gòu)的沒啥關(guān)系,但是感覺過于理論化、學(xué)術(shù)化,那還有沒有通俗化、工程化的說法呢?
A:原因就是前面小匹夫提到過的,.Net運(yùn)行時和Mono運(yùn)行時。也就是說CIL語言其實(shí)是運(yùn)行在虛擬機(jī)中的,具體到咱們的U3D也就是mono的運(yùn)行時了,換言之mono運(yùn)行的其實(shí)CIL語言,CIL也并非真正的在本地運(yùn)行,而是在mono運(yùn)行時中運(yùn)行的,運(yùn)行在本地的是被編譯后生成的原生代碼。當(dāng)然看官博的文章,他們似乎也在開發(fā)自己的“mono”,也就是被稱為腳本的未來的IL2Cpp,這種類似運(yùn)行時的功能是將IL再編譯成c++,再由c++編譯成原生代碼,據(jù)說效率提升很可觀,小匹夫也是蠻期待的。
這里為了“實(shí)現(xiàn)跨平臺式的演示”,小匹夫用mac給各位做個測試好啦:
從C#到CIL
新建一個cs文件,然后使用mono來運(yùn)行。這個cs文件內(nèi)容如下:
然后咱們直接在命令行中運(yùn)行這個cs文件試試~
說的很清楚,文件沒有包含一個CIL映像。可見mono是不能直接運(yùn)行cs文件的。假如我們把它編譯成CIL呢?那么我們用mono帶的mcs來編譯小匹夫的Test.cs文件。
mcs Test.cs生成了什么呢?如圖:
好像沒見有叫.IL的文件生成啊?反而好像多了一個.exe文件?可是沒聽說Mac能運(yùn)行exe文件呀?可為啥又生成了.exe呢?各位看官可能要說,小匹夫你是不是拿windows截圖P的啊?嘿嘿,小匹夫可不敢。辣么真相其實(shí)就是這個exe并不是讓Mac來運(yùn)行的,而是留給mono運(yùn)行時來運(yùn)行的,換言之這個文件的可執(zhí)行代碼形式是CIL的位元碼形態(tài)。到此,我們完成了從C#到CIL的過程。接下來就讓我們運(yùn)行下剛剛的成果好啦。
mono Test.exe?
結(jié)果是輸出了一個大大的“Hi”。這里,就引出了下一個部分。
從CIL到Native Code
這個“HI”可是在小匹夫的MAC終端上出現(xiàn)的呀,那么就證明這個C#寫的代碼在MAC上運(yùn)行的還挺“嗨”。
為啥呢?為啥C#寫的代碼能跑在MAC上呢?這就不得不提從CIL如何到本機(jī)原生代碼的過程了。Mono提供了兩種編譯方式,就是我們經(jīng)常能看到的:JIT(Just-in-Time compilation,即時編譯)和AOT(Ahead-of-Time,提前編譯或靜態(tài)編譯)。這兩種方式都是將CIL進(jìn)一步編譯成平臺的原生代碼。這也是實(shí)現(xiàn)跨平臺的最后一步。下面就分頭介紹一下。
JIT即時編譯:
從名字就能看的出來,即時編譯,或者稱之為動態(tài)編譯,是在程序執(zhí)行時才編譯代碼,解釋一條語句執(zhí)行一條語句,即將一條中間的托管的語句翻譯成一條機(jī)器語句,然后執(zhí)行這條機(jī)器語句。但同時也會將編譯過的代碼進(jìn)行緩存,而不是每一次都進(jìn)行編譯。所以可以說它是靜態(tài)編譯和解釋器的結(jié)合體。不過你想想機(jī)器既要處理代碼的邏輯,同時還要進(jìn)行編譯的工作,所以其運(yùn)行時的效率肯定是受到影響的。因此,Mono會有一部分代碼通過AOT靜態(tài)編譯,以降低在程序運(yùn)行時JIT動態(tài)編譯在效率上的問題。
不過一向嚴(yán)苛的IOS平臺是不允許這種動態(tài)的編譯方式的,這也是U3D官方無法給出熱更新方案的一個原因。而Android平臺恰恰相反,Dalvik虛擬機(jī)使用的就是JIT方案。
AOT靜態(tài)編譯:
其實(shí)Mono的AOT靜態(tài)編譯和JIT并非對立的。AOT同樣使用了JIT來進(jìn)行編譯,只不過是被AOT編譯的代碼在程序運(yùn)行之前就已經(jīng)編譯好了。當(dāng)然還有一部分代碼會通過JIT來進(jìn)行動態(tài)編譯。下面小匹夫就手動操作一下mono,讓它進(jìn)行一次AOT編譯。
//在命令行輸入 mono --aot Test.exe結(jié)果:
從圖中可以看到JIT time: 39 ms,也就是說Mono的AOT模式其實(shí)會使用到JIT,同時我們看到了生成了一個適應(yīng)小匹夫的MAC的動態(tài)庫Test.exe.dylib,而在Linux生成就是.so(共享庫)。
AOT編譯出來的庫,除了包括我們的代碼之外,還有被緩存的元數(shù)據(jù)信息。所以我們甚至可以只編譯元數(shù)據(jù)信息而不編譯代碼。例如這樣:
//只包含元數(shù)據(jù)的信息 mono --aot=metadata-only Test.exe
可見代碼沒有被包括進(jìn)來。
那么簡單總結(jié)一下AOT的過程:
收集要被編譯的方法
使用JIT進(jìn)行編譯
發(fā)射(Emitting)經(jīng)JIT編譯過的代碼和其他信息
直接生成文件或者調(diào)用本地匯編器或連接器進(jìn)行處理之后生成文件。(例如上圖中使用了小匹夫本地的gcc)
Full AOT
當(dāng)然上文也說了,IOS平臺是禁止使用JIT的,可看樣子Mono的AOT模式仍然會保留一部分代碼會在程序運(yùn)行時動態(tài)編譯。所以為了破解這個問題,Mono提供了一個被稱為Full AOT的模式。即預(yù)先對程序集中的所有CIL代碼進(jìn)行AOT編譯生成一個本地代碼映像,然后在運(yùn)行時直接加載這個映像而不再使用JIT引擎。目前由于技術(shù)或?qū)崿F(xiàn)上的原因在使用Full AOT時有一些限制,不過這里不再多說了。以后也還會更細(xì)的分析下AOT。
總結(jié)
好啦,寫到現(xiàn)在也已經(jīng)到了凌晨3:04分了。感覺寫的內(nèi)容也差不多了。那么對本文的主題U3D為何能跨平臺以及CIL做個最終的總結(jié)陳詞:
CIL是CLI標(biāo)準(zhǔn)定義的一種可讀性較低的語言。
以.NET或mono等實(shí)現(xiàn)CLI標(biāo)準(zhǔn)的運(yùn)行環(huán)境為目標(biāo)的語言要先編譯成CIL,之后CIL會被編譯,并且以位元碼的形式存在(源代碼--->中間語言的過程)。
這種位元碼運(yùn)行在虛擬機(jī)中(.net mono的運(yùn)行時)。
這種位元碼可以被進(jìn)一步編譯成不同平臺的原生代碼(中間語言--->原生代碼的過程)。
面向?qū)ο?/p>
基于堆棧
附錄
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Unity3D为何能跨平台?聊聊CIL(MSIL)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【BZOJ3196】Tyvj 1730
- 下一篇: Python遥感数据主成分分析