现如今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方案。
不过那样就太重了,经过摸索后发现,还是存在一些方法,可以简单的实现完整栈信息的获取。
使用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
下面将基于这个思路,继续封装协程代码。
下一步,我们可以创建一个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(); }
实际上非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;//... }
通过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()); }
最终效果如下:
我们希望将这些rpc结果数据缓存起来,并在一定时间后自动删除,以实现在一定时间后获取到最新数据。类似Redis的过期时间。本文是我的调研步骤和开发过程。