是的,没错,前两篇(《如何在Debian中编译Unity Mono生成Android版的libmono.so》、《如何在CentOS/RHEL中编译Unity Mono生成Android版的libmono.so》)中说的需求其实就是Android中Unity的DLL热更新。热更新可以说一直都是游戏(特别是手游)必不可少的功能之一。目前由于苹果对JIT的限制,我们无法在IOS上直接的热更新DLL(最多也只能采取一种曲线救国的方式使用Lua之流的脚本语言调用C# API的方式来实现“热更新”,但也只能某种程度上),但安卓却完全不受此限制。此外,不少的游戏商(特别是中小型寨厂)上线一款游戏都会先上安卓版,然后去各大平台、渠道买量,再根据当前的市场环境以及收到的反馈进行程序上与数值、玩法上的修改和调试,等改得差不多了再上Apple store,上线前期会出现比较多的更新需求。如何热更新Android的DLL?使用反射的方式从外部Load程序集?听起来可行,但这却有着一个致命性的缺点,那就是IOS用不了,这就意味着需要分别对Android和IOS分别进行处理,这只会徒然增加工作量。好在Unity是使用Mono作为运行时并且Mono是开源的,这提供了一种“非官方”的解决方案,那就是改掉Mono的源码,让它可以实现Android的DLL热更。
关于改Mono源码实现Android DLL热更新目前已有不少文章有说明,核心代码基本上都是同一份(包括本文),按照那些代码改基本没有啥太大的问题。本文就仅作一个“炒冷饭”以及额外补充一些我自己遇到的坑以及需要注意的事项。
先说说怎么修改,打开“(unity-mono源码根目录)/mono/metadata/image.c”文件,找到 “mono_image_open_from_data_with_name”函数,把这个函数改成:
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.dll")) { //强制要求程序包名都是:com.xxx.yyy const char *_pack = strstr(name, "com."); const char *_pfie = strstr(name, "-"); char _name[512]; memset(_name, 0, 512); int _len0 = (int)(_pfie - _pack); memcpy(_name, "/data/data/", 11); //_name = "/data/data/" memcpy(_name + 11, _pack, _len0); //_name = "/data/data/com.xxx.yyy" memcpy(_name + 11 + _len0, "/Assembly-CSharp.dll", 20); //_name = "/data/data/com.xxx.yyy/Assembly-CSharp.dll" char *bytes = ReadStringFromFile(_name, &datasize); if (datasize > 0) { data = bytes; data_len = datasize; } } /////修改结束////// MonoCLIImageInfo *iinfo; MonoImage *image; char *datac; if (!data || !data_len) { if (status) *status = MONO_IMAGE_IMAGE_INVALID; return NULL; } datac = data; if (need_copy) { datac = g_try_malloc(data_len); if (!datac) { if (status) *status = MONO_IMAGE_ERROR_ERRNO; return NULL; } memcpy(datac, data, data_len); } /////修改开始////// //释放读取到内存的dll临时内容 if (datasize > 0 && data != 0) { g_free(data); } /////修改结束////// image = g_new0(MonoImage, 1); image->raw_data = datac; image->raw_data_len = data_len; image->raw_data_allocated = need_copy; image->name = (name == NULL) ? g_strdup_printf("data-%p", datac) : g_strdup(name); iinfo = g_new0(MonoCLIImageInfo, 1); image->image_info = iinfo; image->ref_only = refonly; image->ref_count = 1; image = do_mono_image_load(image, status, TRUE, TRUE); if (image == NULL) return NULL; return register_image(image); }
并在这个函数上面添加这个函数:
static char *ReadStringFromFile(const char *pathName, int *size) { g_message("ReadStringFromFile PathName:%s\n", pathName); FILE *file = fopen(pathName, "rb"); if (file == NULL) { g_message("%s not exists\n", pathName); return 0; } fseek(file, 0, SEEK_END); int length = ftell(file); fseek(file, 0, SEEK_SET); if (length < 0) { fclose(file); return 0; } *size = length; char *outData = g_try_malloc(length); int readLength = fread(outData, 1, length, file); fclose(file); if (readLength != length) { g_free(outData); return 0; } g_message("%s exists\n", pathName); return outData; }
再然后的话就可以执行“build_runtime_android.sh”开始编译我们修改后的代码了。
编译好的so,你可以在apk出包之后替换apk中“lib/armeabi-v7a”里面的“libmono.so”然后重新封包+签名。
也可以直接替换Unity Editor中的libmono.so,具体地址为“(Unity安装目录)/Editor/Data/PlaybackEngines/AndroidPlayer/Variations/mono/Development 和 Release/Libs/armeabi-v7a”里面(两个都要替换)。
至此,Mono部分的修改也就完成了。这里有个点需要注意的:修改代码的时候小心不要带上了Windows的换行符(具体可以在Linux中用vi打开文件看看每行的结尾有没有多出个“^M”)。
接下来我们再开个新的Unity Demo简单的演示下如何编写热更新部分的代码。我们先做个很简单的界面,功能也如图所示,就三个不同功能的按钮以及一个用于输出内容的Text控件:
然后编写一些Java部分的代码:
package com.mytest; import com.unity3d.player.UnityPlayer; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; public class mytest1 { /** * 获得包名 * * @return */ public static String GetPackageName() { return UnityPlayer.currentActivity.getPackageName(); } /** * 重启app */ public static void Reboot() { ContextWrapper ctx = UnityPlayer.currentActivity; Intent intent = ctx.getBaseContext().getPackageManager().getLaunchIntentForPackage(ctx.getBaseContext().getPackageName()); PendingIntent restartIntent = PendingIntent.getActivity(ctx.getApplicationContext(), 0, intent,PendingIntent.FLAG_ONE_SHOT); AlarmManager mgr = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, restartIntent); // 1秒钟后重启应用 System.exit(0); } }
再然后编写C#部分的代码:
using System.Collections; using System.IO; using System.Text; using UnityEngine; using UnityEngine.UI; public class c003 : MonoBehaviour { private AndroidJavaClass _jc; private string _pakcageName; private string _dllPath; private void Start() { _jc = new AndroidJavaClass("com.mytest.mytest1"); _pakcageName = _jc.CallStatic<string>("GetPackageName", new object[0]); _dllPath = "/data/data/" + _pakcageName + "/Assembly-CSharp.dll"; var sb = new StringBuilder(); sb.AppendLine("初始化完成"); sb.AppendLine(string.Format("当前包名:{0}", _pakcageName)); sb.AppendLine(string.Format("DLL存储地址:{0}", _dllPath)); txt.text = sb.ToString(); } public Text txt; public void btnUpdateClick() { StartCoroutine(UpdateMethod()); } public IEnumerator UpdateMethod() { using (var www = new WWW("http://192.168.3.188/Assembly-CSharp.dll")) { yield return www; File.WriteAllBytes(_dllPath, www.bytes); txt.text = "下载DLL完成"; } } public void btnDelete() { File.Delete(_dllPath); txt.text = "删除DLL完成"; } public void btnReboot() { _jc.CallStatic("Reboot", new object[0]); } }
再然后就是用Unity出Android包,并且安装好并启动我们的程序。
我们提前连接好了Android Studio,首次启动的时候是并没有下载到DLL的,因此输出日志那里也显示“DLL not exists”,APP将加载默认的DLL。
此时我们尝试改改C#脚本,然后把新的DLL上传到Web服务器中(不需要重新出包,直接上传新dll就好),点击下载按钮让他下载新的DLL,然后再点击重启APP按钮让程序重启。
就可以看到它会读取下载到的DLL,同时Android Studio上日志也有记录了。 什么?点击中间的按钮会怎样?当然是删掉那个下载的DLL,然后程序恢复没更新前的样子了。
嗯,大概就是这样子了,已经深夜一点了,明天还得上班,我得赶紧睡觉了。