Unity里使用包含C#事件的Dll

原文地址: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();
	}
}

addremove函数不太重要,但是定义了它们就会阻止编译器产生它们自己的实现。我们再把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的问题。

该方法由两点不足。首先要多写点代码,因为你定义了自己的addremove方法。下面是一个比较:

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平台上能正确运行。如果你知道其他解决方案,请在评论里留言给我:)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值