Какая хорошая стратегия обработки исключений для Unity3D?

Я изучаю некоторые скриптовые материалы Unity3D, и я бы хотел настроить глобальную систему обработки исключений. Это не для запуска в версии релиза игры, целью является исключение из пользовательских сценариев, а также в сценарии редактора и их перенаправление в базу данных для анализа (а также для отправки электронной почты соответствующим разработчикам, чтобы они могли исправить их shizzle).

В приложении с ванилью С# у меня будет try-catch вокруг метода Main. В WPF я зацепил один или несколько необработанных событий исключения. В единстве...?

Пока лучшее, что мне удалось найти, это что-то вроде этого:

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(). Я не реализовал параллельную версию для редакторов, но я предполагаю, что мне придется делать то же самое.

Мне не нравится эта стратегия, потому что мне нужно будет передать ее в любой скрипт, который мы выберем из сообщества (и мне нужно будет обеспечить его исполнение в команде). Сценарии редактора не имеют ни одной точки входа, сравнимой с MonoBehavior, поэтому я предполагаю, что мне придется внедрять безопасные версии мастеров, редакторов и т.д.

Я видел предложения об улавливании сообщений журнала (в отличие от исключений) с помощью Application.RegisterLogCallback, но это делает меня неудобным, потому что я 'd нужно проанализировать строку журнала отладки, а не иметь доступ к фактическим исключениям и stacktraces.

Итак... что правильно делать?

Ответ 1

Существует рабочая реализация 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);
        }
    }

У меня также есть обработчик ошибок по электронной почте через эту процедуру, поэтому я всегда знаю, когда мое приложение вылетает и получает как можно больше деталей.

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

Ответ 2

Вы упомянули Application.RegisterLogCallback, пытались ли вы его реализовать? Поскольку обратный вызов отсылает обратно трассировку стека, ошибку и тип ошибки (предупреждение, ошибка и т.д.).

Стратегия, описанная выше, будет сложной для реализации, поскольку MonoBehaviours не просто единственная точка входа. Вам придется обрабатывать OnTriggerEvent, OnCollisionEvent, OnGUI и так далее. Каждый из них завершает свою логику в обработчике исключений.

IMHO, обработка исключений - плохая идея здесь. Если вы не сразу перебросите исключение, вы, в конечном итоге, будете распространять эти ошибки по странным путям. Возможно, Foo полагается на Bar и Bar on Baz. Скажем, Baz выдает исключение, которое поймано и зарегистрировано. Затем Bar выдает исключение, потому что значение, которое ему требуется от Baz, неверно. Наконец, Foo выдает исключение, потому что значение, которое оно получало из Bar, недействительно.

Ответ 3

Прикрепите этот script к пустующему игровому объекту в вашей сцене:

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
        }
    }
}

убедитесь, что есть один экземпляр.

Ответ 4

Единственные разработчики просто не предоставляют нам таких инструментов. Они ломают исключения внутри в рамках здесь и там и записывают их как строки, предоставляя нам Application.logMessageReceived [Threaded]. Итак, если вам нужны исключения, которые могут произойти или будут зарегистрированы с вашей собственной обработкой (не единственными), я могу подумать:

  • не использовать фреймворк, но использовать свои собственные, поэтому исключение не попадает в рамки

  • создайте собственный класс, реализующий UnityEngine.ILogHandler:

    открытый интерфейс ILogHandler {   void LogFormat (LogType logType, контекст объекта, строковый формат, params object [] args);   void LogException (исключение исключения, контекст объекта); }

И используйте его, как сказано в официальных документах, чтобы регистрировать свои исключения. Но таким образом вы не получаете необработанных исключений и исключений, зарегистрированных из плагинов (да, кто-то делает исключения журналов в фреймворках, а не бросает их)

  1. Или вы можете сделать предложение/запрос к единству, чтобы сделать Debug.unityLogger(Debug.logger устарел в Unity 2017) имеют сеттер или другой механизм, чтобы мы могли передавать свои собственные.

  2. Просто установите его с отражением. Но это временный взлом и не будет работать, когда код изменения единицы.

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

Примечание. Чтобы получить правильные стеки, вам нужно установить StackTraceLogType в настройках/коде редактора в ScriptOnly (чаще всего это то, что вам нужно, Я написал статью о том, как она работает) И, при создании для iOS, говорится, что Script оптимизация вызовов должна быть медленной и безопасной

Если вам интересно, вы можете прочитать, как работает популярный инструмент анализа сбоев. Если вы посмотрите на crashlytics (инструмент отчета об авариях для android/ios), вы обнаружите, что он внутренне использует события Application.logMessageReceived и AppDomain.CurrentDomain.UnhandledException для регистрации управляемых исключений С#.

Если вы заинтересованы в примерах об единстве, в которых есть исключения, вы можете посмотреть ExecuteEvents.Update И еще одна статья от меня с проверкой ее исключения в прослушивателе кнопок можно найти здесь.

Некоторое резюме по официальным способам регистрации необработанного исключения:

я. Application.logMessageReceived запускается, когда исключение происходит в основном потоке. Есть способы для этого:

  • исключение попало в код С# и зарегистрировано через Debug.LogException
  • исключение попало в собственный код (возможно, код С++ при использовании il2cpp). В этом случае собственный код вызывает Application.CallLogCallback, что приводит к запуску Application.logMessageReceived

Примечание: строка stacktrace будет содержать "rethrow", когда исходное исключение содержит внутренние исключения

II. Application.logMessageReceivedThreaded запускается, когда исключение происходит в любом потоке, включая main (он указан в docs). Примечание: он должен быть потокобезопасным

III. Например, приложение AppDomain.CurrentDomain.UnhandledException запускается, когда:

Вы вызываете следующий код в редакторе:

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

Но это приводит к сбою в выпуске сборки Android 4.4.2 при использовании Unity 5.5.1f1

Примечание. При регистрации исключений и утверждений я воспроизвел некоторые ошибки с единицей, отсутствующими стековыми кадрами. Я отправил один из них.

Ответ 5

Вы можете использовать плагин под названием Reporter для получения электронной почты отладочных журналов, трассировки стека и захвата экрана в момент необработанной ошибки. Трассировки экрана и трассировки стека обычно достаточно, чтобы выяснить причину ошибки. Для упрямых проницательных ошибок вы должны записать больше подозрительных данных, построить и снова ждать ошибки. Надеюсь, это поможет.