首页 > 其他分享 >扩展实现Unity协程的完整栈跟踪

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

时间:2024-05-12 14:54:07浏览次数:13  
标签:StackTrace return yield private Unity static 跟踪 协程 public

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

 

最终调用时如下:

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

 

最终效果如下:

 

4.源码

最后提供下这部分功能源码:

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,return,yield,private,Unity,static,跟踪,协程,public
From: https://www.cnblogs.com/hont/p/18187817

相关文章

  • Lua热更学习--使用toLua中的协程
    [6]C#访问调table类中的成员变量和函数访问table中的变量和函数lua中可以使用table作为class,因此对table中的函数访问调用是必要的根据前面对table访问和function的获取调用,这里尝试获取调用。依然是如此,此种调用方式获取到的table中的函数是引用拷贝。Main.lua脚本新增内容......
  • m基于FPGA的MPPT最大功率跟踪算法verilog实现,包含testbench
    1.算法仿真效果其中Vivado2019.2仿真结果如下:   使用matlab进行显示如下:   2.算法涉及理论知识概要       在太阳能光伏系统中,最大功率点跟踪(MaximumPowerPointTracking,MPPT)是提高能量转换效率的关键技术之一。爬山法(HillClimbingAlgorithm,HCA)......
  • Unity Editor下运行DoTween动画
    DOTweenEditorPreview.PrepareTweenForPreview(tar.GetTween());DOTweenEditorPreview.Start();以Test脚本为例:publicclassUTest:MonoBehaviour{publicTweenGetTween(){vartw=transform.DOMove(Vector3.left,2);tw.onUpdate=()=>......
  • Unity性能优化——合批(Batching)的限制与失败原因汇总
    Unity中Batching大致可以分为StaticBatching,DynamicBatching,SRPBatching与GPUInstancing四大类,但在使用时我们经常会遇到合批失败的情况,这里汇总了四大类的合批使用限制与合批失败的关键错误信息.StaticBatching的限制额外的内存开销64000个顶点限制影响......
  • 进度管理与成本跟踪
      实验2:项目管理中的进度跟踪和成本跟踪本次实验内容是资源和成本管理部分,通过本次实验,学生将掌握以下内容:1、掌握Project中资源的进度跟踪;2、掌握Project中的成本跟踪。 [实验任务一]:Project中进度跟踪1.打开上次上机时提交的Project源文件2.在任务工作表中加入列......
  • Unity热更学习toLua使用--[1]toLua的导入和默认加载执行lua脚本
    [0]toLua的导入下载toLua资源包,访问GitHub项目地址,点击下载即可。将文件导入工程目录中:导入成功之后会出现Lua菜单栏,如未成功生成文件,可以点击GenerateAll重新生成(注意很可能是路径问题导致的生成失败!)之后就可以开始编写脚本执行第一个lua程序了![1]C#调用Lua脚本编写C#......
  • Unity面试手册:2021最新Unity面试题汇总
    1、什么是协同程序?答:在主线程运行时同时开启另一段逻辑处理,来协助当前程序的执行。换句话说,开启协程就是开启一个可以与程序并行的逻辑。可以用来控制运动、序列以及对象的行为。2、Unity3D中的碰撞器和触发器的区别?碰撞器是触发器的载体,而触发器只是碰撞器身上的一个属性。......
  • Sxstrace.exe 是 Windows 操作系统提供的一个工具,用于跟踪和分析应用程序的依赖项解析
    sxstrace|MicrosoftLearnSxstrace.exe是Windows操作系统提供的一个工具,用于跟踪和分析应用程序的依赖项解析过程。该工具可以帮助用户诊断应用程序启动或运行时出现的依赖项错误或加载问题。在Windows中,许多应用程序依赖于共享组件和库文件,如动态链接库(DLL)。当应用......
  • Python高阶---协程并发
    importasyncioimporttime====================================1.定义协程对象asyncdefhello(x):#time.sleep(x)#time.sleep是一个同步操作语句,无法达到异步的结果print('-222-',x)awaitasyncio.sleep(x)return'等待了{}秒'.format(x)#returnxifname==......
  • 跟羽夏去实现协程
    写在前面  此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章......