现如今Unity中的协程(Coroutine)机制已略显陈旧,随着Unitask等异步方案的崭露头角,诸如协程异常等问题也迎刃而解
并且Unity官方也在开发一套异步方案,但对于仍使用协程的项目,依旧需要在这个方案上继续琢磨。
众所周知Unity协程中无法输出完整的栈跟踪,因为协程编译后会转换为IL编码的状态机,中间存在栈回到堆的过程,因此
假如在有多干yield函数嵌套的协程中出现报错,看到的栈信息会是缺失的:
- public class TestClass : MonoBehaviour {
- private void Start() {
- StartCoroutine(A());
- }
- private IEnumerator A() {
- yield return B();
- }
- private IEnumerator B() {
- yield return C();
- yield return null;
- }
- private IEnumerator C() {
- yield return null;
- Debug.Log("C");
- }
- }
输出(栈信息丢失):
- C
- UnityEngine.Debug:Log (object)
- TestClass/<C>d__3:MoveNext () (at Assets/TestClass.cs:31)
- UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
若要比较好的解决这个问题,是不是只能拿到MoveNext()重新封装或采用Unitask。
不过那样就太重了,经过摸索后发现,还是存在一些可行的途径。
1.StackTrace类打印栈跟踪
使用StackTrace类可以得到当前执行栈的相关信息,通过接口GetFrame可以得到当前哪一层调用的相关信息:
- public class TestClass : MonoBehaviour {
- private void Start() {
- Method1();
- }
- private void Method1() {
- Method2();
- }
- private void Method2() {
- var st = new System.Diagnostics.StackTrace(true);
- var sf = st.GetFrame(0);
- Debug.Log(sf.GetMethod().Name);
- sf = st.GetFrame(1);
- Debug.Log(sf.GetMethod().Name);
- sf = st.GetFrame(2);
- Debug.Log(sf.GetMethod().Name);
- //Print:
- //Method2
- //Method1
- //Start
- }
- }
但是之前提到,协程会在编译后转换为状态机,所以此处的代码就得不到栈信息:
- public class TestClass : MonoBehaviour {
- private void Start() {
- StartCoroutine(A());
- }
- private IEnumerator A() {
- yield return null;
- yield return B();
- }
- private IEnumerator B() {
- yield return null;
- Debug.Log("Hello");
- }
- }
打印:
- Hello
- UnityEngine.Debug:Log (object)
- TestClass/<B>d__2:MoveNext () (Assets/TestClass.cs:14)
- UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
抖个机灵,如果在非yield语句中进行常规代码的调用或函数调用,则可正常拿到类名和代码行数:
- 1 public class TestClass : MonoBehaviour
- 2 {
- 3 private StringBuilder mStb = new StringBuilder(1024);
- 4
- 5 private void Start() {
- 6 StartCoroutine(A());
- 7 }
- 8 private IEnumerator A() {
- 9 StackTrace st = new StackTrace(true);
- 10 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
- 11 yield return B();
- 12 }
- 13 private IEnumerator B() {
- 14 StackTrace st = new StackTrace(true);
- 15 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
- 16 yield return C();
- 17 }
- 18 private IEnumerator C() {
- 19 StackTrace st = new StackTrace(true);
- 20 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
- 21 yield return null;
- 22 UnityEngine.Debug.Log(mStb.ToString());
- 23 }
- 24 }
打印:
14
19
24
下面将基于这个思路继续扩展。
2.StackTrace封装
2.1 Begin/End 语句块
下一步,创建一个类CoroutineHelper存放协程的相关扩展,先在类中添加一个栈对象,保存每一步的栈跟踪信息:
- public static class CoroutineHelper
- {
- private static StackTrace[] sStackTraceStack;
- private static int sStackTraceStackNum;
- static CoroutineHelper()
- {
- sStackTraceStack = new StackTrace[64];
- sStackTraceStackNum = 0;
- }
- public static void BeginStackTraceStabDot() {
- sStackTraceStack[sStackTraceStackNum] = new StackTrace(true);
- ++sStackTraceStackNum;
- }
- public static void EndStackTraceStabDot() {
- sStackTraceStack[sStackTraceStackNum-1] = null;
- --sStackTraceStackNum;
- }
- }
注意这里没有直接用C#自己的Stack,是因为无法逆序遍历不方便输出栈日志,因此直接采用数组实现。
若这样的话,每一步协程函数跳转都要用Begin、End语句包装又太丑。
- private void Start() {
- StartCoroutine(A());
- }
- private IEnumerator A() {
- CoroutineHelper.BeginStackTraceStabDot();
- yield return B();
- CoroutineHelper.EndStackTraceStabDot();
- }
2.2 使用扩展方法与using语法糖优化
实际上非yield语句,普通函数调用也是可以的,编译后不会被转换。因此可用扩展方法进行优化:
- public static class CoroutineHelper
- {
- //加入了这个函数:
- public static IEnumerator StackTrace(this IEnumerator enumerator)
- {
- BeginStackTraceStabDot();
- return enumerator;
- }
- }
这样调用时就舒服多了,对原始代码的改动也最小:
- private void Start() {
- StartCoroutine(A());
- }
- private IEnumerator A() {
- yield return B().StackTrace();
- }
- private IEnumerator B() {
- yield return C().StackTrace();
- }
不过还需要处理函数结束时调用Pop方法,这个可以结合using语法糖使用:
- //加入该结构体
- public struct CoroutineStabDotAutoDispose : IDisposable {
- public void Dispose() {
- CoroutineHelper.EndStackTraceStabDot();
- }
- }
- public static class CoroutineHelper
- {
- //加入该函数
- public static CoroutineStabDotAutoDispose StackTracePop() {
- return new CoroutineStabDotAutoDispose();
- }
- }
加入Pop处理后调用时如下:
- private void Start()
- {
- StartCoroutine(A());
- }
- private IEnumerator A()
- {
- using var _ = CoroutineHelper.StackTracePop();
- yield return B().StackTrace();
- //...
- }
- private IEnumerator B()
- {
- using var _ = CoroutineHelper.StackTracePop();
- yield return C().StackTrace();
- //...
- }
2.3 不使用Using语法糖
后来我想到StackTrace可以拿到某一调用级的Method,可以通过比较之前记录的StackTrace查看有没有重复Method来确认
是否退出栈,因此可以优化掉Using语法糖的Pop操作。
修改函数如下:
- public static void StackTraceStabDot()
- {
- var currentTrack = new StackTrace(true);
- var currentTrackSf = currentTrack.GetFrame(2);
- for (int i = sStackTraceStackNum - 1; i >= 0; --i)
- {
- var sf = sStackTraceStack[i].GetFrame(2);
- if (sf.GetMethod().GetHashCode() == currentTrackSf.GetMethod().GetHashCode())
- {
- for (int j = i; j < sStackTraceStackNum; ++j)
- sStackTraceStack[j] = null;
- sStackTraceStackNum = i;
- break;
- }
- }
- sStackTraceStack[sStackTraceStackNum] = currentTrack;
- ++sStackTraceStackNum;
- }
这样也是最简洁的(没测试过复杂情形,可能存在Bug):
- private void Start() {
- StartCoroutine(A());
- }
- private IEnumerator A() {
- yield return B().StackTrace();
- }
- private IEnumerator B() {
- yield return C().StackTrace();
- }
3.打印输出
在拿到完整栈信息后,还需要打印输出,
我们可以加入Unity编辑器下IDE链接的语法,这样打印日志直接具有超链接效果:
- public static void PrintStackTrace()
- {
- var stb = new StringBuilder(4096);
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- for (int i = 0; i < sStackTraceStackNum; ++i)
- {
- var sf = sStackTraceStack[i].GetFrame(2);
- stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
- }
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- UnityEngine.Debug.Log(stb.ToString());
- }
最终效果如下:

4.源码
最后提供下这部分功能源码。
需要手动触发Pop函数,稳定版:

- using System;
- using System.Collections;
- using System.Diagnostics;
- using System.Text;
- public struct CoroutineStabDotAutoDispose : IDisposable
- {
- public void Dispose()
- {
- CoroutineHelper.EndStackTraceStabDot();
- }
- }
- public static class CoroutineHelper
- {
- private static StackTrace[] sStackTraceStack;
- private static int sStackTraceStackNum;
- static CoroutineHelper()
- {
- sStackTraceStack = new StackTrace[64];
- sStackTraceStackNum = 0;
- }
- public static CoroutineStabDotAutoDispose StackTracePop()
- {
- return new CoroutineStabDotAutoDispose();
- }
- public static IEnumerator StackTrace(this IEnumerator enumerator)
- {
- BeginStackTraceStabDot();
- return enumerator;
- }
- public static void BeginStackTraceStabDot()
- {
- sStackTraceStack[sStackTraceStackNum] = new StackTrace(true);
- ++sStackTraceStackNum;
- }
- public static void EndStackTraceStabDot()
- {
- sStackTraceStack[sStackTraceStackNum - 1] = null;
- --sStackTraceStackNum;
- }
- public static void PrintStackTrace()
- {
- var stb = new StringBuilder(4096);
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- for (int i = 0; i < sStackTraceStackNum; ++i)
- {
- var sf = sStackTraceStack[i].GetFrame(2);
- stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
- }
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- UnityEngine.Debug.Log(stb.ToString());
- }
- }
View Code
比较之前记录的StackTrace,无需手动触发Pop函数,可能有bug版:

- using System;
- using System.Collections;
- using System.Diagnostics;
- using System.Text;
- public static class CoroutineHelper
- {
- private static StackTrace[] sStackTraceStack;
- private static int sStackTraceStackNum;
- static CoroutineHelper()
- {
- sStackTraceStack = new StackTrace[64];
- sStackTraceStackNum = 0;
- }
- public static IEnumerator StackTrace(this IEnumerator enumerator)
- {
- StackTraceStabDot();
- return enumerator;
- }
- public static void StackTraceStabDot()
- {
- var currentTrack = new StackTrace(true);
- var currentTrackSf = currentTrack.GetFrame(2);
- for (int i = sStackTraceStackNum - 1; i >= 0; --i)
- {
- var sf = sStackTraceStack[i].GetFrame(2);
- if (sf.GetMethod().GetHashCode() == currentTrackSf.GetMethod().GetHashCode())
- {
- for (int j = i; j < sStackTraceStackNum; ++j)
- sStackTraceStack[j] = null;
- sStackTraceStackNum = i;
- break;
- }
- }
- sStackTraceStack[sStackTraceStackNum] = currentTrack;
- ++sStackTraceStackNum;
- }
- public static void PrintStackTrace()
- {
- var stb = new StringBuilder(4096);
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- for (int i = 0; i < sStackTraceStackNum; ++i)
- {
- var sf = sStackTraceStack[i].GetFrame(2);
- stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
- }
- stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
- UnityEngine.Debug.Log(stb.ToString());
- }
- }
View Code
5.异常捕获+完整栈跟踪
知乎上找了一个协程异常捕获的扩展:
https://zhuanlan.zhihu.com/p/319551938
然后就可以实现协程异常捕获+完整栈跟踪:
- public class TestClass : MonoBehaviour {
- private void Start() {
- StartCoroutine(new CatchableEnumerator(A(), () => {
- CoroutineHelper.PrintStackTrace();
- }));
- }
- private IEnumerator A() {
- yield return B().StackTrace();
- }
- private IEnumerator B() {
- yield return C().StackTrace();
- }
- private IEnumerator C() {
- yield return null;throw new System.Exception();
- }
- }