扩展实现Unity协程的完整栈跟踪

现如今Unity中的协程(Coroutine)机制已略显陈旧,随着Unitask、ETTask等异步方案的崭露头角,诸如协程异常等问题也迎刃而解

并且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
    }
}

但是之前提到,协程会在编译后转换为状态机,所以此处的代码就得不到栈信息

 1 public class TestClass : MonoBehaviour {
 2     private void Start() {
 3         StartCoroutine(A());
 4     }
 5     private IEnumerator A() {
 6         yield return null;
 7         yield return B();
 8     }
 9     private IEnumerator B() {
10         yield return null;
11         var st = new StackTrace(true);
12         for (int i = 0; i < st.FrameCount; ++i)
13             UnityEngine.Debug.Log(st.GetFrame(i).GetFileLineNumber().ToString());
14         //print
15         //11
16         //0
17     }
18 }

不过抖个机灵,如果在非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();
    }
}

 

posted @ 2024-05-12 14:46  HONT  阅读(278)  评论(0编辑  收藏  举报