转自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"); }
转自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()); }
然后问题到此就解决了。
但是对于导致这个问题的原因却很纠结,于是又去请教另一位大神同事,大神回复我说,是因为Mono的实现和donet 的实现不一致导致,可以去查看 反编译出来的 IL 代码。
然后我就去看了 反编译出来的 IL代码。
反编译的流程如上篇文章所讲:
转自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
http://blog.csdn.net/huutu/article/details/46576279
http://blog.csdn.net/huutu/article/details/46573417
http://blog.csdn.net/huutu/article/details/46573435
http://pan.baidu.com/s/1sjLsjrn
http://download.csdn.net/detail/cp790621656/8825391