这是一篇翻译文章,原文来自 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 属性,示例代码如下

    // 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 现在对于这一块的支持还不够优雅。

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

原代码: Github