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

news/2024/9/26 5:19:43

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

 

最终效果如下:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/30577.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Object.values()对象遍历

Object.keys() 对象的遍历返回给定对象所有可枚举属性的数组;是属性名组成的数组let obj = { a: 1, b: 2, c: 3 };Object.keys(obj).map((key) => {console.log(key, obj[key]);}); Object.values() 对象的遍历 返回一个给定对象自身的所有属性值的数组; 是属性值组成的…

浅析OSERDESE3

在高速接口的应用场景下,我们会经常听说SerDes(Serializer-Deserializer)这个词,也就是串行器和解串器,更为通俗的讲就是进行串并转换的。在Xilinx的FPGA中提供了ISERDES(提供串行数据到并行数据的转换)和OSERDES(提供并行数据到串行数据的转换)。在7系列的FPGA里面提供…

玩转AI,笔记本电脑安装属于自己的Llama 3 8B大模型和对话客户端

2024 年 4 月 18 日,Meta**开源**了 Llama 3 大模型,把 AI 的门槛降低到了最低,这是人工智能领域的一个重要飞跃。我们个人也可以部署大模型了,这简直就是给个人开发者发了个大红包!Llama 3 模型有不同的参数版本,本文主要分享我在个人笔记本电脑是部署 8B 参数过程和编写…

ABC353 | 如同流星划过天空

AtCoder Beginner Contest 353 (7/7)ABC353 | 如同流星划过天空 A. & B. 依题意模拟即可。 C. 注意只有 \(f(x,y)\) 需要对 \(10^8\) 取模,\(f\) 的求和不需要。 关注到 \(a_i \in [1,10^8)\),故 \(a_i + a_j \in [2,2 \times 10^8)\)。 从而 \(f(x,y)=[x+y<10^8](x…

代码随想录算法训练营第四天 | 24.两辆交换链表中的节点

23.两两交换链表中的两个节点 题目链接 文章讲解 视频讲解时间复杂度 o(n) 空间复杂度 o(1)tips: 画图,并将每一步操作用伪代码写出来,然后将代码理顺可以避免修改代码的麻烦 初始化的时候就要将previous初始化为current的前一个节点,这样可以保证循环的第一步满足循环要求c…

CSRF(原理)

CSRF是什么?Cross-site request forgery 简称为“CSRF”,在CSRF的攻击场景中攻击者会伪造一个请求(这个请求一般是一个链接),然后欺骗目标用户进行点击,用户一旦点击了这个请求,整个攻击就完成了。所以CSRF攻击也成为"one click"攻击。 很多人搞不清楚CSRF的概…

XXE漏洞(Pikachu)

原理 要补好多知识~XXE漏洞全称XML External Entity Injection 即XML外部实体注入。 XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。 XXE漏洞触发…

平均汇总(Power Pivot)

问题:如何在数据透视表中显示类似列总计的平均汇总?解决:在数据模型中添加列Dax公式:=SUMX(区域,区域[数量]*(区域[物料编码]=earlier(区域[物料编码])))/distinctcount(区域[日期 (月)])数据透视表布局: 行字段:物料编码、平均 列字段:组后为月的日期 值字段:数量 其他…