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

unity · 浏览次数 : 0

小编点评

**1. 使用 StackTrace 类** ```csharp public class TestClass : MonoBehaviour { private void Start() { StartCoroutine(A()); } private IEnumerator A() { var st = new StackTrace(true); mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString()); yield return B(); mStb.AppendLine(st.GetFrame(1).GetFileLineNumber().ToString()); yield return null; } // 打印:141924 } ``` **2. 使用 CoroutineHelper 类** ```csharp public 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; } } public class TestClass : MonoBehaviour { private void Start() { StartCoroutine(A()); } private IEnumerator A() { CoroutineHelper.BeginStackTraceStabDot(); yield return B().StackTrace; CoroutineHelper.EndStackTraceStabDot(); } // 打印:141924 } ``` **3. 使用扩展方法与using 语法糖** ```csharp public static class CoroutineHelper { public static IEnumerator StackTrace(this IEnumerator enumerator) { BeginStackTraceStabDot(); return enumerator; } } public class TestClass : MonoBehaviour { private void Start() { StartCoroutine(A()); } private IEnumerator A() { yield return B().StackTrace(); yield return C().StackTrace(); } } ```

正文

现如今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()重新封装协程的执行流程,或直接使用async方案。

不过那样就太重了,经过摸索后发现,还是存在一些方法,可以简单的实现完整栈信息的获取。

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();
    }
}

 

最终调用时如下:

private void Start()
{
    StartCoroutine(A());
}
private IEnumerator A()
{
    using var _ = CoroutineHelper.StackTracePop();

    yield return null;yield return B().StackTrace();
    //...
}
private IEnumerator B()
{
    using var _ = CoroutineHelper.StackTracePop();

    yield return null;
    yield return C().StackTrace();
    yield return null;//...

}

 

3.打印输出

通过StackTrace类以及语法糖处理,可以拿到完整栈信息后,还需要打印输出,

我们可以加入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());
}

 

最终效果如下:

 

与扩展实现Unity协程的完整栈跟踪相似的内容:

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

现如今Unity中的协程(Coroutine)方案已显得老旧,Unitask等异步方案可以直接解决如异常捕获等各类问题, 并且Unity官方也在开发一套异步方案,但现阶段还是需要在协程这个方案上继续琢磨。 Unity协程中无法输出完整的栈跟踪,因为协程编译后会转换为IL编码的状态机,中间存在栈回到堆

Spring缓存是如何实现的?如何扩展使其支持过期删除功能?

我们希望将这些rpc结果数据缓存起来,并在一定时间后自动删除,以实现在一定时间后获取到最新数据。类似Redis的过期时间。本文是我的调研步骤和开发过程。

造轮子之多语言管理

多语言也是我们经常能用到的东西,asp.net core中默认支持了多语言,可以使用.resx资源文件来管理多语言配置。但是在修改资源文件后,我们的应用服务无法及时更新,属实麻烦一些。我们可以通过扩展IStringLocalizer,实现我们想要的多语言配置方式,比如Json配置,PO 文件配置,E

Dubbo3应用开发—XML形式的Dubbo应用开发和SpringBoot整合Dubbo开发

Dubbo3程序的初步开发 Dubbo3升级的核心内容 易⽤性 开箱即⽤,易⽤性⾼,如 Java 版本的⾯向接⼝代理特性能实现本地透明调⽤功能丰富,基于原⽣库或轻量扩展即可实现绝⼤多数的 微服务治理能⼒。更加完善了多语言支持(GO PYTHON RUST) 超⼤规模微服务实践 ⾼性能通信(Tripl

从零实现的Chrome扩展

# 从零实现的Chrome扩展 `Chrome`扩展是一种可以在`Chrome`浏览器中添加新功能和修改浏览器行为的软件程序,例如我们常用的`TamperMonkey`、`Proxy SwitchyOmega`、`AdGuard`等等,这些拓展都是可以通过`WebExtensions API`来修改

如何在kubernetes中实现分布式可扩展的WebSocket服务架构

如何在kubernetes中实现分布式可扩展的WebSocket服务架构 How to implement a distributed and auto-scalable WebSocket server architecture on Kubernetes一文中虽然解决是WebSocket长连接问

乐高式扩展:在Seal软件供应链防火墙中轻松集成代码规范工具

上个月,Seal 软件供应链防火墙 v0.2(以下简称“Seal”)正式发布,这一版本实现了可扩展架构,用户可以根据自身需求插件式集成原生或第三方解决方案,灵活扩展扫描能力。 在前一个版本中,Seal 集成了 SCA、SAST 和配置检查等功能,在这一架构中最大的优势是调试方便、调用链路短,但同时也

在DevExpress的GridView的列中,使用RepositoryItemSearchLookUpEdit控件实现产品列表信息的展示和选择

有时候,我们为了方便,我们往往使用扩展函数的代码方式创建很多GridView的操作功能,如在随笔《在DevExpress中使用BandedGridView表格实现多行表头的处理》中介绍过多行表头的创建及绑定处理,在《基于DevExpress的GridControl实现的一些界面处理功能》也介绍了一些特殊的展示效果,本篇随笔介绍在DevExpress的GridView的列中,使用Repository

[转帖]XCopy命令实现增量备份

https://www.cnblogs.com/pachongshangdexuebi/p/5051977.html xcopy XCOPY是COPY的扩展,可以把指定的目录连文件和目录结构一并拷贝,但不能拷贝系统文件;使用时源盘符、源目标路径名、源文件名至少指定一个;选用/S时对源目录下及其子目录

一个.NET 7 + DDD + CQRS +React+Vite的实战项目

## 项目简介 基于SignalR实现聊天通信,支持横向扩展,可支撑上万用户同时在线聊天 ## 快速体验 http://server.tokengo.top:8888/ 可在这里快速体验使用,请注意目前只适配了PC端,请勿使用手机访问,可能出现样式不适应的情况, 当然如果你想要自己部署也可以,目前提