前項(xiàng)目的C#熱更方案
小甜甜的C#熱更方案
前段時(shí)間 noodle 說他把 小甜甜 項(xiàng)目中他做的 C#熱更方案 開源了眨猎。
這個(gè)方案是一個(gè) 騷操作,不過是針對(duì) il2cpp 的织狐,核心思想是更新 libil2cpp.so轻黑,具體細(xì)節(jié)可以參考 github主頁维苔。
暗黑血統(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 中的代碼通過 Assembly.Load 的方式去加載 GameLogic.dll犯建,GameLogic.dll 可以從服務(wù)器下載獲取讲冠,以此達(dá)到熱更新的目的。
這樣做看起來OK适瓦,但是有一個(gè)很大的限制: 預(yù)設(shè)不能掛載非firstpass目錄的腳本竿开,原因可以參考這篇帖子。 當(dāng)然玻熙,我們可以在運(yùn)行時(shí)通過 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)了問題唤殴,只能換包般婆。
熱更后,如果 GameLogic.dll 新增了一個(gè)上架包中并不存在的腳本朵逝,那么掛載這個(gè)腳本的預(yù)設(shè)在加載時(shí)依然還是會(huì)出現(xiàn)腳本丟失蔚袍。
總體來說,這套方案沒什么大問題配名,在線上運(yùn)行良好啤咽。出現(xiàn)上面的問題2時(shí),我們就 AddComponent 繞一下渠脉。
隨著Unity的升級(jí)換代宇整,metadata的格式也在變化,這套依賴 改meta文件 的方案在版本兼容性上出現(xiàn)了很大問題芋膘,最終因?yàn)殡y以維護(hù)被拋棄了鳞青。
目前的C#熱更新方案
時(shí)至今日霸饲,如果再做方案選擇,我傾向于集成 xLua臂拓。
對(duì)于Android平臺(tái)的C#熱更厚脉,我傾向于目前公司所采用的方案:自己編譯libmono.so。
我們可以在github上找到各個(gè)Unity版本對(duì)應(yīng)的 mono源碼胶惰,做如下操作:
- 打開 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) 的做法類似脓杉,我們還是通過 Assembly-CSharp-firstpass.dll 中的代碼來加載它。
改完源碼简逮,重新編譯生成新的 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ì)人員的要求沒那么高皿渗,維護(hù)成本相對(duì)較低。
當(dāng)然轻腺,這個(gè)方案也有以下一些限制:
如果更新 Assembly-CSharp-firstpass.dll乐疆,需要重啟一次進(jìn)程。
Standard Assets 或者 Plugins 目錄下的代碼可以被掛載贬养,但是 非firstpass目錄 下的代碼不行挤土,因?yàn)檫@里并沒有 暗黑血統(tǒng)改meta 的那一步騷操作。
重啟進(jìn)程對(duì)用戶體驗(yàn)有一點(diǎn)傷害煤蚌,特別是 進(jìn)程不能被快速拉起 時(shí)耕挨,可能會(huì)影響留存细卧。不過后來我們?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目錄 下代碼無法掛載的問題,我們會(huì)把需要掛載的代碼統(tǒng)一移動(dòng)到 Plugins 目錄下蜘犁,因?yàn)?Assembly-CSharp-firstpass.dll 已經(jīng)可以被熱更新了翰苫。
個(gè)人主頁
本文的個(gè)人主頁鏈接:https://baddogzz.github.io/2019/12/26/CSharp-Patch/
好了,拜拜这橙。