您的位置:首页 > 移动开发 > Unity3D

Mono2.0 对C#闭包 与 donet 不同的实现导致Unity的Bug 及解决方案

2015-06-20 23:27 507 查看
转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
因为项目中加载大量资源时造成卡顿,所以打算用异步协同来处理,但是却碰到自己难以理解的一个问题。

问题描述:

在 异步函数中 ,对界面上的 9 个按钮进行 onClick 设置匿名函数,函数使用Log 打印出当前的Button 的 Index 。代码看起来没有问题,但是测试发现

点击所有按钮 都输出了 8 ,也就是说,虽然我在代码中重新创建了一个 int 值并赋值index的值,但是实际上却根本没有生效。

如下:转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn


转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
代码:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{

[SerializeField]
List<GameObject> m_HeroObjList;

// Use this for initialization
void Start ()
{
StartCoroutine(TestAsync());
Debug.Log("end");
}

IEnumerator TestAsync()
{
Debug.Log("TestAsync");

for(int index=0;index<9;index++)
{
Button btn=m_HeroObjList[index].GetComponent<Button>();

int a=index;//从结果来看,每次创建一个 a 并没有效果

btn.onClick.AddListener(delegate {
ShowBtn(a);
});
}

Debug.Log("TestAsync end");

yield break;
}

void ShowBtn(int index)
{
Debug.Log(index.ToString());
}
}


结果如下图,不管点哪一个按钮,都是输出 8 。



转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
想不出自己的异步代码哪里有问题,难道我每次创建的局部变量 a 在C#中变成了成员变量不成 ,于是切换到同步代码测试。

void TestSync()
{
Debug.Log("TestSync");

for(int index=0;index<9;index++)
{
Button btn=m_HeroObjList[index].GetComponent<Button>();

int a=index;

btn.onClick.AddListener(delegate {
ShowBtn(a);
});
}

Debug.Log("TestSync end");
}


测试发现同步代码一切都正常,每个按钮都输出了正确的 index。



转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
自己想了两天无果,于是请教各位同事大神。一位同事给出答复,这是Unity的Bug,异步中不能像我这样写匿名函数,建议用delegate 来修改替换。

于是先使用Delegate 来替换匿名函数解决这个bug。

首先添加一个类 EventTriggerListener ,用来监听Unity 中的 点击事件,然后回调。

using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;

public class EventTriggerListener : UnityEngine.EventSystems.EventTrigger
{
public delegate void VoidDelegate(GameObject go,object data);
public VoidDelegate onClick;

private object m_Param=null;

public static EventTriggerListener Get(GameObject go)
{
EventTriggerListener listener=go.GetComponent<EventTriggerListener>();
if(listener==null)
{
listener=go.AddComponent<EventTriggerListener>();
}
return listener;
}

public void SetParam(object data)
{
m_Param=data;
}

public override void OnPointerClick (PointerEventData eventData)
{
base.OnPointerClick (eventData);
if(onClick!=null)
{
onClick(gameObject,m_Param);
}
}
}


然后代码修改如下:

IEnumerator TestAsyncUseDelegate()
{
Debug.Log("TestAsyncUseDelegate");

for(int index=0;index<9;index++)
{
Button btn=m_HeroObjList[index].GetComponent<Button>();

EventTriggerListener listener=EventTriggerListener.Get(btn.gameObject);
listener.SetParam(index);
listener.onClick+=ClickBtn;
}

Debug.Log("TestAsyncUseDelegate end");

yield break;
}

void ClickBtn(GameObject go,object data)
{
ShowBtn((int)data);
}

void ShowBtn(int index)
{
Debug.Log(index.ToString());
}


测试结果转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn


然后问题到此就解决了。

但是对于导致这个问题的原因却很纠结,于是又去请教另一位大神同事,大神回复我说,是因为Mono的实现和donet 的实现不一致导致,可以去查看 反编译出来的 IL 代码。

然后我就去看了 反编译出来的 IL代码。

反编译的流程如上篇文章所讲:

Unity3d 反编译破解游戏 简单示例 (使用ildasm反编译DLL修改然后重新编译DLL)

转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
首先了解 C# 中的闭包

http://blog.csdn.net/huutu/article/details/46576279


知识点先行:

对于匿名函数中的变量,C#在编译成 IL 代码的时候,会为这些变量创建一个类。

如下图



转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
我们看到同步函数中的 a 会创建出一个类 <TestSync>c_AnonStorey2

异步中的a ,嗯?没有为a 创建类,但是为 迭代器创建了一个类 '<TestAsync>c__Iterator1

首先来看看同步代码反编译出来的 IL 代码:

.method private hidebysig instance void  TestSync() cil managed
{
// 代码大小       101 (0x65)
//定义函数代码所用堆栈的最大深度,也可理解为Call Stack的变量个数
.maxstack  12

//以下我们把它看做是完成代码中的初始化
//定义 int 类型参数 V_0,class类型(Button)V_1,class类型(<TestSync>c__AnonStorey1)V_2
//(此时已经把V_0,V_1,V_2存入了Call Stack中)
.locals init (int32 V_0,
class [UnityEngine.UI]UnityEngine.UI.Button V_1,
class NewBehaviourScript/'<TestSync>c__AnonStorey1' V_2)

IL_0000:
//推送对元数据中存储的字符串("TestSync")的新对象引用。
ldstr      "TestSync"

IL_0005:
//调用由传递的方法说明符指示的方法(Debug::Log(object))。
call       void [UnityEngine]UnityEngine.Debug::Log(object)

IL_000a:
//将整数值 0 作为 int32 推送到计算堆栈上。
ldc.i4.0

IL_000b:
//从计算堆栈的顶部弹出当前值(index=0)并将其存储到索引 0 处的局部变量列表中。
stloc.0

IL_000c:
//无条件地将控制转移到目标指令(IL_0052)。
br         IL_0052

IL_0011:
//实例化AnonStorey1,就是变量a闭包产生的类
newobj     instance void NewBehaviourScript/'<TestSync>c__AnonStorey1'::.ctor()

IL_0016:
//从计算堆栈的顶部弹出当前值(Button)并将其存储到索引 2 处的局部变量列表中。
stloc.2

IL_0017:
//将指定索引处(2)的局部变量加载到计算堆栈上。
ldloc.2

IL_0018:
//将索引为 0 的参数加载到计算堆栈上。
ldarg.0

IL_0019:
//新值替换在对象引用或指针的字段中的值
stfld      class NewBehaviourScript NewBehaviourScript/'<TestSync>c__AnonStorey1'::'<>f__this'

IL_001e:
//将索引为 0 的参数加载到计算堆栈上。
ldarg.0

IL_001f:
//查找对象中其引用当前位于计算堆栈的字段的值。
ldfld      class [mscorlib]System.Collections.Generic.List`1<class [UnityEngine]UnityEngine.GameObject> NewBehaviourScript::m_HeroObjList

IL_0024:
//将索引为 0 的参数加载到计算堆栈上。
ldloc.0

IL_0025:
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<class [UnityEngine]UnityEngine.GameObject>::get_Item(int32)

IL_002a:
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
callvirt   instance !!0 [UnityEngine]UnityEngine.GameObject::GetComponent<class [UnityEngine.UI]UnityEngine.UI.Button>()

IL_002f:
//从计算堆栈的顶部弹出当前值(Button)并将其存储到索引 1 处的局部变量列表中。
stloc.1

IL_0030:
//将索引为 2 的参数加载到计算堆栈上。
ldloc.2

IL_0031:
//将索引为 0(index) 的参数加载到计算堆栈上。
ldloc.0

IL_0032:
//新值(index)替换在对象引用或指针的字段中的值 index替换a
stfld      int32 NewBehaviourScript/'<TestSync>c__AnonStorey1'::a

IL_0037:
//将索引为 1 的参数加载到计算堆栈上。
ldloc.1

IL_0038:
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
callvirt   instance class [UnityEngine.UI]UnityEngine.UI.Button/ButtonClickedEvent [UnityEngine.UI]UnityEngine.UI.Button::get_onClick()

IL_003d:
//将索引为 2 的参数加载到计算堆栈上。
ldloc.2

IL_003e:
//将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
ldftn      instance void NewBehaviourScript/'<TestSync>c__AnonStorey1'::'<>m__0'()

IL_0044:
//创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
newobj     instance void [UnityEngine]UnityEngine.Events.UnityAction::.ctor(object,native int)

IL_0049:
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
callvirt   instance void [UnityEngine]UnityEngine.Events.UnityEvent::AddListener(class [UnityEngine]UnityEngine.Events.UnityAction)

IL_004e:
//将指定索引处的局部变量加载到计算堆栈上。
ldloc.0

IL_004f:
//将整数值 1 作为 int32 推送到计算堆栈上。
ldc.i4.1

IL_0050:
//将两个值相加并将结果推送到计算堆栈上。
add

IL_0051:
//从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
stloc.0

IL_0052:
//将指定索引  0  处的局部变量加载到计算堆栈上。
ldloc.0
IL_0053:
//将提供的 int8 值(9)作为 int32 推送到计算堆栈上(短格式)。
ldc.i4.s   9

IL_0055:
//如果第一个值小于第二个值(9),则将控制转移到目标指令。(IL_0011)
blt        IL_0011

IL_005a:
//推送对元数据中存储的字符串("TestSync end")的新对象引用。
ldstr      "TestSync end"

IL_005f:
//调用由传递的方法说明符指示的方法(Debug::Log(object))。
call       void [UnityEngine]UnityEngine.Debug::Log(object)

IL_0064:
//从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。
ret

} // end of method NewBehaviourScript::TestSync


大意就是,对 index 与 9进行对比,如果小于9就跳转到开头继续 for循环,每次循环都会 实例化一个 <TestSync>c_AnonStorey2 (int a = index),所以在同步代码中,a是正确的值。

转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
异步代码反编译出来的 IL文件如下:

TestAsync:class[mscorlib]System.Collections.IEnumerator()

.method private hidebysig instance class [mscorlib]System.Collections.IEnumerator
TestAsync() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 )
// 代码大小       15 (0xf)
.maxstack  2
.locals init (class NewBehaviourScript/'<TestAsync>c__Iterator1' V_0)
IL_0000:  newobj     instance void NewBehaviourScript/'<TestAsync>c__Iterator1'::.ctor()
IL_0005:  stloc.0
IL_0006:  ldloc.0
IL_0007:  ldarg.0
IL_0008:  stfld      class NewBehaviourScript NewBehaviourScript/'<TestAsync>c__Iterator1'::'<>f__this'
IL_000d:  ldloc.0
IL_000e:  ret
} // end of method NewBehaviourScript::TestAsync


看到函数中实例化了 为 迭代器创建了一个类 '<TestAsync>c__Iterator1

然后'<TestAsync>c__Iterator1的结构如下



主要查看 MoveNext:bool() 的代码

转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
.method public hidebysig newslot virtual final
instance bool  MoveNext() cil managed
{
// 代码大小       157 (0x9d)
.maxstack  13
IL_0000:  ldarg.0
IL_0001:  ldfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::$PC
IL_0006:  ldarg.0
IL_0007:  ldc.i4.m1
IL_0008:  stfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::$PC
IL_000d:  brtrue     IL_009b
IL_0012:  ldstr      "TestAsync"
IL_0017:  call       void [UnityEngine]UnityEngine.Debug::Log(object)
IL_001c:  ldarg.0
IL_001d:  ldc.i4.0
IL_001e:  stfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_0023:  br         IL_007f
IL_0028:  ldarg.0
IL_0029:  ldarg.0
IL_002a:  ldfld      class NewBehaviourScript NewBehaviourScript/'<TestAsync>c__Iterator1'::'<>f__this'
IL_002f:  ldfld      class [mscorlib]System.Collections.Generic.List`1<class [UnityEngine]UnityEngine.GameObject> NewBehaviourScript::m_HeroObjList
IL_0034:  ldarg.0
IL_0035:  ldfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_003a:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<class [UnityEngine]UnityEngine.GameObject>::get_Item(int32)
IL_003f:  callvirt   instance !!0 [UnityEngine]UnityEngine.GameObject::GetComponent<class [UnityEngine.UI]UnityEngine.UI.Button>()
IL_0044:  stfld      class [UnityEngine.UI]UnityEngine.UI.Button NewBehaviourScript/'<TestAsync>c__Iterator1'::'<btn>__1'
IL_0049:  ldarg.0
IL_004a:  ldarg.0
IL_004b:  ldfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_0050:  stfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<a>__2'
IL_0055:  ldarg.0
IL_0056:  ldfld      class [UnityEngine.UI]UnityEngine.UI.Button NewBehaviourScript/'<TestAsync>c__Iterator1'::'<btn>__1'
IL_005b:  callvirt   instance class [UnityEngine.UI]UnityEngine.UI.Button/ButtonClickedEvent [UnityEngine.UI]UnityEngine.UI.Button::get_onClick()
IL_0060:  ldarg.0
IL_0061:  ldftn      instance void NewBehaviourScript/'<TestAsync>c__Iterator1'::'<>m__1'()
IL_0067:  newobj     instance void [UnityEngine]UnityEngine.Events.UnityAction::.ctor(object,
native int)
IL_006c:  callvirt   instance void [UnityEngine]UnityEngine.Events.UnityEvent::AddListener(class [UnityEngine]UnityEngine.Events.UnityAction)
IL_0071:  ldarg.0
IL_0072:  ldarg.0
IL_0073:  ldfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_0078:  ldc.i4.1
IL_0079:  add
IL_007a:  stfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_007f:  ldarg.0
IL_0080:  ldfld      int32 NewBehaviourScript/'<TestAsync>c__Iterator1'::'<index>__0'
IL_0085:  ldc.i4.s   9
IL_0087:  blt        IL_0028
IL_008c:  ldstr      "TestAsync end"
IL_0091:  call       void [UnityEngine]UnityEngine.Debug::Log(object)
IL_0096:  br         IL_009b
IL_009b:  ldc.i4.0
IL_009c:  ret
} // end of method '<TestAsync>c__Iterator1'::MoveNext


在这段代码中,a 其实是生成的这个类的成员变量,MoveNext中一直在对 a 进行重新赋值,而没有多次创建 a ,所以 a 的值是多次被修改最后修改为 8 。

转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
至此问题解决。

转自http://blog.转自http://blog.csdn.net/huutu http://www.thisisgame.com.cncsdn.net/huutu http://www.thisisgame.com.cn

关于反编译DLL方法:

http://blog.csdn.net/huutu/article/details/46573327


关于C#闭包

http://blog.csdn.net/huutu/article/details/46576279


关于 ildasm使用及IL 语法介绍

http://blog.csdn.net/huutu/article/details/46573417


关于更多IL 语法命令查询

http://blog.csdn.net/huutu/article/details/46573435


示例工程下载:

http://pan.baidu.com/s/1sjLsjrn
http://download.csdn.net/detail/cp790621656/8825391


转自http://blog.csdn.net/huutu http://www.thisisgame.com.cn
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: