通过dll实现代码热更新

思考并回答以下问题:

  • 通过dll实现代码热更新的优缺点是什么?
  • 将你已有工程中的一部分代码打包成dll使用。
  • 完成dll热更新的程序逻辑,然后试着修改外部dll中的代码逻辑。

代码逻辑热更新的方案,最简单的是通过dll进行热更新。虽然这种热更新方案不是主流方案,兼容性也无法覆盖所有平台,但是思路及过程还是值得学习的。

通过dll实现代码热更新

通过dll实现代码热更新,顾名思义就是将代码打包到dll中,通过动态加载dll,实现代码的热更新。

什么是dll?

dll全称是Dynamic Link Library,动态链接库文件。DLL文件中存放的是各类程序的函数(子过程)实现过程,当程序需要调用函数时需要先载入DLL,然后取得函数的地址,最后进行调用。dll是将一些代码进行编译并打包成了一个dll文件。

先说这种方式的局限性:主要就是不支持iOS平台

但是这种方式的好处是:简单,无需太多额外的学习成本。

这种热更方式的思路其实和其他一些方案也是类似的,而且这过程中可以学习到打包dll、C#的反射的相关知识,所以我们从这个比较简单的热更技术开始学习。

这种热更新的流程一般分为以下几步:

  • 1、编写逻辑dll
  • 2、动态加载逻辑dll
  • 3、读取逻辑dll,执行外部dll中的代码
  • 4、替换dll,实现逻辑的热更新

1、编写逻辑dll

先来编写一个外部的dll。

创建一个类库工程

打开Visual Studio,新建一个项目,选择:

创建新项目

如果没有这个类库的选项,说明你的Visual Studio没有安装相应的组件,可以看下面这一小节进行安装。

**安装Visual Studio相应组件**

1.打开Visual Studio Installer

2.选择修改

3.选中.Net桌面开发,右边可选的所有选项可以取消勾选

编写代码

工程创建完成后,你就可以编写C#代码了,这些代码在编译的时候会生成一个dll文件,可以供我们程序进行调用。

1
2
3
4
5
6
7
8
9
10
namespace ClassLibrary
{
public class Class1
{
public static string GetName()
{
return "大智";
}
}
}

上面就是一段最简单的代码,可以从代码中获取一段名称。

**调用Unity API**

当然了,通常我们的代码不可能这么简单,通常需要调用Unity的API。

比如我们新建一个类,叫ConfigMgr.cs,添加如下代码:

1
2
3
4
5
6
namespace ClassLibrary
{
class ConfigMgr:MonoBehaviour
{
}
}

上面代码里使用到了Unity的API,你会发现有报错:

报错

这是因为平时我们通过Unity打开的VS工程,Unity自动帮我们处理好了相关dll的引用工作,但是如果是我们自己创建的类库需要调用Unity的API,那需要我们自己添加引用Unity的dll。

在引用上右键,添加引用

点击下方的浏览,到对应的路径获取Unity的dll

比如我的路径是:C:\ProgramFiles\Unity\Hub\Editor\2018.3.6f1\Editor\Data\Managed

添加引用dll

最后一步,记得勾选后确定

这时候这样的代码就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;

namespace ClassLibrary
{
class ConfigMgr:MonoBehaviour
{
void Start()
{
Debug.Log("我是外部的dll");
}
}
}

编译dll

代码写好以后,我们需要将这些代码编译为dll,这个过程也很简单。

点击菜单栏 生成 > 生成解决方案 即可(快捷键F6)。

编译生成

在左下角看到生成成功,那就是dll生成完毕。可以在这个类库工程目录的bin/Debug中找到生成的dll。

生成好的dll

为什么目录中会有一个Debug目录呢?

这是因为一般都存在调试模式和发布模式,可以在VS中切换,生产环境中建议使用Release。

调试模式和发布模式

在工程内使用dll

添加到Unity工程

添加到Unity工程后如上图所示,可以看到Dll文件和一个ConfigMgr类。

继承了MonoBehaviour的类会单独显示出来,这样可以将这个类拖到场景中的GameObject上。

将ConfigMgr类拖到场景物体上,运行后在Console的显示

可以显示出Log说明我们已经成功调用了这个dll。

2、动态加载逻辑dll

打包dll只能叫做封装代码,要想做到动态加载就必须了解C#中的反射的技术。

既然是动态加载dll,那就意味着这个dll是在运行时才加载进来。我们平时使用dll时都是直接放到工程中,那么如何动态加载进来呢?

动态加载dll

我们先将ClassLibrary.dll放到StreamingAssets目录中准备在运行时加载它。

放到StreamingAssets目录中的资产会保持原样,比如代码或dll文件都不会被引擎编译加载。

动态加载Dll的基础代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.IO;
using System.Reflection;
using UnityEngine;

public class HotDllManager : MonoBehaviour
{
void Start()
{
var path = Path.Combine(Application.streamingAssetsPath, "ClassLibrary.dll");
var dll = Assembly.LoadFile(path);

foreach (var i in dll.GetTypes())
{
//调试代码
Debug.Log(i.Name);
gameObject.AddComponent(i);
}
}
}

通过log可以看到执行的流程

由于我们的dll中有两个类,一个继承了MonoBehaviour,一个没有继承,所以Class1不能被当作组件添加到GameObject上。

那如何获取dll中特定的类来使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.IO;
using System.Reflection;
using UnityEngine;

public class SpecDllManager : MonoBehaviour
{
void Start()
{
var path = Path.Combine(Application.streamingAssetsPath, "ClassLibrary.dll");
var dll = Assembly.LoadFile(path);

// 注意要写类型的全路径
var type = dll.GetType("ClassLibrary.ConfigMgr");
Debug.Log(type.Name);
gameObject.AddComponent(type);
}
}

注意:由于ConfigMgr上面还有命名空间,所以需要写全路径才能去获取这个类型。

不过如果执行这段代码,你会发现有一个报错!

1
2
NullReferenceException: Object reference not set to an instance of an object
SpecDllManager.Start () (at Assets/Scripts/SpecDllManager.cs:14)

这是怎么回事呢?明明之前我们已经加载成功了啊!

如果回到昨天,你会看到我们的ConfigMgr类并没有加public修饰符。
GetTypes方法可以获取到所有的类,但是GetType只能获取到公共的类,通过下面的代码可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.IO;
using System.Reflection;
using UnityEngine;

public class HotDllManager : MonoBehaviour
{
void Start()
{
var path = Path.Combine(Application.streamingAssetsPath, "ClassLibrary.dll");
var dll = Assembly.LoadFile(path);

foreach (var i in dll.GetTypes())
{
//GetTypes
Debug.Log("GetTypes:" + i.Name);
}

foreach (var i in dll.GetExportedTypes())
{
//GetExportedTypes
Debug.Log("Exported:" + i.Name);
}
}
}

上面你就能看到导出的公共类只有Class1,如果想要加载ConfigMgr,需要将ConfigMgr的类修饰符改成public。

反射Reflection

反射指程序可以访问、检测和修改它本身状态或行为的一种能力。

程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。

你可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。

优缺点

优点:

  • 1、反射提高了程序的灵活性和扩展性。
  • 2、降低耦合性,提高自适应能力。
  • 3、它允许程序创建和控制任何类的对象,无需提前硬编码目标类。

缺点:

  • 1、性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
  • 2、使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。

反射(Reflection)的用途

反射(Reflection)有下列用途:

  • 它允许在运行时查看特性(attribute)信息。
  • 它允许审查集合中的各种类型,以及实例化这些类型。
  • 它允许延迟绑定的方法和属性(property)。
  • 它允许在运行时创建新类型,然后使用这些类型执行一些任务。

C#中反射的常用方法

首先是加载外部dll的方法,也就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 从文件加载
var dll = Assembly.LoadFile(path);
// 从字节流加载
var dll2 = Assembly.Load(bytes);
从dll中加载类型:

// 注意要写全路径
var type = dll.GetType("Namespace.ClassName");
调用类型中的方法:

var method = type.GetMethod("MethodName");
// 如果为静态方法,第一个可传null,如果为实例方法,应该传实例对象
method.Invoke(null, null);

常用的一些反射的方法就是这些,遇到具体的问题可以具体分析。

3.替换dll,实现热更新

上面我们发现了一个bug,如果是往常的开发模式,我们需要修改源代码,重新编译发布。

但是!现在不用了,即使是在发布后发现了这个问题,我们只需要修改这个dll就可以实现bug的修复。

按照下图修改我们类库中的代码并重新编译发布。

将生成的dll覆盖原来的dll。

虽然我们在这为了简便,是在编辑器中进行操作,但即使发布出来以后也只需要替换dll就可以实现热更新。

这时候再运行SpecDllMgr,你就会发现:

大功告成了!

总结

  • 将一些经常复用的类打包成dll,方便在不同项目间共享。
  • 打包成dll后,无法直接看到源码,可以一定程度地保护代码(反编译还是可以看到大部分代码)。
    ** 如果需要更好地保护代码,可以学习一下代码混淆的知识(这样虽然还是可以被反编译,但是反编译后的各种类名、变量名都是无意义的符号,拿到的代码几乎没有价值)

如果想在商业项目中使用这种热更新的方案,一定要记住以下两点:

  • 使用这种方案就和iOS系统无缘了
  • 尽量减少工程和动态dll的耦合性,只通过一个或几个类完成工程中的代码和热更新dll的交互。
0%