我们知道,Unity的异常处理做得非常好,源于他在框架底层会自动捕获异常,所以一般的异常(比如空引用、除0操作之类)均不会导致整个进程crash掉,原因很简单,代码在try段中发生了异常,在catch段处理以后,表现在Unity编辑器中便是在日志窗口打印红色错误日志,而在已经发布的项目中,由于异常依然被Unity捕获,所以进程并不会crash掉,但对于我们而言他是未知的,这个异常很明显会导致程序在功能表现上的不正确。
那么我们需要的便是在发生任何未知异常的时候,获取异常的相关信息,用户的反馈信息,同时希望结束掉整个程序,毕竟发生了未知的异常即便他无关紧要,但总会导致某些功能的不正常表现。
好在Unity对于这方面的封装非常完善,我们完全可以借助他自身的异常处理机制来完善我们的需求。
如下:
public static void RegisterLogCallback( Application.LogCallback handler );
RegisterLogCallback是Application中的静态方法,旨在将LogCallback类型的函数注册给Unity的日志回调委托,在Unity输出任何日志的时候都将会调用到方法handler。
在handler中我们就可以截获任何的日志类型(这里的日志包括Debug.Log输出的日志,以及Unity自身在捕获到异常时会打印的异常日志),判断其为我们未知的异常,同时就可以做出我们自己的处理,包括在这里结束掉整个程序并弹出一个友好的bug反馈窗口。
委托LogCallback的形式为:
public delegate void LogCallback( string condition, string stackTrace, LogType type );
其中condition为日志内容,stackTrace为相关的堆栈调用信息,type为日志的类型,日志类型分为以下五种:
public enum LogType
{
Error = 0,
Assert = 1,
Warning = 2,
Log = 3,
Exception = 4,
}
1、Error为错误日志,Debug.LogError输出的日志便是此类型;
2、Assert为Unity本身的异常,这种异常通常都是致命的,会导致整个进程crash;
3、Warning为警告日志,Debug.LogWarning输出的日志便是此类型;
4、Log为普通日志,Debug.Log输出的日志便是此类型;
5、Exception为被Unity捕获的未知异常,也就是我们自己的代码产生的异常,这将是我们处理的重点对象。
如此,我们将代码完善一下:
using UnityEngine;
using System;
using System.IO;
using System.Diagnostics;
using System.Collections;
public class ExceptionHandler : MonoBehaviour
{
//是否作为异常处理者
public bool IsHandler = false;
//是否退出程序当异常发生时
public bool IsQuitWhenException = true;
//异常日志保存路径(文件夹)
private string LogPath;
//Bug反馈程序的启动路径
private string BugExePath;
void Awake()
{
LogPath = Application.dataPath.Substring( 0, Application.dataPath.LastIndexOf( "/" ) );
BugExePath = Application.dataPath.Substring( 0, Application.dataPath.LastIndexOf( "/" ) ) + "\\Bug.exe";
//注册异常处理委托
if( IsHandler )
{
Application.RegisterLogCallback( Handler );
}
}
void OnDestory()
{
//清除注册
Application.RegisterLogCallback( null );
}
void Handler( string logString, string stackTrace, LogType type )
{
if( type == LogType.Error || type == LogType.Exception || type == LogType.Assert )
{
string logPath = LogPath + "\\" + DateTime.Now.ToString( "yyyy_MM_dd HH_mm_ss" ) + ".log";
//打印日志
if( Directory.Exists( LogPath ) )
{
File.AppendAllText( logPath, "[time]:" + DateTime.Now.ToString() + "\r\n" );
File.AppendAllText( logPath, "[type]:" + type.ToString() + "\r\n" );
File.AppendAllText( logPath, "[exception message]:" + logString + "\r\n" );
File.AppendAllText( logPath, "[stack trace]:" + stackTrace + "\r\n" );
}
//启动bug反馈程序
if( File.Exists( BugExePath ) )
{
ProcessStartInfo pros = new ProcessStartInfo();
pros.FileName = BugExePath;
pros.Arguments = "\"" + logPath + "\"";
Process pro = new Process();
pro.StartInfo = pros;
pro.Start();
}
//退出程序,bug反馈程序重启主程序
if( IsQuitWhenException )
{
Application.Quit();
}
}
}
}
我们的处理机制是当接收到LogType.Error、LogType.Exception、LogType.Assert 类型的日志输出请求时,就将日志信息打印到本地文件,同时主程序退出,启动bug反馈程序,我这里的bug反馈程序是一个winform窗口程序,仿照了类似QQ的报错界面:
Bug反馈程序会接收一个参数:日志文件的路径,以便于能够在反馈界面将用户输入的引起异常的原因添加到错误日志中,用户选择发送错误报告则会将该错误日志上传到我们的服务器,也就是说如果不提供参数,直接运行bug反馈程序是不会成功的。
我们在Unity中新建一个测试脚本Test.cs,输入以下内容:
using UnityEngine;
using System.Collections;
public class Test : MonoBehaviour
{
public GameObject TestObj;
void OnGUI()
{
if( GUILayout.Button( "点我就会抛出一个异常" ) )
{
TestObj.transform.position = Vector3.one;
}
}
}
这里的TestObj我们不赋值,也就是说OnGUI里面的调用将会报空引用错。
我们将项目发布,同时将bug反馈程序拷入到exe的同级目录:
我们运行Test.exe,点击屏幕左上角的按钮:
这时引发异常,主程序会直接退出,bug反馈程序启动,我们可以查看错误的日志信息:
我们可以看到一个并未被我们手动用try catch处理的异常被捕获了,这样的机制很明显省去了我们苦心积虑的研究哪个地方该用try catch的苦恼,因为所有的异常都不会被测试遗落同时会展现出非常详细的错误日志,以便于我们修改,当然用户的反馈也是解决这些bug的重大保障。
不过针对平台的不同,比如移动端可能并不能提供一个外部bug反馈程序,但只要能够捕获到这些未知的异常,接下来怎么做都可以,比如可以在程序里直接弹出一个窗口提示报错啦,然后用户填写相关错误反馈,点击确定之后后台提交反馈,同时程序自动重启,如果认为界面或者内容上的一些由异常引发的功能表现不正确无关紧要的话,这里也不用重启,毕竟只要不是Assert类型的异常,Unity都不会crash。