Unity3D的全局exception处理策略是什么?

我正在研究做一些Unity3D脚本的东西,我想建立全局exception处理系统。 这不适用于在游戏的发布版本中运行,目的是捕获用户脚本和编辑器脚本中的exception,并确保将它们转发到数据库进行分析(以及向相关开发人员发送电子邮件,以便他们可以修复他们的shizzle)。

在一个vanilla C#应用程序中,我有一个围绕Main方法的try-catch。 在WPF中,我将挂钩一个或多个未处理的exception事件 。 在Unity ……?

到目前为止,我能够想出的最好的是这样的:

using UnityEngine; using System.Collections; public abstract class BehaviourBase : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { try { performUpdate(); print("hello"); } catch (System.Exception e) { print(e.ToString()); } } public abstract void performUpdate(); } 

在其他脚本中,我派生出BehaviourBase而不是MonoBehavior,并实现performUpdate()而不是Update()。 我没有为Editor clases实现并行版本,但我认为我必须在那里做同样的事情。

但是,我不喜欢这种策略,因为我必须将它反向移植到我们从社区中获取的任何脚本(我将不得不在团队中执行它)。 编辑器脚本也没有与MonoBehavior相当的单一入口点,因此我假设我必须实现向导,编辑器等的exception安全版本。

我已经看到了使用Application.RegisterLogCallback捕获日志消息(而不是exception)的建议,但这让我感到不舒服,因为我需要解析调试日志字符串而不是访问实际的exception和堆栈跟踪。

那么…做对的是什么?

我在这里找到了RegisterLogCallback的工作实现: http : //answers.unity3d.com/questions/47659/callback-for-unhandled-exceptions.html

在我自己的实现中,我使用它来调用我自己的MessageBox.Show而不是写入日志文件。 我只是从我的每个场景调用SetupExceptionHandling。

  static bool isExceptionHandlingSetup; public static void SetupExceptionHandling() { if (!isExceptionHandlingSetup) { isExceptionHandlingSetup = true; Application.RegisterLogCallback(HandleException); } } static void HandleException(string condition, string stackTrace, LogType type) { if (type == LogType.Exception) { MessageBox.Show(condition + "\n" + stackTrace); } } 

我现在也有error handling程序通过此例程给我发电子邮件,所以我总是知道我的应用程序何时崩溃并获得尽可能详细的信息。

  internal static void ReportCrash(string message, string stack) { //Debug.Log("Report Crash"); var errorMessage = new StringBuilder(); errorMessage.AppendLine("FreeCell Quest " + Application.platform); errorMessage.AppendLine(); errorMessage.AppendLine(message); errorMessage.AppendLine(stack); //if (exception.InnerException != null) { // errorMessage.Append("\n\n ***INNER EXCEPTION*** \n"); // errorMessage.Append(exception.InnerException.ToString()); //} errorMessage.AppendFormat ( "{0} {1} {2} {3}\n{4}, {5}, {6}, {7}x {8}\n{9}x{10} {11}dpi FullScreen {12}, {13}, {14} vmem: {15} Fill: {16} Max Texture: {17}\n\nScene {18}, Unity Version {19}, Ads Disabled {18}", SystemInfo.deviceModel, SystemInfo.deviceName, SystemInfo.deviceType, SystemInfo.deviceUniqueIdentifier, SystemInfo.operatingSystem, Localization.language, SystemInfo.systemMemorySize, SystemInfo.processorCount, SystemInfo.processorType, Screen.currentResolution.width, Screen.currentResolution.height, Screen.dpi, Screen.fullScreen, SystemInfo.graphicsDeviceName, SystemInfo.graphicsDeviceVendor, SystemInfo.graphicsMemorySize, SystemInfo.graphicsPixelFillrate, SystemInfo.maxTextureSize, Application.loadedLevelName, Application.unityVersion, GameSettings.AdsDisabled ); //if (Main.Player != null) { // errorMessage.Append("\n\n ***PLAYER*** \n"); // errorMessage.Append(XamlServices.Save(Main.Player)); //} try { using (var client = new WebClient()) { var arguments = new NameValueCollection(); //if (loginResult != null) // arguments.Add("SessionId", loginResult.SessionId.ToString()); arguments.Add("report", errorMessage.ToString()); var result = Encoding.ASCII.GetString(client.UploadValues(serviceAddress + "/ReportCrash", arguments)); //Debug.Log(result); } } catch (WebException e) { Debug.Log("Report Crash: " + e.ToString()); } } 

将此脚本附加到场景中的空游戏对象:

 using UnityEngine; public class ExceptionManager : MonoBehaviour { void Awake() { Application.logMessageReceived += HandleException; DontDestroyOnLoad(gameObject); } void HandleException(string condition, string stackTrace, LogType type) { if (type == LogType.Exception) { //handle here } } } 

确保有一个实例。

您提到了Application.RegisterLogCallback,您是否尝试过实现它? 因为日志记录回调会传回堆栈跟踪,错误和错误类型(警告,错误等)。

您在上面概述的策略很难实现,因为MonoBehaviours不仅仅有一个入口点 。 您必须处理OnTriggerEvent,OnCollisionEvent,OnGUI等。 每个都将其逻辑包装在exception处理程序中。

恕我直言,exception处理在这里是一个坏主意。 如果你没有立即重新抛出exception,你最终会以奇怪的方式传播这些错误。 也许Foo依赖Bar和Bar on Baz。 Say Baz会抛出一个捕获并记录的exception。 然后Bar抛出exception,因为Baz需要的值不正确。 最后,Foo抛出exception,因为它从Bar获得的值无效。

Unity开发人员只是没有为我们提供这样的工具。 它们在框架内部和内部捕获exception,并将它们记录为字符串,为我们提供Application.logMessageReceived [Threaded]。 因此,如果您需要发生exception或使用您自己的处理(而不是统一)进行记录,我可以想到:

  1. 不要使用框架机制,但使用自己的框架,不会被框架捕获

  2. 让你自己的类实现UnityEngine.ILogHandler:

    public interface ILogHandler {void LogFormat(LogType logType,Object context,string format,params object [] args); void LogException(exceptionexception,Object context); }

并按正式文档中的说法使用它来记录您的例外情况。 但是这样你就不会收到从插件中记录的未处理的exception和exception(是的,有人会在框架中记录exception而不是抛出它们)

  1. 或者您可以向Unity提出建议/请求以使Debug.unityLogger(在Unity 2017中不推荐使用Debug.logger)具有setter或其他机制,以便我们可以通过我们自己的。

  2. 只需用reflection设置即可。 但它是暂时的黑客攻击,并且在统一改变代码时不会起作用。

    var field = typeof(UnityEngine.Debug).GetField(“s_Logger”,BindingFlags.Static | BindingFlags.NonPublic); field.SetValue(null,your_debug_logger);

注意:要获得正确的堆栈跟踪,您需要将编辑器设置/代码中的StackTraceLogType设置为ScriptOnly(大多数时候它都是您需要的,我写了一篇关于它是如何工作的文章 )并且,当为iOS构建时,据说脚本调用优化必须设置得慢而且安全

如果有兴趣,您可以阅读流行的崩溃分析工具的工作原理。 如果你研究一下crashlytics(android / ios的崩溃报告工具),你会发现它在内部使用Application.logMessageReceived和AppDomain.CurrentDomain.UnhandledException事件来记录托管的C#exception。

如果对关于统一框架捕获exception的示例感兴趣,您可以查看ExecuteEvents.Update 。我可以在此处找到另一篇文章来测试它在按钮单击侦听器中捕获exception。

关于记录未处理exception的官方方法的一些摘要:

I.在主线程上发生exception时会触发Application.logMessageReceived。 有很多方法可以实现:

  1. exception捕获在c#代码中并通过Debug.LogException记录
  2. 本机代码中捕获的exception(使用il2cpp时可能是c ++代码)。 在这种情况下,本机代码调用Application.CallLogCallback,这会导致触发Application.logMessageReceived

注意:当原始exception具有内部exception时,stacktrace字符串将包含“rethrow”

II。 在任何线程上发生exception时都会触发Application.logMessageReceivedThreaded,包括main(在文档中说)注意:它必须是线程安全的

III。 例如,在以下情况下触发AppDomain.CurrentDomain.UnhandledException:

您在编辑器中调用以下代码:

  new Thread(() => { Thread.Sleep(10000); object o = null; o.ToString(); }).Start(); 

但是当使用Unity 5.5.1f1时,它会导致android 4.4.2发布版本崩溃

注意:在记录exception和断言时,我复制了一些缺少stackframes的错误。 我提交了其中一个 。

您可以使用名为Reporter的插件在未处理的错误时刻收到调试日志,堆栈跟踪和屏幕捕获的电子邮件。 屏幕捕获和堆栈跟踪通常足以找出错误的原因。 对于顽固的偷偷摸摸的错误,你应该记录更多的可疑数据,构建并再次等待错误。我希望这会有所帮助。