现如今Unity中的协程(Coroutine)方案已显得老旧,Unitask等异步方案可以做到异常捕获等yield关键字处理起来很麻烦的问题,

并且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方案。

不过那样就太重了,经过摸索后发现,还是存在一些方法,可以简单的实现完整栈信息的获取。

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();
}
}

最终调用时如下:

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());
}

最终效果如下:

扩展实现Unity协程的完整栈跟踪的更多相关文章

  1. Unity协程(Coroutine)原理深入剖析再续

    Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...

  2. Unity 协程运行时的监控和优化

    我是快乐的搬运工: http://gulu-dev.com/post/perf_assist/2016-12-20-unity-coroutine-optimizing#toc_0 --------- ...

  3. 聊一聊Unity协程背后的实现原理

    Unity开发不可避免的要用到协程(Coroutine),协程同步代码做异步任务的特性使程序员摆脱了曾经异步操作加回调的编码方式,使代码逻辑更加连贯易读.然而在惊讶于协程的好用与神奇的同时,因为不清楚 ...

  4. Unity协程(Coroutine)管理类——TaskManager工具分享

    博客分类: Unity3D插件学习,工具分享 源码分析   Unity协程(Coroutine)管理类——TaskManager工具分享 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处 ...

  5. Unity协程(Coroutine)原理深入剖析

    Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...

  6. Unity协程(Coroutine)原理深入剖析(转载)

    记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...

  7. Lua的函数调用和协程中,栈的变化情况

    Lua的函数调用和协程中,栈的变化情况 1. lua_call / lua_pcall   对于这两个函数,对栈底是没有影响的--调用的时候,参数会被从栈中移除,当函数返 回的时候,其返回值会从函数处 ...

  8. unity协程coroutine浅析

    转载请标明出处:http://www.cnblogs.com/zblade/ 一.序言 在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下 ...

  9. Unity协程Coroutine使用总结和一些坑

    原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...

  10. 深入浅出!从语义角度分析隐藏在Unity协程背后的原理

    Unity的协程使用起来比较方便,但是由于其封装和隐藏了太多细节,使其看起来比较神秘.比如协程是否是真正的异步执行?协程与线程到底是什么关系?本文将从语义角度来分析隐藏在协程背后的原理,并使用C++来 ...

随机推荐

  1. Ubuntu一键安装/卸载docker和docker compose,可指定版本或安装最新版本。

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 @ 目录 前言 一.docker是什么? 二.docker compose是什么? 三.安装步骤 1.Ubuntu安装脚本 2.生成脚 ...

  2. 13 CSS 的position属性

    13 CSS 的position属性 就像photoshop中的图层功能会把一整张图片分层一个个图层一样,网页布局中的每一个元素也可以看成是一个个类似图层的层模型.层布局模型就是把网页中的每一个元素看 ...

  3. [IOI2000]邮局 题解

    简要题意 线段上有 \(V\) 个村庄,现在要建 \(P\) 个邮局,使每个村庄到最近的邮局的距离之和最小. 50分做法 设\(dp[i][j]\) 表示第一个村庄到第 \(i\) 个村庄,建了 \( ...

  4. mogdb里xlog相关的几个参数

    openGauss/MogDB 3.0 闪回恢复测试 本文出处:https://www.modb.pro/db/411368 介绍 闪回恢复功能是数据库恢复技术的一环,可以有选择性的撤销一个已提交事务 ...

  5. 【Java面试指北】单例模式

    单线程下的单例模式: public class Singleton { private static Singleton instance; private Singleton() {} public ...

  6. Prometheus之自定义标签

    前言: 我们一般通过grafana导入Dashboard模板用来展示数据,但是有时候需要自己定义展示项目,这时需要自己在Prometheus重新自定义标签,并在grafana进行应用. 1.Prome ...

  7. 【转】CentOS安装VMware Tools

    [转]CentOS安装VMware Tools VMware 是非常好的虚拟机软件.如果系统安装了VMware Tools以后对虚拟机的性能会提升很多的.下面是如果在CentOS系统内安装VMware ...

  8. seo FAQ 科普

    前言 seo 常规问题整理,后续不断补充,只做科普. 正文 1.什么是seo: seo 也就是搜索排名. 2.百度,360等知名品牌如何排名: 抓取,过滤,建立索引,输出结果 3.是否网站越好看排名就 ...

  9. leetcode:1380. 矩阵中的幸运数

    1380. 矩阵中的幸运数 给你一个 m * n 的矩阵,矩阵中的数字 各不相同 .请你按 任意 顺序返回矩阵中的所有幸运数. 幸运数是指矩阵中满足同时下列两个条件的元素: 在同一行的所有元素中最小 ...

  10. 鸿蒙HarmonyOS实战-ArkUI组件(Shape)

    一.Shape Shape组件是用于创建2D形状和粒子效果的组件.它可以创建包括圆形.正方形.三角形和多边形等基本形状,同时还可以自定义形状.Shape组件创建各种不同的效果,例如火花.烟雾.雨滴等. ...