这是一篇翻译文章,原文来自 how-to-reload-native-plugins-in-unity

在Unity编辑器使用原生插件,Dll之类的,经常会遇到一个问题,替换插件时,Unity会提示正在使用,无法替换,这是因为Unity一旦点了Play,加载了Dll,就不会去卸载。

p000501_Unity-Dll-Replace-Faild.png

要解决这个问题也很简单,那就是先关掉Unity,然后替换Dll,然后再打开Unity。对于插件的使用者,倒不是什么大问题,但是如果你是插件的开发者,需要频繁的修改和测试插件,那就有点悲惨了。

这篇博客将介绍一个我认为不错的解决方案,有很多开发者已经实现了这个或者类似的解决方案,但是在Google或者Github上很难找到。

TLDR

我写了一个200行的代码,在OnAwake时,会加载所有的Dll,在OnDestroy时会卸载所有的Dll,我们自己去管理Dll的加载和卸载,就可以做到停止Play时,卸载掉所有的Dll,这样就可以在不关闭Unity的情况下,替换Dll。

要做到这个,就不能用 PInvoke 去调用,而是用类似的方式,达到相同的目的。

完整的工程代码在 Github。但是我们只需要一个文件就可以 NativePluginLoader.cs

如何使用:

  1. 将 NativePluginLoader.cs 放到你的工程中

  2. 在场景中新建一个GameObject,然后挂载 NativePluginLoader.cs

  3. 定义一个类,用于声明所有的插件方法,例如命名为 FooPlugin,然后给这个类赋予 PluginAttr 属性

  4. 给 delegate 添加 PluginFunctionAttr 属性,示例代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // C# 代码
    [PluginAttr("my_cool_plugin")]
    public static class FooPlugin
    {
    [PluginFunctionAttr("sum")]
    public static Sum sum = null;
    public delegate float Sum(float a, float b); // 原生方法的 delegate
    }

    void CoolFunc() {
    float s = FooPlugin.sum(1.0, 2.0);
    }
    1
    2
    3
    4
    5
    // 这里是原生C代码中的接口, 最后打成Dll给Unity调用
    // my_cool_plugin.h
    extern "C" {
    __declspec(dllexport) float sum(float a, float b);
    }

关于 PInvoke

调用原生插件的常规方法是通过 PInvoke

1
2
3
4
public static class FooPlugin_PInvoke {
[DllImport("cpp_example_dll", EntryPoint = "sum")]
extern static public float sum(float a, float b);
}

但是使用 PInvoke 会有一个问题,就是Dll永远不会 unload。解决方案就是我们自己控制加载和卸载,卸载将使用下面的接口

1
2
3
4
5
6
7
8
9
10
11
12
static class SystemLibrary
{
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
static public extern IntPtr LoadLibrary(string lpFileName);

[DllImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static public extern bool FreeLibrary(IntPtr hModule);

[DllImport("kernel32")]
static public extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
}

自动完成加载卸载工作

NativePluginLoader.cs 是一个单例类,负责完成所有的加载和卸载工作。

主要的加载代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Loop over all assemblies
foreach (var assembly in assemblies) {

// Loop over all types
foreach (var type in assembly.GetTypes()) {

// Consider types with the attribute PluginAttr
var typeAttr = type.FindAttribute(typeof(PluginAttr));
if (!typeAttr)
continue;

// Load the Plugin (this is cached)
var plugin = LoadLibrary(typeAttr.pluginName);

// Loop over all fields for type
foreach (var field in type.GetFields()) {

// Find static, public fields with PluginFunctionAttr
var fieldAttr = field.FindAttribute(typeof(PluginFunctionAttr));
if (!fieldAttr)
continue;

// Get function pointer and store in static delegate field
var fnPtr = GetProcAddress(plugin, fieldAttr.functionName);
var fnDelegate = Marshal.GetDelegateForFunctionPointer(fnPtr, field.FieldType);
field.SetValue(null, fnDelegate);
}
}
}

这段代码在 OnAwake 时会加载所有的Dll,存到一个字典里,然后在 OnDestroy 时卸载所有的Dll。

其他注意的事情

从 Unity 2018.2 开始,设置里添加了新的特性。强烈推荐设置
Editor->Preferences->Script Changes While Playing = Recompile After Finished Playing
ScriptReload 会让所有的原生插件 unload 然后 reload。

当前的 NativePluginLoader.cs 只支持 Windows 平台

结论

Unity 支持原生插件是很不错的,因为有一些模块,使用C/C++之类的语言实现,然后提供API给C#调用是更好的选择。但是 Unity 现在对于这一块的支持还不够优雅。

我没有在网络上找到一个更好的方法去解决这个问题。所以我写了这个脚本,它帮我解决了一部分繁琐的事情,希望也能帮到其他人。

原代码: Github