这是一篇翻译文章,原文来自 how-to-reload-native-plugins-in-unity
在Unity编辑器使用原生插件,Dll之类的,经常会遇到一个问题,替换插件时,Unity会提示正在使用,无法替换,这是因为Unity一旦点了Play,加载了Dll,就不会去卸载。
要解决这个问题也很简单,那就是先关掉Unity,然后替换Dll,然后再打开Unity。对于插件的使用者,倒不是什么大问题,但是如果你是插件的开发者,需要频繁的修改和测试插件,那就有点悲惨了。
这篇博客将介绍一个我认为不错的解决方案,有很多开发者已经实现了这个或者类似的解决方案,但是在Google或者Github上很难找到。
TLDR
我写了一个200行的代码,在OnAwake时,会加载所有的Dll,在OnDestroy时会卸载所有的Dll,我们自己去管理Dll的加载和卸载,就可以做到停止Play时,卸载掉所有的Dll,这样就可以在不关闭Unity的情况下,替换Dll。
要做到这个,就不能用 PInvoke 去调用,而是用类似的方式,达到相同的目的。
完整的工程代码在 Github。但是我们只需要一个文件就可以 NativePluginLoader.cs
如何使用:
将 NativePluginLoader.cs 放到你的工程中
在场景中新建一个GameObject,然后挂载 NativePluginLoader.cs
定义一个类,用于声明所有的插件方法,例如命名为 FooPlugin,然后给这个类赋予
PluginAttr
属性给 delegate 添加
PluginFunctionAttr
属性,示例代码如下// 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); }
// 这里是原生C代码中的接口, 最后打成Dll给Unity调用 // my_cool_plugin.h extern "C" { __declspec(dllexport) float sum(float a, float b); }
关于 PInvoke
调用原生插件的常规方法是通过 PInvoke
public static class FooPlugin_PInvoke {
[DllImport("cpp_example_dll", EntryPoint = "sum")]
extern static public float sum(float a, float b);
}
但是使用 PInvoke
会有一个问题,就是Dll永远不会 unload。解决方案就是我们自己控制加载和卸载,卸载将使用下面的接口
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 是一个单例类,负责完成所有的加载和卸载工作。
主要的加载代码如下
// 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 现在对于这一块的支持还不够优雅。
我没有在网络上找到一个更好的方法去解决这个问题。所以我写了这个脚本,它帮我解决了一部分繁琐的事情,希望也能帮到其他人。