现如今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 voidBeginStackTraceStabDot() {
sStackTraceStack[sStackTraceStackNum] = new StackTrace(true);
++sStackTraceStackNum;
}
public static voidEndStackTraceStabDot() {
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