Unity熱更新哪些事

前言
本文想要給大家分享的是Unity熱更那些事兒,會帶大家了解
在打包時(shí)為什么選擇使用Mono作為腳本引擎的后臺?
JIT與Mono有什么關(guān)系?
IOS熱更新的問題
Lua如何進(jìn)行熱更新?
ILRunTime熱更新介紹
版權(quán)聲明
本文為“優(yōu)夢創(chuàng)客”原創(chuàng)文章,您可以自由轉(zhuǎn)載,但必須加入完整的版權(quán)聲明
更多學(xué)習(xí)資源請加QQ:1517069595或WX:alice17173獲?。ㄆ髽I(yè)級性能優(yōu)化/熱更新/Shader特效/服務(wù)器/商業(yè)項(xiàng)目實(shí)戰(zhàn)/每周直播/一對一指導(dǎo))
點(diǎn)贊、關(guān)注、分享可免費(fèi)獲得配套學(xué)習(xí)資源
詳細(xì)內(nèi)容可觀看文末完整視頻
議題

理想當(dāng)中的熱更新流程
現(xiàn)實(shí)中的熱更新的流程
Unity程序編譯和打包方式
Unity程序的執(zhí)行方式
IOS平臺的App為什么不能熱更新
解決方案(能支持所有平臺熱更新的通用解決方案)
什么是游戲熱更新

游戲熱更新的更新流程
從軟件商店下載游戲App并安裝到手機(jī)上,啟動手機(jī)上的游戲時(shí)會連接游戲的服務(wù)器,檢查服務(wù)器上面有沒有更新文件列表,如果有,就會把更新內(nèi)容下載下來,沒有就不更新,這是游戲玩家的熱更新流程
游戲開發(fā)者的熱更流程
1,制作游戲更新內(nèi)容(可能是新的資料包、新玩法、新道具、數(shù)值調(diào)整)
2,更新內(nèi)容制作完成后,開發(fā)者會把它們打包成Unity里的AB包并上傳到資源更新服務(wù)器上,上傳更新內(nèi)容的同時(shí)還要上傳更新目錄索引,更新目錄索引中儲存的是文件、模型、貼圖資源的索引和資源內(nèi)容
3,如何這時(shí)玩家打開游戲就會檢查更新,并下載更新資源
更新是不是直接把舊的內(nèi)容覆蓋掉或者把新增的內(nèi)容加上去后游戲就能使用更新以后的內(nèi)容?
沒有那么簡單,特別是游戲代碼熱更新,并不是把最新內(nèi)容下載完以后就能直接執(zhí)行到更新以后的內(nèi)容,中間還有一些過程和步驟,這也是本文要著重講的內(nèi)容,也就是上圖中的第六步:如何在資源下載完以后執(zhí)行熱更新
游戲熱更新的種類

游戲的熱更新的種類分為資源熱更新和代碼熱更新
資源熱更新可以參考我們的《Unity小白的游戲夢》,也可以在文末添加愛麗絲老師進(jìn)行咨詢
代碼熱更新
程序員寫的程序代碼也是一種資源,叫做腳本資源,腳本在Unity中打包以后是以動態(tài)鏈接庫文件的形式存在于磁盤上面的,所以代碼也是一種資源,可以和其他資源一樣按照統(tǒng)一步驟進(jìn)行熱更新
如何更新Unity腳本代碼?
要進(jìn)行代碼熱更新其實(shí)無非就是用新的Unity開發(fā)中的代碼所形成的同名文件去覆蓋舊的同名文件,這是熱更新最理想的情況,現(xiàn)實(shí)中并沒有這么容易
如何打包?

很簡單,點(diǎn)擊Unity文件菜單底下的Build選項(xiàng)就會彈出如上圖左邊一樣的對話框,這個(gè)對話框是打包設(shè)置對話框,點(diǎn)擊下面的Unity引擎設(shè)定(PlayerSeting)按鈕,就會打開上圖右邊的屬性面板
在屬性面板里打包時(shí)需要決定一個(gè)非常重要的選項(xiàng),就是腳本引擎后臺,也就是上圖右邊的ScriptingBackend,這個(gè)選項(xiàng)是一個(gè)下拉框,其中有兩個(gè)選項(xiàng)
1,Mono方式打包
2,IL2CPP方式打包
選擇Mono方式打包出來的程序只能支持32位程序
現(xiàn)在的電腦上預(yù)裝的Windows系統(tǒng)或者M(jìn)ac蘋果電腦,包括手機(jī)上的操作系統(tǒng)一般都會裝64位的系統(tǒng),那么32位跟64位有什么區(qū)別呢?
32位系統(tǒng)所能支持的內(nèi)存范圍比較小,只能支持4個(gè)GB的內(nèi)存,所以大部分人會選擇64位系統(tǒng),它能支持的內(nèi)存范圍比較大
如果使用Mono方式打包就只能打一個(gè)32位的系統(tǒng)包,這也就意味著你的程序雖然跑在64位的系統(tǒng)上,但它只能作為一個(gè)兼容的32位程序來運(yùn)行,最多只能支持使用4個(gè)G的內(nèi)存
對于一個(gè)大型游戲而言只使用4個(gè)G的內(nèi)存是完全不夠用的,所以要注意:使用Mono方式打包的程序不支持64位系統(tǒng)
如果把腳本后臺切換為IL2CPP方式就能夠支持64位的系統(tǒng)平臺
這兩種打包方式的區(qū)別

Mono打包方式
用Mono方式來進(jìn)行打包的程序會出現(xiàn)一堆動態(tài)鏈接庫,程序員寫的程序控制邏輯就在上圖左邊被紅框框住的Assembly-CSharp.dll動態(tài)鏈接庫里面
這個(gè)動態(tài)鏈接庫里包括了所有的功能代碼,當(dāng)要執(zhí)行程序時(shí),就必須在程序啟動之前把它加載到Mono虛擬機(jī)里面
Mono本身是一個(gè)虛擬機(jī),因?yàn)镃#本身是運(yùn)行在DotNet平臺上面的,而DotNet平臺本身就是一個(gè)基于虛擬機(jī)的的平臺,Mono虛擬機(jī)是對DotNet虛擬機(jī)的跨平臺移植
IL2CPP打包方式
使用IL2CPP方式打出的包是沒有動態(tài)連接庫的,它將Mono虛擬機(jī)和Assembly-CSharp.dll動態(tài)鏈接庫整合在了一起,放在libil2cpp.so文件里(如上圖右邊最下方的圖片)
Mono方式腳本編譯流程

Unity的項(xiàng)目當(dāng)中可以寫很多C#腳本,C#腳本在打包時(shí)會被Mono平臺中的C#源碼編譯器翻譯成一種匯編語言
在游戲運(yùn)行時(shí),這些匯編語言會跟游戲項(xiàng)目中其他的第三方的DLL一起放入Mono虛擬機(jī),由Mono虛擬機(jī)來解析并執(zhí)行這些中間匯編指令,這就是Mono方式編譯流程

C#程序經(jīng)過編譯器的被翻譯成的中間匯編語言在微軟的技術(shù)術(shù)語里叫做CIL,就是通用中間匯編語言
為什么叫通用中間匯編呢?
因?yàn)椴还苁怯肅#還是其他語言寫的腳本,它們經(jīng)過翻譯以后都是翻譯成同一種匯編指令
這也是DotNet框架非常強(qiáng)大的一點(diǎn),不管是用什么語言寫的程序,最終在虛擬機(jī)里面執(zhí)行時(shí)它執(zhí)行的都是相同的一套指令、并且這套指令是跟操作系統(tǒng)無關(guān)的
它是做到機(jī)器指令跟具體平臺是無關(guān)的?
這種匯編指令相當(dāng)于指定了一個(gè)規(guī)范,這個(gè)規(guī)范的執(zhí)行是通過CLR中間語言編譯器執(zhí)行的,它會把與具體平臺無關(guān)的指令翻譯成能夠在具體平臺上執(zhí)行的跟平臺相關(guān)的指令
所以不管使用什么樣的語言寫DoNetT程序都可以被成功編譯,因?yàn)榫幾g以后形成的是中間匯編語言,中間匯編可以運(yùn)行在各種各樣的平臺上

當(dāng)你打開Unity項(xiàng)目時(shí),VS里會形成四個(gè)子項(xiàng)目,這四個(gè)子項(xiàng)目分別是
Assembly-CSharp(由程序員寫的程序邏輯)
Assembly-CSharp-Editor(由程序員寫的一些編輯器擴(kuò)展)
Assembly-CSharp-Editor-firstpass(針對編輯器擴(kuò)展的一些插件)
Assembly-CSharp-firstpass(第三方的腳本插件)
當(dāng)Unity自動編譯或者程序員手動編譯Unity腳本時(shí),它就會形成如上圖中間所示的跟項(xiàng)目名稱相對應(yīng)的動態(tài)鏈接庫,這些動態(tài)鏈接庫里存放了程序源代碼對應(yīng)的機(jī)器指令
這些機(jī)器指令長就是右邊的第三幅圖的樣子,它是微軟匯編語言的指令集所形成的代碼

總結(jié)
CLI相當(dāng)于中間匯編語言、CIL相當(dāng)于微軟的虛擬機(jī)、CLR是微軟平臺相關(guān)的運(yùn)行時(shí)庫,各個(gè)平臺的功能都包裝在這個(gè)庫里,它們構(gòu)成了微軟的虛擬機(jī)
這個(gè)虛擬機(jī)有兩個(gè)功能
前端把C#代碼編譯成匯編語言
后端在運(yùn)行時(shí)把這些中間匯編語言翻譯成具體平臺的原生機(jī)器指令
CLR
CLR負(fù)責(zé)了平臺相關(guān)的功能,這些功能包括了進(jìn)程和線程的管理、內(nèi)存分配、垃圾收集、文件管理等
Mono虛擬機(jī)
微軟傳統(tǒng)的虛擬機(jī)叫做DotNet虛擬機(jī),DotNet平臺本身是不支持跨平臺,但經(jīng)過Mono的移植,使它能夠運(yùn)行在很多平臺上,包括常見的安卓、蘋果、BSD、Linux、Windows等等
所以Unity能支持跨平臺,其實(shí)并不是Unity自身的能力,而是利用了開源項(xiàng)目的能力

Mono虛擬機(jī)運(yùn)行匯編語言有三種方式
JIT模式
在這種模式下,虛擬機(jī)會加載動態(tài)鏈接庫文件里的匯編指令,然后進(jìn)行逐條翻譯,翻譯成針對某一個(gè)特定的手機(jī)平臺機(jī)器指令后交給CPU執(zhí)行
AOT方式
AOT方式是在程序編譯成中間匯編以后進(jìn)一步把程序直接編譯成針對特定平臺的原生機(jī)器碼,然后運(yùn)行時(shí)交給CPU執(zhí)行
這種模式下的程序都是提前編譯好的,它的缺點(diǎn)就是編譯時(shí)間長,優(yōu)點(diǎn)是運(yùn)行速度快,因?yàn)樗阉械某绦蛉刻崆熬幾g好了
這種方式還有一個(gè)問題:如果采用AOT方式,它有一部分代碼還是會在運(yùn)行時(shí)動態(tài)編譯,這就引出了第三種模式
FullAOT模式
FullAOT模式也可以叫做完全提前編譯模式,它會在程序形成中間匯編以后,把這些中間匯編全部翻譯成一些原生碼,然后在運(yùn)行時(shí)執(zhí)行
這樣就會形成一個(gè)特征,就是使用完全AOT模式編譯的代碼不會在運(yùn)行時(shí)動態(tài)生成任何代碼,這件事情究竟是好是壞呢?這要從兩方面來看
FullAOT模式的優(yōu)點(diǎn)是:安全性比較好,因?yàn)樗谶\(yùn)行時(shí)不允許動態(tài)執(zhí)行程序代碼
缺點(diǎn):如果想要熱更新一些新的程序代碼,那么用FullAOT模式就很不方便,因?yàn)樗辉试S動態(tài)更新程序代碼,而熱更新是要求在程序啟動時(shí)動態(tài)加載程序代碼的
這也是熱更新的困境所在,IOS平臺并不支持即時(shí)編譯,安卓平臺則能夠支持即時(shí)編譯,也就是說哪怕你在程序里通過熱更藏了一個(gè)病毒或者木馬,那么安卓手機(jī)照樣可以執(zhí)行,這也是它的安全問題所在
而FullAOT模式是完全禁止動態(tài)運(yùn)行的,所以你沒有機(jī)會把一些從網(wǎng)上下載的病毒木馬放到內(nèi)存里面執(zhí)行,從而保證了程序安全
安卓系統(tǒng)可以支持上面第三種模式,而IOS由于安全性考慮只支持第三種,所以IOS想要進(jìn)行熱更就不太方便
IL2CPP方式腳本編譯流程

IL2CPP方式熱更流程的前面幾個(gè)步驟跟Mono方式是一樣的,只是IL2CPP方式不是在代碼運(yùn)行時(shí)放到虛擬機(jī)去執(zhí)行,而是進(jìn)一步再編譯,用IL2CPP工具把中間匯編語言轉(zhuǎn)換成C++代碼,然后經(jīng)過C++的本機(jī)編譯器來進(jìn)行編譯
編譯完了以后會形成一些本機(jī)可執(zhí)行的匯編語言代碼機(jī)器指令,然后會把它交到IL2CPP的虛擬機(jī)來執(zhí)行
所以IL2CPP其實(shí)和Mono一樣,也是由兩部分構(gòu)成,只不過IL2CPP的編譯是翻譯成中間匯編后,直接進(jìn)一步的翻譯成C++代碼,然后再把C++代碼翻譯成匯編機(jī)器指令,這樣IL2CPP在運(yùn)行時(shí)就沒有動態(tài)編譯過程了
Unity對于不同系統(tǒng)平臺的腳本后臺支持

Unity對于不同的系統(tǒng)平臺的腳本后臺支持也是不一樣的,安卓能同時(shí)支持Mono,及時(shí)JIT和IL2CPP,而IOS平臺就只能使用IL2CPP方式,大部分主機(jī)平臺也是一樣只能采用IL2CPP方式
IOS平臺禁止JIT編譯

使用Mono腳本后臺編譯能夠支持AOT、FullAOT和JIT,而使用IL2CPP就只能支持提前編譯方式
IOS平臺的編譯選項(xiàng)只能支持FullAOT和IL2CPP方式,但因?yàn)镕ullAOT方式只能支持32位系統(tǒng),而蘋果要求從2016年以后都必須支持64位架構(gòu),所以只能采用IL2CPP方式
理想的熱更流程

最為理想的熱更流程就是把熱更功能寫在動態(tài)鏈接庫里,然后在程序啟動時(shí)用新的同名動態(tài)鏈接庫覆蓋舊的,并通過Assembly.Load動態(tài)加載這個(gè)動態(tài)連接庫,最后通過反射來獲取到熱更DLL里的類型,并創(chuàng)建這個(gè)類的實(shí)例
這是最理想的熱更流程,而且因?yàn)榘沧科脚_支持即時(shí)編譯方式,所以這樣的熱更流程方式在安卓平臺使用沒有任何問題,這也是為什么安卓平臺編譯簡單,安全性相對差一點(diǎn)的原因,但這樣的流程放在IOS平臺上面實(shí)施就會失敗
IOS禁止為動態(tài)分配內(nèi)存賦予執(zhí)行權(quán)限
上面是我準(zhǔn)備的一段實(shí)驗(yàn)代碼,它是在蘋果電腦上執(zhí)行的,這段代碼做了哪些事情呢?
這段代碼中create_space函數(shù)是一個(gè)內(nèi)存分配函數(shù),它會按照指定的字節(jié)數(shù)來創(chuàng)建一個(gè)內(nèi)存映射文件,創(chuàng)建內(nèi)存映射文件可以理解為創(chuàng)建一塊內(nèi)存,這塊內(nèi)存具有執(zhí)行權(quán)限,創(chuàng)建好這塊內(nèi)存后會返回這塊內(nèi)存區(qū)域
內(nèi)存分配函數(shù)下面的copy_code_2_space函數(shù)的功能是指定一個(gè)地址,然后往地址里寫一段程序代碼,這個(gè)代碼很簡單,可以把它理解成一個(gè)兩個(gè)數(shù)相加的代碼,或是簡單的賦值,函數(shù)中的memcpy就是把這段程序代碼的機(jī)器指令拷貝到m所代表的內(nèi)存里
最下方main函數(shù)指定一塊內(nèi)存,大小是1024,然后通過create_space按照內(nèi)存大小來創(chuàng)建一塊內(nèi)存,創(chuàng)建了這塊內(nèi)存地址以后會往這塊內(nèi)存地址里寫入一個(gè)函數(shù)
在Mac電腦中執(zhí)行這段代碼時(shí)會得到運(yùn)行報(bào)錯(cuò),為什么會報(bào)錯(cuò)?
因?yàn)榇a中的分配內(nèi)存函數(shù)為這塊內(nèi)存賦予了執(zhí)行權(quán)限,而IOS平臺是禁止為動態(tài)分配的內(nèi)存塊賦予執(zhí)行權(quán)限的,這就是為什么IOS平臺無法通過動態(tài)加載程序代碼進(jìn)行熱更新的原因

所以IOS只能采用靜態(tài)編譯方式,靜態(tài)編譯方式有兩種方案:Full-AOT和IL2CPP,但即使采用了這兩種方式的任意一種,也不能完全避免一些由于不合理使用代碼所帶來的問題
下面是一段IOS平臺上的程序

在這段程序中我創(chuàng)建了一個(gè)管理器接口和一個(gè)接收者接口,管理器接口可以發(fā)送消息,接收者接口可以響應(yīng)消息
通過管理器接口繼承實(shí)現(xiàn)管理器類,在這個(gè)管理器類里實(shí)現(xiàn)SendMessage方法,當(dāng)管理器類實(shí)現(xiàn)SendMessage方法時(shí),它會對IReceiver類型的target對象調(diào)用OnMessage方法,傳入的參數(shù)是泛型類型的value,這里會出現(xiàn)一些問題
調(diào)用SendMessage方法時(shí)會執(zhí)行到OnMessage方法,問題在于OnMessage的參數(shù)是泛型類型
由于采用了Full-AOT和IL2CPP來進(jìn)行編譯,而泛型代碼由于在運(yùn)行之前無法提前得知泛型的實(shí)際數(shù)據(jù)類型,所以當(dāng)Mono虛擬機(jī)以Full-AOT的方式執(zhí)行編譯代碼,或者是IL2CPP虛擬機(jī)執(zhí)行這樣代碼時(shí)就會直接跳過OnMessage的執(zhí)行
因?yàn)镕ull-AOT方式包括和IL2CPP方式是靜態(tài)編譯的,所以不能執(zhí)行這些在程序運(yùn)行當(dāng)中動態(tài)指定類型的代碼
所以實(shí)際上IL2CPP根本就沒有把泛型類型對應(yīng)的實(shí)際類型代碼編譯到最終程序當(dāng)中,當(dāng)你執(zhí)行OnMessage時(shí)就會看到下面的報(bào)錯(cuò),意思是你嘗試在AOT編譯的程序中執(zhí)行動態(tài)類型的代碼


要解決這個(gè)問題可以強(qiáng)制AOT編譯系統(tǒng)在程序運(yùn)行之前提前生成針對某種類型的代碼,比如你發(fā)送消息時(shí)要發(fā)送的類型是AnyEnum類型的消息,那么就可以提前寫一段OnMessage(AnyEnum.Zero)
這樣就會強(qiáng)制IL2CPP編譯這段代碼,從而生成針對AnyEnum類型的程序代碼,但這樣也就喪失了泛型的靈活性

腳本限制其實(shí)還是很多的,大家如果想要具體的了解IOS是如何禁止動態(tài)內(nèi)存執(zhí)行的,可以參考一下我們的《Unity小白的游戲夢》課程,如果想要了解如何通過反射像病毒一樣動態(tài)生成代碼也可以參考一下《Unity小白的游戲夢》課程
解決方案

既然IOS平臺有這么多的限制,那么應(yīng)該怎么去應(yīng)對IOS的平臺限制呢?
有一個(gè)簡單粗暴的方法,就是不為IOS平臺準(zhǔn)備熱更新功能,只為安卓平臺準(zhǔn)備熱更新功能,但這種方案可行嗎?
IOS的用戶按照傳統(tǒng)認(rèn)為都是一些高價(jià)值用戶,而且就算不考慮高價(jià)值,IOS的用戶至少也占到手機(jī)市場三分之一,任何一個(gè)開發(fā)商都不會放棄這部分用戶
所以不針對IOS做熱更是不行的,那么應(yīng)該怎樣針對IOS做熱更呢?
我們的解決方案是嵌入一種腳本語言,嵌入的腳本語言有兩種
Lua,這是比較傳統(tǒng)的熱更方案,很多PC端的游戲都是使用Lua方式進(jìn)行熱更的,Lua熱更方案有兩種,一種是ToLua,一種是XLua
C#熱更方案,C#熱更方案是比較有前景的一種方式,因?yàn)镮LRunTime熱更已經(jīng)被加入到Unity官方的PackageManager里面了
為什么腳本語言可以熱更?
腳本語言的工作原理就是每一次啟動游戲時(shí)都要在服務(wù)器上檢測一下有沒有程序腳本更新,有就從服務(wù)器上把腳本下載到本地客戶端,下載完以后客戶端啟動時(shí)就會把腳本加載到內(nèi)存里執(zhí)行
但剛才也說了IOS平臺禁止動態(tài)分配一塊內(nèi)存執(zhí)行其中的代碼,為什么兩種方案的代碼能加載到內(nèi)存里面并執(zhí)行呢?限于篇幅問題,我會在我們的《Unity小白的游戲夢》課程里花上一個(gè)專題來專門介紹這塊內(nèi)容
小結(jié)

1,Unity腳本后臺有哪幾種?
2,每種腳本后天分別支持哪幾種編譯方式?
3,安卓/蘋果分別支持哪幾種編譯方式?
4,C#腳本對反射的使用有限制,那么什么樣的反射方法可以使用呢?
5,Lua/ILRunTime熱更方案都會把腳本加載到內(nèi)存并執(zhí)行,但是為什么這兩種方式就能正常執(zhí)行動態(tài)加載的腳本呢?
進(jìn)一步學(xué)習(xí)的內(nèi)容

寫在最后
更多學(xué)習(xí)資源請加QQ:1517069595或WX:alice17173獲取(企業(yè)級性能優(yōu)化/熱更新/Shader特效/服務(wù)器/商業(yè)項(xiàng)目實(shí)戰(zhàn)/每周直播/一對一指導(dǎo))
點(diǎn)贊、關(guān)注、分享可免費(fèi)獲得配套學(xué)習(xí)資源
詳細(xì)內(nèi)容可觀看下方完整視頻
