Unity Editor 崩溃问题分析与修复记录:反射调用 ReactiveProperty 时崩溃
一、问题概述
在实现 ViewModel ↔ Model 双向绑定 时,
使用反射调用 ReactiveProperty<T>.Subscribe(Action<T>)
并同步 Value
时,
Unity 编辑器直接崩溃(非异常抛出)。
崩溃发生在以下场景:
-
泛型类型:
ReactiveProperty<TEnum>
-
使用反射订阅:
csharpSubscribeMethod.Invoke(null, new object[] { reactiveProp, action });
-
使用反射给枚举类型属性赋值:
csharppropertyInfo.SetValue(target, value);
Unity 控制台没有任何异常提示,只提示:
A crash has been intercepted by the crash handler.
See crash report in: C:/Users/.../AppData/Local/Temp/Unity/Editor/Crashes
二、崩溃堆栈(关键段)
mono_object_handle_isinst
ves_icall_RuntimeTypeHandle_IsInstanceOfType_raw
System.RuntimeType:IsInstanceOfType (object)
System.Reflection.RuntimePropertyInfo:SetValue (object,object)
EnumDescriptorViewModel/<>c__DisplayClass13_0:<.ctor>b__0 (object)
R3.ReactiveProperty`1<TEnum>:set_Value (TEnum)
结论:
Unity 内部在
PropertyInfo.SetValue
时,Mono 的类型检查逻辑访问了空指针。这是 Mono 特有的"类型句柄误判"问题。
三、问题根源分析
根本原因
-
Mono 的反射类型检查存在 Bug
当传入的对象是一个 从泛型参数推断出的枚举值 时(例如
TEnum
),Mono 运行时在执行
IsInstanceOfType
时无法正确识别类型句柄。 -
Delegate.CreateDelegate 装箱转换隐患
原始委托使用了
Action<object>
绑定到Action<TEnum>
,在某些 Unity/Mono 版本下,这会触发内部类型适配逻辑错误。
这两个问题叠加,最终导致:
⚠️ Mono 在 Editor 模式中访问了一个无效的对象句柄,从而直接崩溃。
四、最小复现示例
csharp
public enum AxisType { X, Y, Z }
public class Demo
{
public ReactiveProperty<AxisType> Model = new ReactiveProperty<AxisType>(AxisType.X);
public Demo()
{
var vm = new ReactiveProperty<AxisType>(AxisType.X);
// 反射绑定(示例)
var prop = typeof(ReactiveProperty<AxisType>).GetProperty("Value");
// 此行会导致 Unity 崩溃(非异常)
prop.SetValue(Model, vm.Value);
}
}
五、解决方案
修复点 1:在 SetValue 前调用 Enum.ToObject
csharp
if (property.PropertyType.IsEnum && value != null)
{
// 防止 Mono 类型识别错误
value = Enum.ToObject(property.PropertyType, value);
}
property.SetValue(target, value);
解释:
重新封装枚举值为目标类型对象,确保 Mono 在运行时能识别类型。
修复点 2:使用 Expression 生成强类型委托
csharp
var param = Expression.Parameter(elementType, "x");
var body = Expression.Invoke(Expression.Constant(onNext), Expression.Convert(param, typeof(object)));
var lambda = Expression.Lambda(actionType, body, param);
var action = lambda.Compile();
解释:
Expression 创建出的委托是真正的
Action<TEnum>
类型,避免了跨类型装箱拆箱带来的潜在崩溃。
六、完整的安全封装示例
csharp
public static class SafeSubscribeUtility
{
public static IDisposable SubscribeSafe(object reactiveProp, Action<object> onNext)
{
var type = reactiveProp.GetType();
var elementType = type.GetGenericArguments()[0];
// 构造强类型 Action<T>
var param = Expression.Parameter(elementType, "x");
var body = Expression.Invoke(Expression.Constant(onNext), Expression.Convert(param, typeof(object)));
var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(elementType), body, param);
var action = lambda.Compile();
// 反射调用扩展方法 Subscribe<T>(Action<T>)
var method = typeof(ObservableSubscribeExtensions)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.First(m => m.Name == "Subscribe" && m.GetParameters().Length == 2)
.MakeGenericMethod(elementType);
return (IDisposable)method.Invoke(null, new object[] { reactiveProp, action });
}
public static void SetEnumValueSafe(PropertyInfo property, object target, object value)
{
if (property.PropertyType.IsEnum && value != null)
value = Enum.ToObject(property.PropertyType, value);
property.SetValue(target, value);
}
}
七、经验总结
问题 | 根源 | 修复手段 |
---|---|---|
PropertyInfo.SetValue 崩溃 |
Mono 识别枚举类型句柄错误 | Enum.ToObject 重新封箱 |
Delegate.CreateDelegate 不稳定 |
Action → Action 装箱 | 用 Expression.Lambda 创建强类型委托 |
Unity Editor 崩溃无异常 | Mono native 崩溃,无托管异常 | 加入类型显式转换保障 |
八、延伸建议
- 尽量避免 Editor 下频繁使用反射绑定 ReactiveProperty;
- 若需运行时动态绑定,请封装在工具类中统一使用;
- 如果可能,使用源码生成(如 Source Generator)代替反射。
一句话总结:
"Unity 崩溃并不是逻辑错误,而是 Mono 在处理反射泛型枚举赋值时的类型句柄 Bug。
通过
Enum.ToObject
+ 强类型委托修正,就能彻底规避此问题。"
(本文由本人撰写,ChatGPT润色)