一般情况下,定时器使用匿名函数(callback)作为回调,一旦函数内部发生错误,很难定位到问题来源。
解决思路是:在调用 AddTimer 时,对当前调用栈做一次快照,保存为字符串,作为 Timer 对象的成员。当定时器触发时捕获到异常,直接将快照输出,调试起来就方便多了。这套辅助机制在 release 发行版中可以去掉。
/*
* Author: caoshanshan
* Email: me@dreamyouxi.com
code copy from https://git.oschina.net/dreamyouxi/XYGame;
*/
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using ExtBase;
public sealed class TimerQueue : Singleton<TimerQueue>
{
/// <summary>
/// 添加一个真实时间定时器,返回函数调用即可 打断定时任务
/// </summary>
/// <param name="time_delay"></param>
/// <param name="cb"></param>
/// <param name="repeat_times"> <0 will be run forever</param>
public VoidFuncVoid AddTimer(float time_delay, VoidFuncNN cb, int repeat_times = 1, params object[] objs)
{
return this.AddTimer(time_delay, () => { cb(objs); }, repeat_times);
}
/// <summary>
/// 添加一个真实时间定时器,返回函数调用即可 打断定时任务
/// </summary>
/// <param name="time_delay"></param>
/// <param name="cb"></param>
/// <param name="repeat_times"> <0 will be run forever</param>
public VoidFuncVoid AddTimer(float time_delay, VoidFuncVoid cb, int repeat_times = 1)
{
if (repeat_times == 0) return null;// () => { };
Timer time = new Timer();
time.delay = time_delay;
time.cb = cb;
time.repeat_times = repeat_times;
//记录定时器添加的时候的快照栈信息,以 提供错误时 更多调试信息
System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace(true);
string ss = "\n";
for (int i = 0; i < st.FrameCount; i++)
{
var f = st.GetFrame(i);
string name = f.GetFileName();
if (name.Length > Application.dataPath.Length)
{
name = name.Substring(Application.dataPath.Length, name.Length - Application.dataPath.Length);
}
ss += " " + f.GetMethod().ToString() + " at " + name + " (" + f.GetFileLineNumber() + ") \n";
}
time.stackInfo = ss;
time.Init();
list.Add(time);
return () =>
{
if (time != null && this.list.Contains(time))
time.SetInValid();
};
}
/// <summary>
/// unity engine tick
/// </summary>
public void Tick()
{
this.Tick(ref list);
}
private void Tick(ref List<TimerBase> list)
{
if (list.Count < 1)
return;
for (int i = 0; i < list.Count; i++)
{
if (list[i] == null)
continue;
TimerBase timer = list[i] as TimerBase;
timer.Tick();
}
for (int i = 0; i < list.Count; )
{
TimerBase b = list[i] as TimerBase;
if (b.IsInValid())
{
list.Remove(b);
}
else
{
++i;
}
}
}
public void Clear()
{
this.list_ms.Clear();
this.list.Clear();
}
List<TimerBase> list_ms = new List<TimerBase>();
List<TimerBase> list = new List<TimerBase>();
}
class TimerBase : GAObject
{
public string stackInfo = "";
public virtual void Tick()
{
}
public VoidFuncVoid cb = null;
public int repeat_times = 1;
protected int repeat_times_current = 0;
}
sealed class Timer : TimerBase
{
public override void Tick()
{
if (this.IsInValid()) return;
if (current < delay)
{
current += Time.deltaTime;
return;
}
if (cb != null)
{
try
{
cb();
}
catch (System.Exception e)
{
Debug.LogError(e.Message);
Debug.LogError("add timer stackTrace:" + this.stackInfo);
Debug.LogError("call stackTrace:" + e.StackTrace);
}
cb = null;
}
current = 0.0f;
if (++repeat_times_current >= repeat_times && repeat_times >= 0)
{
this.SetInValid();
}
}
public override bool Init()
{
return true;
}
float current = 0.0f;
public float delay = 0.0f;
}