Unity C#热更新方案 ILRuntime学习笔记(一) Hello World

转载请标明原文地址:https://segmentfault.com/a/11...

一、ILRuntime介绍

问:什么是热更新?
答:软件在使用时就能实现更新的方式就叫做热更新。热更新无需用户重新下载安装或重启,在使用时即可更新,方便快捷体验良好。

问:什么是ILRuntime?
答:ILRuntime是一个C#热更新方案。ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新

问:lua 和 ILRuntime哪个热更新方案更好?
答:如果你的团队更熟悉lua,就用lua。如果你的团队更熟悉C#就用ILRuntime。如果你是主程,你可以选择自己喜欢的方案,但是要肩负起填坑的责任。

我个人的感觉是:lua在Unity中用起来很难受,不是lua这门语言不好,而是因为在Unity中官方的开发语言是C#。用lua就意味着开发者要会两种语言,学习和开发成本都高,而且因为C#是强类型、面向对象的语言。lua是弱类型,非面向对象的语言。
lua从编程思想和代码写法都和C#有较大差距,这一点在面对越大的项目时感受越明显,项目小的时候觉得lua还好,项目做大了以后会发现lua带给你的麻烦会大于便利。
而ILRuntime方案是基于C#的,开发语言统一,编码更容易。不过他的缺点是实际经过验证的项目还是太少了,不太成熟,可能有很多坑需要填,不像是经过很多项目验证的lua,有比较成熟的方案。

二、下载ILRuntime

GitHub:https://github.com/Ourpalm/IL...
Unity Demo:https://github.com/Ourpalm/IL...
国内码云:https://gitee.com/zhangyu800/...
中文手册:http://ourpalm.github.io/ILRu...

先去GitHub上点个赞,支持一下该项目,再去Unity Demo上把项目下载下来,顺便也点个赞。
如果国外地址下载慢,可以在国内码云上下载Demo。

ILRuntimeDemo.png

三、导入ILRuntime

1.解压缩Unity Demo
打开Unity工程目录下的ProjectSettings/ProjectVersion.txt 查看工程版本。

为了避免因为不同版本导致的兼容问题,工程版本和Unity版本尽量保持一致,我下载的Demo工程版本是2019.3.6f1, 我尽量用2019.3.6 或稍微高一点儿的版本导入。

ProjectVersion.png

2.目录结构

工程导入完毕,看下目录结构。

Demo目录:
在Samples/ILRuntime/1.6.2/Demo/_Scenes/Examples文件夹下

目录结构.png

热更新加载的代码目录:
热更新代码会从StreamingAssets目录下加载编译后的dll
其中mdb和pdb文件都是调试时用的,发布时只需要dll。

StreamingAssets文件夹2.png

运行Hello World Demo:

打开并运行 01_Hello World 场景。可以看到控制台输出了如下结果。
01_Hello World.png

ILR是如何工作的呢?看下HelloWorld脚本。

using UnityEngine;
using System.Collections;
using System.IO;
using ILRuntime.Runtime.Enviorment;

public class HelloWorld : MonoBehaviour
{
    //AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
    //大家在正式项目中请全局只创建一个AppDomain
    AppDomain appdomain;

    System.IO.MemoryStream fs;
    System.IO.MemoryStream p;
    void Start()
    {
        StartCoroutine(LoadHotFixAssembly());
    }

    IEnumerator LoadHotFixAssembly()
    {
        //首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
        //正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
        //正式发布的时候需要大家自行从其他地方读取dll

        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
        //工程目录在Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~
        //以下加载写法只为演示,并没有处理在编辑器切换到Android平台的读取,需要自行修改
#if UNITY_ANDROID
        WWW www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.dll");
#else
        WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.dll");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] dll = www.bytes;
        www.Dispose();

        //PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
        www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
        www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] pdb = www.bytes;
        fs = new MemoryStream(dll);
        p = new MemoryStream(pdb);
        try
        {
            appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
        }

        InitializeILRuntime();
        OnHotFixLoaded();
    }

    void InitializeILRuntime()
    {
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
        //这里做一些ILRuntime的注册,HelloWorld示例暂时没有需要注册的
    }

    void OnHotFixLoaded()
    {
        //HelloWorld,第一次方法调用
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);

    }

    private void OnDestroy()
    {
        if (fs != null)
            fs.Close();
        if (p != null)
            p.Close();
        fs = null;
        p = null;
    }

    void Update()
    {

    }
}

惊喜!注释是纯中文的,作者一定是中国人!注释很详细,看注释就行了。

其中比较重要的代码是:

//这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
//工程目录在Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~
//以下加载写法只为演示,并没有处理在编辑器切换到Android平台的读取,需要自行修改
WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.dll");

//HelloWorld,第一次方法调用
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);

打开HotFix_Project:
HotFix_Project 是Demo自带的C#热更新代码工程。

工程目录在
Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~文件夹下,用Visual Studio打开该工程。

注意:HotFix_Project~ 后面带了个波浪号,Unity会自动忽略该目录,资源不会被导入,代码不会被编译,文件不会带进发布包。
具体规则可以看官方文档:
https://docs.unity3d.com/Manu...

修改热更新代码:
打开HotFix项目下的InstanceClass类 修改StaticFunTest方法中输出的内容:

public static void StaticFunTest()
{
    UnityEngine.Debug.Log("调用了热更新类的静态方法!");
}

生成热更新代码dll:
右键项目 > 生成
右键项目 生成.png

重新运行Hello World Demo:
查看输出结果,可以看到 我们修改的内容被输出了。

Log.png

通过以上步骤,了解了IRuntime的基本运行流程。
1.热更代码生成dll
2.Unity加载dll
3.Unity调用dll里的方法

三、创建自己的Hotfix工程

从零开始创建自己的热更工程,体验完整配置流程。
1.打开Visual Studio,新建项目。
新建 项目.png

2.修改项目配置
选择类库(.NET Framework)。
修改名称为 Hotfix
Hotfix 项目设置.png

修改路径为 Unity项目根目录
Hotfix 文件夹.png

3.添加UnityEngine的引用
在Unity中用Visual Studio打开Unity Demo的任意一个脚本,VS会自动关联需要的Unity类库引用,在项目的引用中可以看到引用的dll路径。
Hotfix 引用路径.png

在我们自己的Hotfix工程引用中添加该文件。

有两个主要的路径

(1)在Unity安装目录下的Editor\Data
我的电脑路径是:D:\SDK\Unity\2019.4.1f1\Editor\Data\Managed\UnityEngine\
把该文件夹内所有dll文件都引入到Hotfix工程里。
拷贝dll.png

(2)在Unity工程根目录下的Library\ScriptAssemblies\
我的电脑路径是:
D:\Projects\Unity3D\Unity 2019.3 Projects\ILRuntimeU3D\ILRuntimeDemo\Library\ScriptAssemblies
添加自己需要的dll,我引用了以下两个dll。
Assembly-CSharp.dll
UnityEngine.UI.dll

注:Unity 2019中已经把类库拆的很零散了,应该是为了解耦,为了以后的规划。他把类库拆成两个部分,内置的类库放在Unity安装目录下了,PackageManager包管理器下载的插件都放在Unity工程目录下了,UGUI的库从内置位置移动到PackageManager里了,可能以后要淘汰掉。

4.编写 Hello World
添加完引用,可以开始写代码了。
在我们自己的Hotfix工程中添加一个类 HelloWorld.cs 并输入以下代码:

namespace Hotfix {

    using UnityEngine;

    // 冰封百度的Blog:https://segmentfault.com/a/1190000023183723
    public class HelloWorld {

        public void Test() {
            Debug.Log("Hello World");
        }

    }

}

5.配置Hotfix.dll输出路径
右键Hotfix工程, 选择生成选项卡,在下方的输出路径里浏览到StreamingAssets文件夹或填写相对路径:..\..\Assets\StreamingAssets\
Hotfix 输出路径.png

5.生成Hotfix.dll
右键Hotfix工程,选择生成。
生成成功后可以看到如下提示,如果失败则根据错误提示进行处理。
Hotfix 生成成功.png

6.修改Unity Demo中的HelloWorld脚本
主要修改脚本中加载dll的名称,HotFix_Project改为Hotfix。
并且添加调用热更方法的代码。

修改后如下:

using UnityEngine;
using System.Collections;
using System.IO;
using ILRuntime.Runtime.Enviorment;

public class HelloWorld : MonoBehaviour
{
    //AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
    //大家在正式项目中请全局只创建一个AppDomain
    AppDomain appdomain;

    System.IO.MemoryStream fs;
    System.IO.MemoryStream p;
    void Start()
    {
        StartCoroutine(LoadHotFixAssembly());
    }

    IEnumerator LoadHotFixAssembly()
    {
        //首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
        //正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
        //正式发布的时候需要大家自行从其他地方读取dll

        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
        //工程目录在Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~
        //以下加载写法只为演示,并没有处理在编辑器切换到Android平台的读取,需要自行修改
#if UNITY_ANDROID
        WWW www = new WWW(Application.streamingAssetsPath + "/Hotfix.dll");
#else
        WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/Hotfix.dll");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] dll = www.bytes;
        www.Dispose();

        //PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
        www = new WWW(Application.streamingAssetsPath + "/Hotfix.pdb");
#else
        www = new WWW("file:///" + Application.streamingAssetsPath + "/Hotfix.pdb");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] pdb = www.bytes;
        fs = new MemoryStream(dll);
        p = new MemoryStream(pdb);
        try
        {
            appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
        }

        InitializeILRuntime();
        OnHotFixLoaded();
    }

    void InitializeILRuntime()
    {
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
        //这里做一些ILRuntime的注册,HelloWorld示例暂时没有需要注册的
    }

    void OnHotFixLoaded()
    {
        //HelloWorld,第一次方法调用
        //appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);

        // 实例化Hotfix中的类
        object obj = appdomain.Instantiate("Hotfix.HelloWorld");
        appdomain.Invoke("Hotfix.HelloWorld", "Test", obj);
    }

    private void OnDestroy()
    {
        if (fs != null)
            fs.Close();
        if (p != null)
            p.Close();
        fs = null;
        p = null;
    }

}

运行Unity,可以看到输出了Hello World
Hotfix Hello World 输出.png

至此,ILRuntime Hello World部分已经全部完成了。

总结:
ILRuntime使用时,除了Hotfix工程的配置稍微麻烦一些外,其他部分都不难,Demo做的很完善,基本是开箱即用。具体使用细节建议仔细阅读官方文档:
http://ourpalm.github.io/ILRu...

阅读 1.5k

推荐阅读

程序生涯中的技术整理

10 人关注
41 篇文章
专栏主页