原文地址:http://jacksondunstan.com/articles/2949
正文:
dll——像flash里的swc一样——是一种方便的把你的代码转换成可复用模块的方法。不幸的是,Unity因为自身的原因(不支持JIT编译),导致在IOS和其他一些平台上会崩溃。最大的问题就是你在dll里使用C#事件。本文探究了这个问题发生的原因并找出一个简单可行的解决方案。让我们开始学习如何安全的在Unity的自定义dll里使用C#事件吧!
让我们写一个C#事件的简单例子来说明这个问题:
public class Normal
{
//声明一个事件回调用的委托
public delegate void SomethingHandler();
// 声明一个事件,使用刚才声明的委托
public event SomethingHandler OnSomething = () => {};
private void DispatchTheEvent()
{
// 派发事件
OnSomething();
}
}
当你在ios或者其他不支持JIT的平台上运行这个Unity程序的时候你会得到下面的报错:
ExecutionEngineException: Attempting to JIT compile method '(wrapper managed-to-native) System.Threading.Interlocked:CompareExchange |
为了找出为啥报错,让我们使用mono3.12来编译这个dll,mono3.12是本文写作时的当前版本。如果你使用MonoDevelop/Xamarin Studio,Mono正是编译你的dll所用的编译器。编译好dll后,我们使用.NET反编译工具查看这个dll,可以看到下面这些用来添加和移除事件回调的函数:
public void add_OnSomething(SomethingHandler value)
{
SomethingHandler handler2;
SomethingHandler onSomething = this.OnSomething;
do
{
handler2 = onSomething;
onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, (SomethingHandler) Delegate.Combine(handler2, value), onSomething);
}
while (onSomething != handler2);
}
public void remove_OnSomething(SomethingHandler value)
{
SomethingHandler handler2;
SomethingHandler onSomething = this.OnSomething;
do
{
handler2 = onSomething;
onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, (SomethingHandler) Delegate.Remove(handler2, value), onSomething);
}
while (onSomething != handler2);
}
就像你看到的那样,编译器产生的代码包含了对
System.Threading.Interlocked.CompareExchange
的调用,他会尝试JIT编译。
但是为什么在非dll的代码里不会发生这个问题呢?把相同的代码放到Unity的Asset目录下,反编译Library/ScriptAssemblies/Assembly-CSharp.dll
,会得到下面的代码:
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_OnSomething(SomethingHandler value)
{
this.OnSomething = (SomethingHandler) Delegate.Combine(this.OnSomething, value);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_OnSomething(SomethingHandler value)
{
this.OnSomething = (SomethingHandler) Delegate.Remove(this.OnSomething, value);
}
我们看到Unity4.6生成的代码和mono3.12是完全不同的。这些生成的代码不包含
CompareExchange
的调用,因此也不会试图进行JIT编译,也就没有错误啦。不幸的是,为了解决这个问题,我们只能把一部分代码移出dll。但是如果我就是想把这部分代码放到dll里,那就试试另一个编译器:Microsoft Visual Studio 2013:
public void add_OnSomething(SomethingHandler value)
{
SomethingHandler handler2;
SomethingHandler onSomething = this.OnSomething;
do
{
handler2 = onSomething;
SomethingHandler handler3 = (SomethingHandler) Delegate.Combine(handler2, value);
onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, handler3, handler2);
}
while (onSomething != handler2);
}
public void remove_OnSomething(SomethingHandler value)
{
SomethingHandler handler2;
SomethingHandler onSomething = this.OnSomething;
do
{
handler2 = onSomething;
SomethingHandler handler3 = (SomethingHandler) Delegate.Remove(handler2, value);
onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, handler3, handler2);
}
while (onSomething != handler2);
}
vs产生的代码和mono3.12类似,但是有一点不同。关键是它仍然调用了
CompareExchange
,仍然会产生JIT的问题。
鉴于编译器产生的代码都会触发JIT,让我们自己写一套代码添加和删除事件监听。这是我们的方案:
public class Workaround
{
// 声明一个委托
public delegate void SomethingHandler();
// 创建一个委托的实例
private SomethingHandler somethingInvoker = () => {};
//创建事件,并自定义add和remove函数
public event SomethingHandler OnSomething
{
// 把监听监听器添加到委托
add { somethingInvoker += value; }
// 把监听器从委托移除
remove { somethingInvoker -= value; }
}
private void DispatchTheEvent()
{
// 派发事件
somethingInvoker();
}
}
add
和
remove
函数不太重要,但是定义了它们就会阻止编译器产生它们自己的实现。我们再把dll反编译,下面是mono3.12编译器产生的代码:
public void add_OnSomething(SomethingHandler value)
{
this.somethingInvoker = (SomethingHandler) Delegate.Combine(this.somethingInvoker, value);
}
public void remove_OnSomething(SomethingHandler value)
{
this.somethingInvoker = (SomethingHandler) Delegate.Remove(this.somethingInvoker, value);
}
有两点需要注意的,第一也是最重要的一点,没有任何函数使用
CompareExchange
。此外,第二点,这些代码和Unity4.6编译器生成的代码一样。这意味着我们有效的绕开了JIT的问题。下一步,让我们看看在Visual Studio 2013里这个方法好使么:
public void add_OnSomething(SomethingHandler value)
{
this.somethingInvoker = (SomethingHandler) Delegate.Combine(this.somethingInvoker, value);
}
public void remove_OnSomething(SomethingHandler value)
{
this.somethingInvoker = (SomethingHandler) Delegate.Remove(this.somethingInvoker, value);
}
生成的代码和Unity编译器生成的一样,也就是说解决了JIT的问题。
该方法由两点不足。首先要多写点代码,因为你定义了自己的add
和remove
方法。下面是一个比较:
public class Normal
{
public delegate void SomethingHandler();
public event SomethingHandler OnSomething = () => {};
}
public class Workaround
{
public delegate void SomethingHandler();
private SomethingHandler somethingInvoker = () => {};
public event SomethingHandler OnSomething
{
add { somethingInvoker += value; }
remove { somethingInvoker -= value; }
}
}
第二点不足是
Interlocked.CompareExchange
函数更高效。但是仅在添加和移除事件时才会有效率损失,派发事件都比它们频繁,因此综上所述我们认为它应用程序的性能影响很小。
这个解决方案允许我们在Unity dll里使用C#事件,并且对性能影响很小,却保证了应用程序在非JIT平台上能正确运行。如果你知道其他解决方案,请在评论里留言给我:)