经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 软件/图像 » unity » 查看文章
扩展实现Unity协程的完整栈跟踪
来源:cnblogs  作者:HONT  时间:2024/5/13 8:54:05  对本文有异议

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

并且Unity官方也在开发一套异步方案,但对于仍使用协程的项目,依旧需要在这个方案上继续琢磨。

 

众所周知Unity协程中无法输出完整的栈跟踪,因为协程编译后会转换为IL编码的状态机,中间存在栈回到堆的过程,因此

假如在有多干yield函数嵌套的协程中出现报错,看到的栈信息会是缺失的:

  1. public class TestClass : MonoBehaviour {
  2. private void Start() {
  3. StartCoroutine(A());
  4. }
  5. private IEnumerator A() {
  6. yield return B();
  7. }
  8. private IEnumerator B() {
  9. yield return C();
  10. yield return null;
  11. }
  12. private IEnumerator C() {
  13. yield return null;
  14. Debug.Log("C");
  15. }
  16. }

输出(栈信息丢失):

  1. C
  2. UnityEngine.Debug:Log (object)
  3. TestClass/<C>d__3:MoveNext () (at Assets/TestClass.cs:31)
  4. UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)

若要比较好的解决这个问题,是不是只能拿到MoveNext()重新封装或采用Unitask。

不过那样就太重了,经过摸索后发现,还是存在一些可行的途径。

1.StackTrace类打印栈跟踪

使用StackTrace类可以得到当前执行栈的相关信息,通过接口GetFrame可以得到当前哪一层调用的相关信息:

  1. public class TestClass : MonoBehaviour {
  2. private void Start() {
  3. Method1();
  4. }
  5. private void Method1() {
  6. Method2();
  7. }
  8. private void Method2() {
  9. var st = new System.Diagnostics.StackTrace(true);
  10. var sf = st.GetFrame(0);
  11. Debug.Log(sf.GetMethod().Name);
  12. sf = st.GetFrame(1);
  13. Debug.Log(sf.GetMethod().Name);
  14. sf = st.GetFrame(2);
  15. Debug.Log(sf.GetMethod().Name);
  16. //Print:
  17. //Method2
  18. //Method1
  19. //Start
  20. }
  21. }

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

  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. Debug.Log("Hello");
  12. }
  13. }

打印:

  1. Hello
  2. UnityEngine.Debug:Log (object)
  3. TestClass/<B>d__2:MoveNext () (Assets/TestClass.cs:14)
  4. UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)

抖个机灵,如果在非yield语句中进行常规代码的调用或函数调用,则可正常拿到类名和代码行数:

  1. 1 public class TestClass : MonoBehaviour
  2. 2 {
  3. 3 private StringBuilder mStb = new StringBuilder(1024);
  4. 4
  5. 5 private void Start() {
  6. 6 StartCoroutine(A());
  7. 7 }
  8. 8 private IEnumerator A() {
  9. 9 StackTrace st = new StackTrace(true);
  10. 10 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
  11. 11 yield return B();
  12. 12 }
  13. 13 private IEnumerator B() {
  14. 14 StackTrace st = new StackTrace(true);
  15. 15 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
  16. 16 yield return C();
  17. 17 }
  18. 18 private IEnumerator C() {
  19. 19 StackTrace st = new StackTrace(true);
  20. 20 mStb.AppendLine(st.GetFrame(0).GetFileLineNumber().ToString());
  21. 21 yield return null;
  22. 22 UnityEngine.Debug.Log(mStb.ToString());
  23. 23 }
  24. 24 }

打印:

14
19
24

 

下面将基于这个思路继续扩展。

2.StackTrace封装

2.1 Begin/End 语句块

下一步,创建一个类CoroutineHelper存放协程的相关扩展,先在类中添加一个栈对象,保存每一步的栈跟踪信息:

  1. public static class CoroutineHelper
  2. {
  3. private static StackTrace[] sStackTraceStack;
  4. private static int sStackTraceStackNum;
  5. static CoroutineHelper()
  6. {
  7. sStackTraceStack = new StackTrace[64];
  8. sStackTraceStackNum = 0;
  9. }
  10. public static void BeginStackTraceStabDot() {
  11. sStackTraceStack[sStackTraceStackNum] = new StackTrace(true);
  12. ++sStackTraceStackNum;
  13. }
  14. public static void EndStackTraceStabDot() {
  15. sStackTraceStack[sStackTraceStackNum-1] = null;
  16. --sStackTraceStackNum;
  17. }
  18. }

注意这里没有直接用C#自己的Stack,是因为无法逆序遍历不方便输出栈日志,因此直接采用数组实现

 

若这样的话,每一步协程函数跳转都要用Begin、End语句包装又太丑。

  1. private void Start() {
  2. StartCoroutine(A());
  3. }
  4. private IEnumerator A() {
  5. CoroutineHelper.BeginStackTraceStabDot();
  6. yield return B();
  7. CoroutineHelper.EndStackTraceStabDot();
  8. }

2.2 使用扩展方法与using语法糖优化

实际上非yield语句,普通函数调用也是可以的,编译后不会被转换。因此可用扩展方法进行优化:

  1. public static class CoroutineHelper
  2. {
  3. //加入了这个函数:
  4. public static IEnumerator StackTrace(this IEnumerator enumerator)
  5. {
  6. BeginStackTraceStabDot();
  7. return enumerator;
  8. }
  9. }

这样调用时就舒服多了,对原始代码的改动也最小:

  1. private void Start() {
  2. StartCoroutine(A());
  3. }
  4. private IEnumerator A() {
  5. yield return B().StackTrace();
  6. }
  7. private IEnumerator B() {
  8. yield return C().StackTrace();
  9. }

不过还需要处理函数结束时调用Pop方法,这个可以结合using语法糖使用:

  1. //加入该结构体
  2. public struct CoroutineStabDotAutoDispose : IDisposable {
  3. public void Dispose() {
  4. CoroutineHelper.EndStackTraceStabDot();
  5. }
  6. }
  7. public static class CoroutineHelper
  8. {
  9. //加入该函数
  10. public static CoroutineStabDotAutoDispose StackTracePop() {
  11. return new CoroutineStabDotAutoDispose();
  12. }
  13. }

加入Pop处理后调用时如下:

  1. private void Start()
  2. {
  3. StartCoroutine(A());
  4. }
  5. private IEnumerator A()
  6. {
  7. using var _ = CoroutineHelper.StackTracePop();
  8. yield return B().StackTrace();
  9. //...
  10. }
  11. private IEnumerator B()
  12. {
  13. using var _ = CoroutineHelper.StackTracePop();
  14.    yield return C().StackTrace();
  15. //...
  16. }

2.3 不使用Using语法糖

后来我想到StackTrace可以拿到某一调用级的Method,可以通过比较之前记录的StackTrace查看有没有重复Method来确认

是否退出栈,因此可以优化掉Using语法糖的Pop操作。

修改函数如下:

  1. public static void StackTraceStabDot()
  2. {
  3. var currentTrack = new StackTrace(true);
  4. var currentTrackSf = currentTrack.GetFrame(2);
  5. for (int i = sStackTraceStackNum - 1; i >= 0; --i)
  6. {
  7. var sf = sStackTraceStack[i].GetFrame(2);
  8. if (sf.GetMethod().GetHashCode() == currentTrackSf.GetMethod().GetHashCode())
  9. {
  10. for (int j = i; j < sStackTraceStackNum; ++j)
  11. sStackTraceStack[j] = null;
  12. sStackTraceStackNum = i;
  13. break;
  14. }
  15. }
  16. sStackTraceStack[sStackTraceStackNum] = currentTrack;
  17. ++sStackTraceStackNum;
  18. }

 

这样也是最简洁的(没测试过复杂情形,可能存在Bug):

  1. private void Start() {
  2. StartCoroutine(A());
  3. }
  4. private IEnumerator A() {
  5. yield return B().StackTrace();
  6. }
  7. private IEnumerator B() {
  8. yield return C().StackTrace();
  9. }

3.打印输出

在拿到完整栈信息后,还需要打印输出,

我们可以加入Unity编辑器下IDE链接的语法,这样打印日志直接具有超链接效果:

  1. public static void PrintStackTrace()
  2. {
  3. var stb = new StringBuilder(4096);
  4. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  5. for (int i = 0; i < sStackTraceStackNum; ++i)
  6. {
  7. var sf = sStackTraceStack[i].GetFrame(2);
  8. stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
  9. }
  10. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  11. UnityEngine.Debug.Log(stb.ToString());
  12. }

 

最终效果如下:

 

 

4.源码

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

 

需要手动触发Pop函数,稳定版:

  1. using System;
  2. using System.Collections;
  3. using System.Diagnostics;
  4. using System.Text;
  5. public struct CoroutineStabDotAutoDispose : IDisposable
  6. {
  7. public void Dispose()
  8. {
  9. CoroutineHelper.EndStackTraceStabDot();
  10. }
  11. }
  12. public static class CoroutineHelper
  13. {
  14. private static StackTrace[] sStackTraceStack;
  15. private static int sStackTraceStackNum;
  16. static CoroutineHelper()
  17. {
  18. sStackTraceStack = new StackTrace[64];
  19. sStackTraceStackNum = 0;
  20. }
  21. public static CoroutineStabDotAutoDispose StackTracePop()
  22. {
  23. return new CoroutineStabDotAutoDispose();
  24. }
  25. public static IEnumerator StackTrace(this IEnumerator enumerator)
  26. {
  27. BeginStackTraceStabDot();
  28. return enumerator;
  29. }
  30. public static void BeginStackTraceStabDot()
  31. {
  32. sStackTraceStack[sStackTraceStackNum] = new StackTrace(true);
  33. ++sStackTraceStackNum;
  34. }
  35. public static void EndStackTraceStabDot()
  36. {
  37. sStackTraceStack[sStackTraceStackNum - 1] = null;
  38. --sStackTraceStackNum;
  39. }
  40. public static void PrintStackTrace()
  41. {
  42. var stb = new StringBuilder(4096);
  43. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  44. for (int i = 0; i < sStackTraceStackNum; ++i)
  45. {
  46. var sf = sStackTraceStack[i].GetFrame(2);
  47. stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
  48. }
  49. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  50. UnityEngine.Debug.Log(stb.ToString());
  51. }
  52. }
View Code

比较之前记录的StackTrace,无需手动触发Pop函数,可能有bug版:

  1. using System;
  2. using System.Collections;
  3. using System.Diagnostics;
  4. using System.Text;
  5. public static class CoroutineHelper
  6. {
  7. private static StackTrace[] sStackTraceStack;
  8. private static int sStackTraceStackNum;
  9. static CoroutineHelper()
  10. {
  11. sStackTraceStack = new StackTrace[64];
  12. sStackTraceStackNum = 0;
  13. }
  14. public static IEnumerator StackTrace(this IEnumerator enumerator)
  15. {
  16. StackTraceStabDot();
  17. return enumerator;
  18. }
  19. public static void StackTraceStabDot()
  20. {
  21. var currentTrack = new StackTrace(true);
  22. var currentTrackSf = currentTrack.GetFrame(2);
  23. for (int i = sStackTraceStackNum - 1; i >= 0; --i)
  24. {
  25. var sf = sStackTraceStack[i].GetFrame(2);
  26. if (sf.GetMethod().GetHashCode() == currentTrackSf.GetMethod().GetHashCode())
  27. {
  28. for (int j = i; j < sStackTraceStackNum; ++j)
  29. sStackTraceStack[j] = null;
  30. sStackTraceStackNum = i;
  31. break;
  32. }
  33. }
  34. sStackTraceStack[sStackTraceStackNum] = currentTrack;
  35. ++sStackTraceStackNum;
  36. }
  37. public static void PrintStackTrace()
  38. {
  39. var stb = new StringBuilder(4096);
  40. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  41. for (int i = 0; i < sStackTraceStackNum; ++i)
  42. {
  43. var sf = sStackTraceStack[i].GetFrame(2);
  44. stb.AppendFormat("- {0} (at <a href=\"{1}\" line=\"{2}\">{1}:{2}</a>)\n", sf.GetMethod().Name, sf.GetFileName(), sf.GetFileLineNumber());
  45. }
  46. stb.AppendLine(" --- Coroutine Helper StackTrace --- ");
  47. UnityEngine.Debug.Log(stb.ToString());
  48. }
  49. }
View Code

 

5.异常捕获+完整栈跟踪

知乎上找了一个协程异常捕获的扩展:

https://zhuanlan.zhihu.com/p/319551938

然后就可以实现协程异常捕获+完整栈跟踪:

  1. public class TestClass : MonoBehaviour {
  2. private void Start() {
  3. StartCoroutine(new CatchableEnumerator(A(), () => {
  4. CoroutineHelper.PrintStackTrace();
  5. }));
  6. }
  7. private IEnumerator A() {
  8. yield return B().StackTrace();
  9. }
  10. private IEnumerator B() {
  11. yield return C().StackTrace();
  12. }
  13. private IEnumerator C() {
  14. yield return null;throw new System.Exception();
  15. }
  16. }

 

原文链接:https://www.cnblogs.com/hont/p/18187817

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号