2018/4/8 技术探讨

是的,没错,前两篇(《如何在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,然后程序恢复没更新前的样子了。


嗯,大概就是这样子了,已经深夜一点了,明天还得上班,我得赶紧睡觉了。

  [ Unity ]   [ Android ]   [ 热更新 ]
知识共享许可协议 本作品由小蝶惊鸿创作,采用知识共享署名 4.0 国际许可协议进行许可,转载时请保留本文署名及链接。