官术网_书友最值得收藏!

2.3 Unity 3D為何能跨平臺?聊聊CIL

在日常的工作中,筆者發現很多從事Unity 3D開發的朋友,一直對Unity 3D的跨平臺能力很好奇。那么到底是什么原理使Unity 3D可以跨平臺呢?帶著這個問題,讓我們來看看Mono的貢獻,然后再進一步了解CIL(Common Intermediate Language,通用中間語言,也叫MSIL微軟中間語言)的作用。

2.3.1 Unity 3D為何能跨平臺

如果筆者或者讀者來做,應該怎么實現一套代碼對應多種平臺呢?其實原理想想也簡單,生活中也有很多可以參考的例子。現實生活中“跨平臺”的例子,如圖2-8所示。

圖2-8 現實生活中“跨平臺”的例子

像這樣一根連接(傳輸)線,無論目標設備是安卓手機還是蘋果手機,都能為手機充電。所以從這個意義上來說,這根連接(傳輸)線也實現了跨平臺。那么我們能從它身上獲得什么靈感呢?那就是從一樣的能(電)源到不同的平臺(iOS、安卓)之間需要一個中間層過渡轉換一下。

那么再回到Unity 3D為何能跨平臺的問題上,簡而言之,其實原理在于使用了叫CIL(Common Intermediate Language,通用中間語言,也叫MSIL微軟中間語言)的一種代碼指令集。CIL可以在任何支持CLI(Common Language Infrastructure,通用語言基礎結構)的環境中運行,就像.NET是微軟對這一標準的實現,Mono則是對CIL的又一實現。由于CIL能運行在所有支持CIL的環境中,例如剛剛提到的.NET運行時以及Mono運行時,也就是說和具體的平臺或者CPU無關。這樣就無須根據平臺的不同而部署不同的內容了。所以,原來在使用Unity 3D開發游戲的過程中,代碼的編譯只需要分為兩部分就可以了:第一部分是從代碼本身到CIL的編譯(其實之后CIL還會被編譯成一種位元碼,生成一個CLI assembly);第二部分是運行時從CIL(其實是CLI assembly,不過為了直觀理解,此處不必糾結這種細節)到本地指令的即時編譯(這就引出了為何Unity 3D官方沒有提供熱更新的原因:在iOS平臺中Mono無法使用JIT引擎,而是以Full AOT模式運行的,所以此處說的即時編譯不包括iOS平臺)。

2.3.2 CIL是什么

CIL是指令集,但是不是太模糊了呢?不妨先通過工具來看看CIL。而這個工具就是——ildasm。下面就通過編譯一個簡單的C#文件來看看生成的CIL代碼。

C#部分的代碼如下。

與其對應的CIL代碼如下。

代碼雖然簡單,但是也能說明足夠多的問題。那么和CIL的第一次接觸,能給我們留下什么直觀的印象呢?

? 以“.”(一個點號)開頭的,例如.class、.method,稱為CIL指令(directive),用于描述.NET程序集總體結構的標記。為什么需要它呢?因為你總得告訴編譯器你處理的是什么。

? 在CIL代碼中還看到了private、public,暫時稱為CIL特性(attribute)。它的作用也很好理解,通過CIL指令并不能完全說明.NET成員和類,針對CIL指令進行補充說明成員或者類的特性的,常見的還有extends、implements等。

? 每一行CIL代碼基本都有的就是CIL操作碼。

對CIL有了直觀的印象,但是要弄明白Unity 3D為何能跨平臺,還需要進一步的學習。

參照CIL的操作碼表,可以總結出一份更易讀的表格,如圖2-9所示。希望讀者朋友們可以認真讀表。

圖2-9 CIL操作碼表

基于堆棧

筆者的第一感覺就是基本每一條描述中都包含一個“棧”。CIL是基于堆棧的,也就是說CIL的VM(Mono運行時)是一個棧式機。這就意味著數據是推入堆棧,通過堆棧來操作的,而非通過CPU的寄存器來操作的,這更加驗證了其和具體的CPU架構沒有關系。為了說明這一點,舉個例子。

上大學學單片機時,使用匯編語言做加法的代碼如下。

其中的eax是什么——寄存器。所以,如果CIL處理數據要通過CPU的寄存器,那也就不可能和CPU的架構無關了。

當然,CIL之所以是基于堆棧而非CPU的另一個原因,是相較于CPU的寄存器,操作堆棧實在太簡單了。大學時學單片機,在學的時候需要記得各種寄存器、各種標志位、各種操作,而堆棧只需要簡單地壓棧和彈出,因此對于虛擬機的實現來說是再合適不過了。所以想要更具體地了解CIL基于堆棧這一點,讀者可以自學一下堆棧方面的內容,這里就不再贅述。

? 面向對象。

表中有new對象的語句,CIL同樣是面向對象的。

這意味著什么呢?那就是在CIL中你可以創建對象、調用對象的方法、訪問對象的成員。而這里需要注意的就是對方法的調用。

圖2-9的右上角就是對參數的操作部分。靜態方法和實例方法是不同的。

? 靜態方法:ldarg.0沒有被占用,所以參數從ldarg.0開始。

? 實例方法:ldarg.0是被this占用的,也就是說實際上的參數是從ldarg.1開始的。

舉個例子,假設有一個叫Murong的類中有一個靜態方法Add(int32 a,int32 b),實現的內容是使兩個數相加,所以需要兩個參數。另一個實例方法TellName(string name),這個方法會告訴你傳入的名字。

Murong的代碼如下。

分別來看看CIL語言對靜態方法和實例方法的不同處理。

? 靜態方法的處理。

靜態方法Add的CIL代碼如下。

調用這個靜態函數的代碼如下。

對應的CIL代碼如下。

可見CIL直接調用了Murong的Add方法,而不需要一個Murong的實例。

? 實例方法的處理。

Murong類中的實例方法TellName()的CIL代碼如下。

第一個參數對應的是ldarg.1中的參數1,而不是靜態方法中的參數0。因為此時參數0相當于this,this是不用參與參數傳遞的。

再看看調用實例方法的C#代碼和對應的CIL代碼,對應的C#代碼如下。

對應的CIL代碼如下。

由于篇幅限制,CIL是什么的問題大概介紹到這里。下面將會介紹Unity 3D是如何通過CIL來實現跨平臺的。

2.3.3 Unity 3D如何使用CIL跨平臺

Q:知道了Unity 3D能跨平臺是因為存在著一個中間語言CIL,這也是所謂跨平臺的前提,但是為什么CIL能“通吃”各大平臺呢?當然可以說CIL基于堆棧,與CPU怎么架構沒關系,但是感覺過于理論化、學術化,那還有沒有通俗化、工程化的說法呢?

A:原因就是前面提到過的.NET運行時和Mono運行時。也就是說CIL語言其實是運行在虛擬機中的,具體到Unity 3D游戲引擎也就是Mono的運行時了,換言之Mono運行的其實是CIL語言,CIL也并非真正在本地運行,而是在Mono運行時中運行,運行在本地的是被編譯后生成的原生代碼。

因此這里為了“實現跨平臺式的演示”,使用OS X系統做個測試,代碼如下。

在OSX系統上通過最簡單的文本編輯器,輸入上述代碼并保存為.cs文件。這里使用Test.cs作為這個示例的名字。這樣就有一個最基本的C#文件,如果在OSX系統上直接使用Mono來運行這個文件會發生什么結果呢?使用Mono直接運行.cs文件,如圖2-10所示。

圖2-10 使用Mono直接運行.cs文件

文件沒有包含一個CIL映像。可見Mono是不能直接運行.cs文件的。假如把它編譯成CIL呢?那么用Mono帶的mcs來編譯Test.cs文件,代碼如下。

生成的內容如圖2-11所示。

圖2-11 使用Mono的mcs編譯器在Mac上生成的.exe文件

沒有.IL文件生成,反而多了一個.exe文件。可是OS X系統不能運行.exe文件,但是為什么生成了.exe文件呢?真相其實就是這個.exe文件并不是直接讓OS X系統來運行的,而是留給Mono運行時來運行的。換言之,這個文件的可執行代碼形式是CIL的位元碼形態。這樣就完成了從C#到CIL的過程。接下來就運行下剛剛的成果,代碼如下。

再次使用Mono,只不過這次的目標換成了Test.exe文件,如圖2-12所示。結果是輸出了一個“Hi”。

圖2-12 在OS X系統上運行.exe文件

? 從CIL到Native Code

為什么C#寫的代碼能在Mac上運行呢?這就不得不提從CIL如何到本機原生代碼的過程了。Mono提供了兩種編譯方式,就是經常能看到的JIT(Just-in-time compilation,即時編譯)和AOT(Ahead-of-time,提前編譯或靜態編譯)。這兩種編譯方式都是將CIL進一步編譯成平臺的原生代碼。這也是實現跨平臺的最后一步。

? JIT即時編譯

即時編譯,或者稱為動態編譯,是在程序執行時才編譯代碼,解釋一條語句執行一條語句,即將一條中間的托管語句翻譯成一條機器語句,然后執行這條機器語句。但同時也會將編譯過的代碼進行緩存,而不是每一次都進行編譯。所以可以說它是靜態編譯和解釋器的結合體。不過機器既要處理代碼的邏輯,同時還要進行編譯的工作,所以其運行時的效率肯定是受到影響的。因此,Mono會有一部分代碼通過AOT靜態編譯,以解決在程序運行時JIT動態編譯在效率上的問題。

不過一向嚴苛的iOS平臺是不允許這種動態的編譯方式的,這也是Unity官方無法給出熱更新方案的一個原因。而Android平臺恰恰相反,Dalvik虛擬機使用的就是JIT方案。

? AOT靜態編譯

其實Mono的AOT靜態編譯和JIT并非對立的。AOT同樣使用了JIT編譯器來進行編譯,只不過被AOT編譯的代碼在程序運行之前就已經編譯好了。當然,還有一部分代碼會通過JIT來進行動態編譯。下面就手動操作一下Mono,讓它進行一次AOT靜態編譯,代碼如下。

這條命令運行的結果,如圖2-13所示。

圖2-13 Mono的AOT靜態編譯運行結果

從圖2-13可以看到JIT time:39 ms,也就是說Mono的AOT模式其實會使用到JIT編譯器,同時可以看到生成了一個適應Mac的動態庫Test.exe.dylib,而在Linux中生成則是.so(共享庫)。

AOT靜態編譯出來的庫,除了包括代碼之外,還有被緩存的元數據(Metodata,是一種二進制信息,用以對存儲在公共語言運行庫可移值可執行文件(PE文件)或存儲在內存中的程序進行描述)信息,所以甚至可以只編譯元數據信息而不編譯代碼。例如如下所示代碼。

再次運行的結果,如圖2-14所示。可見代碼沒有被包括進來。

圖2-14 只編譯元數據的運行結果

簡單總結一下AOT的過程。

(1)收集要被編譯的方法。

(2)使用JIT編譯器進行編譯。

(3)發射(Emitting)經JIT編譯過的代碼和其他信息。

(4)直接生成文件,或者調用本地匯編器或連接器進行處理后生成文件(例如圖2-14中使用了本地的gcc)。

? Full AOT

iOS平臺是禁止使用JIT的,但是Mono的AOT模式仍然會保留一部分代碼在程序運行時動態編譯。所以為了解決這個問題,Mono提供了一個被稱為Full AOT的模式。即預先對程序集中的所有CIL代碼進行AOT編譯生成一個本地代碼映像,然后在運行時直接加載這個映像,而不再使用JIT引擎。目前由于技術或實現上的原因,在使用Full AOT時有一些限制,所以這里不再贅述。

? 總結

本節的主要內容總結有以下幾點。

(1)CIL(Common Intermediate Language,通用中間語言)是CLI(Common Language Infrastructure,通用語言基礎結構)標準定義的一種可讀性較低的語言。

(2)以.NET或Mono等實現CLI標準的運行環境為目標的語言要先編譯成CIL,然后CIL會被編譯,并且以位元碼的形式存在(源代碼→中間語言的過程)。

(3)這種位元碼運行在虛擬機中(.NET Mono的運行時)。

(4)這種位元碼可以被進一步編譯成不同平臺的原生代碼(中間語言→原生代碼的過程)。

(5)面向對象。

(6)基于堆棧。

主站蜘蛛池模板: 开远市| 铁力市| 清苑县| 丽水市| 西充县| 南通市| 玉龙| 乌鲁木齐县| 西畴县| 嵩明县| 白水县| 鄂托克前旗| 旬阳县| 永登县| 探索| 鄄城县| 会昌县| 崇仁县| 高碑店市| 繁昌县| 玛沁县| 长沙市| 海阳市| 龙岩市| 屏南县| 博爱县| 临西县| 象州县| 彭阳县| 蓬莱市| 米易县| 若尔盖县| 宁都县| 揭西县| 临湘市| 阜平县| 行唐县| 贵阳市| 泰和县| 渑池县| 湘阴县|