因为项目中加载大量资源时造成卡顿,所以打算用异步协同来处理,但是却碰到自己难以理解的一个问题。
问题描述:
在 异步函数中 ,对界面上的 9 个按钮进行 onClick 设置匿名函数,函数使用Log 打印出当前的Button 的 Index 。代码看起来没有问题,但是测试发现
点击所有按钮 都输出了 8 ,也就是说,虽然我在代码中重新创建了一个 int 值并赋值index的值,但是实际上却根本没有生效。
如下:
代码:
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 。
想不出自己的异步代码哪里有问题,难道我每次创建的局部变量 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。
自己想了两天无果,于是请教各位同事大神。一位同事给出答复,这是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代码。
反编译的流程如上篇文章所讲:
Unity3d 反编译破解游戏 简单示例 (使用ildasm反编译DLL修改然后重新编译DLL)
首先了解 C# 中的闭包
/article/details/46576279
知识点先行:
对于匿名函数中的变量,C#在编译成 IL 代码的时候,会为这些变量创建一个类。
如下图
我们看到同步函数中的 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是正确的值。
异步代码反编译出来的 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() 的代码
.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 。
至此问题解决。
关于反编译DLL方法:
/article/details/46573327
关于C#闭包
/article/details/46576279
关于 ildasm使用及IL 语法介绍
/article/details/46573417
关于更多IL 语法命令查询
/article/details/46573435
示例工程下载:
http://pan.baidu.com/s/1sjLsjrn