现如今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();
}
}
加入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 <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.源码
最后提供下这部分功能源码。
需要手动触发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 <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
比较之前记录的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 <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
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();
}
}