扩展实现Unity协程的完整栈跟踪
现如今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协程的完整栈跟踪的更多相关文章
- Unity协程(Coroutine)原理深入剖析再续
Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...
- Unity 协程运行时的监控和优化
我是快乐的搬运工: http://gulu-dev.com/post/perf_assist/2016-12-20-unity-coroutine-optimizing#toc_0 --------- ...
- 聊一聊Unity协程背后的实现原理
Unity开发不可避免的要用到协程(Coroutine),协程同步代码做异步任务的特性使程序员摆脱了曾经异步操作加回调的编码方式,使代码逻辑更加连贯易读.然而在惊讶于协程的好用与神奇的同时,因为不清楚 ...
- Unity协程(Coroutine)管理类——TaskManager工具分享
博客分类: Unity3D插件学习,工具分享 源码分析 Unity协程(Coroutine)管理类——TaskManager工具分享 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处 ...
- Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...
- Unity协程(Coroutine)原理深入剖析(转载)
记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...
- Lua的函数调用和协程中,栈的变化情况
Lua的函数调用和协程中,栈的变化情况 1. lua_call / lua_pcall 对于这两个函数,对栈底是没有影响的--调用的时候,参数会被从栈中移除,当函数返 回的时候,其返回值会从函数处 ...
- unity协程coroutine浅析
转载请标明出处:http://www.cnblogs.com/zblade/ 一.序言 在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下 ...
- Unity协程Coroutine使用总结和一些坑
原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...
- 深入浅出!从语义角度分析隐藏在Unity协程背后的原理
Unity的协程使用起来比较方便,但是由于其封装和隐藏了太多细节,使其看起来比较神秘.比如协程是否是真正的异步执行?协程与线程到底是什么关系?本文将从语义角度来分析隐藏在协程背后的原理,并使用C++来 ...
随机推荐
- C++虚继承原理与类布局分析
C++虚继承原理与类布局分析 引言 在开始深入了解虚继承之前,我们先要明白C++引入虚继承的目的.C++有别于其他OOP语言最明显的特性就是类的多继承,而菱形继承结构则是多继承中最令人头疼的情况. 我 ...
- #状压dp#洛谷 3959 [NOIP2017 提高组] 宝藏
题目 选定一个起点 \(S\),找到一棵生成树,最小化 \[\sum_{i=1}^n dep_i\times dis_i \] \(n\leq 12\) 分析 设 \(dp[d][S]\) 表示当前树 ...
- #贪心,树#C 平衡的树
分析 处理出子树内剩余删减以及最大的剩余\(a\)和, 如果删了还是超过\(b\)输出无解 代码 #include <cstdio> #include <cctype> #de ...
- OHOS IDE和SDK的安装方法
参照OpenHarmony应用开发环境安装流程,下载安装OHOS的IDE,过程中需要全程联网. IDE,安装至D:\Tools\Huawei\DevEcoStudio. IDE安装成功之后,按照提示下 ...
- 如何获取华为运动健康服务授权码并调用Rest API访问数据?
华为运动健康服务(HUAWEI Health Kit)允许三方生态应用在获取用户授权后,通过REST API接口访问数据库,读取华为和生态伙伴开放的运动健康数据或写入数据到华为运动健康服务,为用户提供 ...
- C++调用Python-1:hello world
#include "Python.h" #include <iostream> using namespace std; int main(int argc, char ...
- HarmonyOS 3百机升级计划,来了!
HarmonyOS 3规模升级来了! 为大家奉上百余款机型升级计划! 你的手机什么时候可以升级? 赶快下滑查看!
- 动态规划(四)——区间dp
区间dp: 就是对于区间的一种动态规划,对于某个区间,它的合并方式可能有很多种,我们需要去枚举所有的方式,通常是去枚举区间的分割点,找到最优的方式(一般是找最少消耗). 通常都是先枚举区间长度,区间长 ...
- Java构建工具:Maven与Gradle的对比
在Java码农的世界里,构建工具一直是一个不可或缺的元素.一开始,世上是只有一个构建工具的那就是Make后来发展为GNU Make.但是由于需求的不断涌现,这个小圈子里又逐渐衍生出其他千奇百怪的构建工 ...
- Mybatis终极案例之注解开发
Mybatis终极案例之注解开发 一.注解开发实现CURD操作 1.环境搭建 参考入门案例,环境搭建主要分为如下几步: 1.配置pom.xml <packaging>jar</pac ...