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


    现如今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/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/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();
        }
    }
    复制代码

    加入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 {1}:{2})\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 {1}:{2})\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 {1}:{2})\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();
        }
    }
    复制代码

     

  • 相关阅读:
    给视频批量添加背景图,轻松简单的操作方法
    导出excel换行问题,一个单元格多张图片问题,数组对象去重处理,计算属性传参
    安装jdk1.6
    Java 多线程编程
    cs224n-2022-assignment1
    Maya自定义工具架
    【Push Kit】推送返回80100016错误
    采集数据工具推荐,以及采集数据列表详细图解流程
    Vue3 + Nodejs 实战 ,文件上传项目--实现文件批量上传(显示实时上传进度)
    简单上手_Kotlin,让开发更简洁
  • 原文地址:https://www.cnblogs.com/hont/p/18187817