在CommandBuffer中调用托管函数引发域重载卡顿问题
问题描述
通过CommandBuffer.IssuePluginEventAndData调用托管函数,或从C++原生插件线程调用托管函数指针时,运行阶段一切正常,但停止游戏触发域重载时,编辑器卡在以下日志阶段,只能重启恢复:
Reloading assemblies for play mode. Begin MonoManager ReloadAssembly
相关代码如下:
C# 委托与回调定义
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public unsafe delegate void PluginDelegate(int eventId, void* data); [MonoPInvokeCallback(typeof(PluginDelegate))] private static unsafe void MakeGraphicsCallback(int eventId, void* data) { // 空实现或业务逻辑 }
委托存储与CommandBuffer调用
// 尝试通过静态字段防止委托被GC回收 private static PluginDelegate makeCallbackDelegate; public void Start() { makeCallbackDelegate = MakeGraphicsCallback; cmdBuffer.IssuePluginEventAndData( Marshal.GetFunctionPointerForDelegate(makeCallbackDelegate), 0, IntPtr.Zero); }
C++插件调用代码
typedef void (__stdcall *PluginCallback)(int32_t eventId, void *data); auto func = static_cast<PluginCallback>((void*)funcPtr); func((int32_t)eventIdValue, (void*)dataValue);
注:切换cdecl/stdcall调用约定并同步修改UnmanagedFunctionPointerAttribute后问题依旧;主线程直接调用无异常,仅非托管线程调用后触发域重载卡顿。
解决方案
1. 域重载前清理非托管侧的函数指针引用
Unity域重载时,原托管程序集被卸载,旧函数指针失效。需在域重载前通知C++插件清除所有托管函数指针引用,停止相关线程调用:
- C#侧订阅PlayMode状态变更事件,触发清理逻辑:
private void OnEnable() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } private void OnDisable() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } private void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingPlayMode) { // 调用C++插件清理函数,停止持有托管指针的线程/任务 NativePlugin.ClearAllManagedCallbacks(); // 等待所有非托管线程完成当前调用 NativePlugin.WaitForCallbacksComplete(); } }
- C++侧实现清理逻辑:
#include <atomic> #include <vector> #include <thread> #include <condition_variable> std::atomic<bool> s_ShouldStopCallbacks = false; std::vector<PluginCallback> s_ActiveCallbacks; std::condition_variable s_Cv; std::mutex s_Mtx; int s_ActiveCallCount = 0; extern "C" void __declspec(dllexport) ClearAllManagedCallbacks() { s_ShouldStopCallbacks = true; std::lock_guard<std::mutex> lock(s_Mtx); s_ActiveCallbacks.clear(); } extern "C" void __declspec(dllexport) WaitForCallbacksComplete() { std::unique_lock<std::mutex> lock(s_Mtx); s_Cv.wait(lock, []{ return s_ActiveCallCount == 0; }); } void InvokeManagedCallback(int32_t eventId, void* data) { if (s_ShouldStopCallbacks) return; std::lock_guard<std::mutex> lock(s_Mtx); s_ActiveCallCount++; for (auto& cb : s_ActiveCallbacks) { cb(eventId, data); } s_ActiveCallCount--; s_Cv.notify_one(); }
2. 避免静态委托的跨域重载残留
域重载时静态字段会被重新初始化,旧委托实例可能仍被非托管侧持有。可在每次进入PlayMode时重新注册委托:
public void Start() { // 每次进入PlayMode重新绑定委托 makeCallbackDelegate = MakeGraphicsCallback; var funcPtr = Marshal.GetFunctionPointerForDelegate(makeCallbackDelegate); NativePlugin.RegisterManagedCallback(funcPtr); cmdBuffer.IssuePluginEventAndData(funcPtr, 0, IntPtr.Zero); }
3. 托管回调内增加域重载状态检查
在托管回调中先判断是否处于域重载阶段,避免无效执行:
public static class DomainReloadStatus { public static volatile bool IsDomainReloading = false; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Init() { IsDomainReloading = false; EditorApplication.playModeStateChanged += state => { if (state == PlayModeStateChange.ExitingPlayMode) { IsDomainReloading = true; } }; } } [MonoPInvokeCallback(typeof(PluginDelegate))] private static unsafe void MakeGraphicsCallback(int eventId, void* data) { if (DomainReloadStatus.IsDomainReloading) { return; } // 原业务逻辑 }
4. 改用Unity Job System替代原生线程(可选)
若业务允许,使用Unity Job System处理异步任务,它会自动处理域重载时的任务清理,避免直接使用原生线程调用托管函数。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

