因为项目中加载大量资源时造成卡顿,所以打算用异步协同来处理,但是却碰到自己难以理解的一个问题。

问题描述:

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

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

如下:

Unity 显示在cube上的 不被遮挡_bug

代码:

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 。

Unity 显示在cube上的 不被遮挡_闭包_02

想不出自己的异步代码哪里有问题,难道我每次创建的局部变量 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 显示在cube上的 不被遮挡_Unity3d_03

自己想了两天无果,于是请教各位同事大神。一位同事给出答复,这是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());
}


测试结果


Unity 显示在cube上的 不被遮挡_Unity3d_04


然后问题到此就解决了。


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


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

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

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

首先了解 C# 中的闭包

/article/details/46576279


知识点先行:

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


如下图

Unity 显示在cube上的 不被遮挡_bug_05

我们看到同步函数中的 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的结构如下

Unity 显示在cube上的 不被遮挡_bug_06


主要查看 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