思考并回答以下问题:
- 通过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
10namespace ClassLibrary
{
public class Class1
{
public static string GetName()
{
return "大智";
}
}
}
上面就是一段最简单的代码,可以从代码中获取一段名称。
**调用Unity API**当然了,通常我们的代码不可能这么简单,通常需要调用Unity的API。
比如我们新建一个类,叫ConfigMgr.cs,添加如下代码:1
2
3
4
5
6namespace 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
12using 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
19using 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
17using 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
2NullReferenceException: 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
24using 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的交互。