前項(xiàng)目的C#熱更方案
小甜甜的C#熱更方案
前段時(shí)間 noodle 說(shuō)他把 小甜甜 項(xiàng)目中他做的 C#熱更方案 開(kāi)源了。
這個(gè)方案是一個(gè) 騷操作,不過(guò)是針對(duì) il2cpp 的,核心思想是更新 libil2cpp.so,具體細(xì)節(jié)可以參考 github主頁(yè)。
暗黑血統(tǒng)的C#熱更方案
再早一點(diǎn)的項(xiàng)目 暗黑血統(tǒng),那還是 Unity4 的時(shí)代,我們的C#熱更方案也是一個(gè) 騷操作,這里列一下要點(diǎn):
Assembly-CSharp-firstpass.dll 和 Assembly-CSharp.dll 是Unity預(yù)定的2個(gè)程序集。
Assembly-CSharp-firstpass.dll 包括了加載熱更代碼的代碼,不可被熱更新,Assembly-CSharp.dll 包括主要的游戲邏輯代碼,期望可以被熱更新。
打包的時(shí)候,把 Assembly-CSharp.dll 中的代碼移動(dòng)到我們自定的 GameLogic.dll 中,并把 Assembly-CSharp.dll 清空(namespace顛倒)。
運(yùn)行的時(shí)候,Assembly-CSharp-firstpass.dll 中的代碼通過(guò) Assembly.Load 的方式去加載 GameLogic.dll,GameLogic.dll 可以從服務(wù)器下載獲取,以此達(dá)到熱更新的目的。
這樣做看起來(lái)OK,但是有一個(gè)很大的限制: 預(yù)設(shè)不能掛載非firstpass目錄的腳本,原因可以參考這篇帖子。 當(dāng)然,我們可以在運(yùn)行時(shí)通過(guò) AddComponent 的方式去掛載腳本,但是這樣做限制較大。
騷操作 之所以被稱為 騷操作,就是我們可以打破這個(gè)限制:即把 Assembly-CSharp.dll 換成了 GameLogic.dll 后,也要保證預(yù)設(shè)能夠找得到原先引用的腳本。
暗黑血統(tǒng) 的做法是:在生成GameLogic.dll后,改cs文件對(duì)應(yīng)的meta文件,把dll重新定向到GameLogic.dll,重啟編輯器再打包。

在打包的時(shí)刻,預(yù)設(shè)已經(jīng)認(rèn)定了 GameLogic.dll,所以加載時(shí)就不會(huì)丟失腳本了。
當(dāng)然,這個(gè)方案依然也有局限:
必須嚴(yán)格保證 Assembly-CSharp-firstpass.dll 的穩(wěn)定,一旦出現(xiàn)了問(wèn)題,只能換包。
熱更后,如果 GameLogic.dll 新增了一個(gè)上架包中并不存在的腳本,那么掛載這個(gè)腳本的預(yù)設(shè)在加載時(shí)依然還是會(huì)出現(xiàn)腳本丟失。
總體來(lái)說(shuō),這套方案沒(méi)什么大問(wèn)題,在線上運(yùn)行良好。出現(xiàn)上面的問(wèn)題2時(shí),我們就 AddComponent 繞一下。
隨著Unity的升級(jí)換代,metadata的格式也在變化,這套依賴 改meta文件 的方案在版本兼容性上出現(xiàn)了很大問(wèn)題,最終因?yàn)殡y以維護(hù)被拋棄了。
目前的C#熱更新方案
時(shí)至今日,如果再做方案選擇,我傾向于集成 xLua。
對(duì)于Android平臺(tái)的C#熱更,我傾向于目前公司所采用的方案:自己編譯libmono.so。
我們可以在github上找到各個(gè)Unity版本對(duì)應(yīng)的 mono源碼,做如下操作:
- 打開(kāi) image.c 文件。
- 找到 mono_image_open_from_data_with_name 函數(shù)。
- 截住加載 Assembly-CSharp-firstpass.dll 的邏輯,做我們自己的操作。
代碼流程如下:
MonoImage *
mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name)
{
int datasize = 0;
if(name != NULL && strstr(name,"Assembly-CSharp-firstpass.dll"))
{
// 從我們的Patch目錄讀取我們自己的dll文件,覆蓋傳入的data
}
// 解密data
// 走原先的do_mono_image_load流程
}
這里有幾個(gè)細(xì)節(jié)要注意:
Assembly-CSharp-firstpass.dll 和 Assembly-CSharp.dll 我們都做了加密,所以這里有一個(gè)步驟是解密。
mono_image_open_from_data_with_name 函數(shù)只攔截了加載 Assembly-CSharp-firstpass.dll 的操作,并未攔截 Assembly-CSharp.dll。
至于 Assembly-CSharp.dll,我們打包的時(shí)候直接把他刪了,所以這里不會(huì)加載它。和 暗黑血統(tǒng) 的做法類似,我們還是通過(guò) Assembly-CSharp-firstpass.dll 中的代碼來(lái)加載它。
改完源碼,重新編譯生成新的 libmono.so,就大功告成了。
這個(gè)方案比較成熟,網(wǎng)上的文章一搜一大把。我之所以傾向于這個(gè)方案,主要有以下幾點(diǎn)考慮:
這個(gè)方案對(duì)整個(gè)項(xiàng)目的 侵入性很小,只需要替換掉 libmono.so 即可。
加載熱更代碼的代碼從 Assembly-CSharp-firstpass.dll 轉(zhuǎn)移到了 libmono.so,因此 Assembly-CSharp-firstpass.dll 也可以被熱更新了。
這個(gè)方案對(duì)團(tuán)隊(duì)人員的要求沒(méi)那么高,維護(hù)成本相對(duì)較低。
當(dāng)然,這個(gè)方案也有以下一些限制:
如果更新 Assembly-CSharp-firstpass.dll,需要重啟一次進(jìn)程。
Standard Assets 或者 Plugins 目錄下的代碼可以被掛載,但是 非firstpass目錄 下的代碼不行,因?yàn)檫@里并沒(méi)有 暗黑血統(tǒng)改meta 的那一步騷操作。
重啟進(jìn)程對(duì)用戶體驗(yàn)有一點(diǎn)傷害,特別是 進(jìn)程不能被快速拉起 時(shí),可能會(huì)影響留存。不過(guò)后來(lái)我們?cè)趕dk里加了一個(gè) 秒啟 的函數(shù),現(xiàn)在重啟的代價(jià)可以忽略不計(jì)了,代碼如下:
public void doRestartApp()
{
new Thread()
{
public void run()
{
Intent localIntent = mContext.getPackageManager().getLaunchIntentForPackage(mContext.getPackageName());
localIntent.addFlags(67108864);
mContext.startActivity(localIntent);
android.os.Process.killProcess(android.os.Process.myPid());
}
}.start();
finish();
}
至于 非firstpass目錄 下代碼無(wú)法掛載的問(wèn)題,我們會(huì)把需要掛載的代碼統(tǒng)一移動(dòng)到 Plugins 目錄下,因?yàn)?Assembly-CSharp-firstpass.dll 已經(jīng)可以被熱更新了。
個(gè)人主頁(yè)
本文的個(gè)人主頁(yè)鏈接:https://baddogzz.github.io/2019/12/26/CSharp-Patch/
好了,拜拜。