Unity日志工具——封装,跳转
在软件开发中,日志工具是不可或缺的一部分,几乎所有的开发团队都会对日志工具进行封装,Unity 开发也不例外。之前团队同事封装的日志工具存在一个问题:在 Unity 的 ConsoleWindow 中双击日志时,代码跳转总是指向封装类中的函数,而无法直接跳转到调用封装类的实际代码位置。
正好在筹备新项目,我对原有的日志工具进行了改进,力求达到尽可能完美的效果。此前我了解到以下几点知识:
- 利用反射可以获取 Unity 的私有
FieldInfo和MethodInfo,从而实现很多特殊功能。 - Unity 提供了相关 API 用于跳转到指定的代码位置。
- Unity 具备跳转回调机制。
基于这些理论知识,今天到公司后我就着手实现了这个优化,同时对 LogLevel 和 StackFrame 信息也进行了优化(之前的实现是 2013 年一位前同事编写的,效果不太理想)。
实现思路(Unity 5.3)
- 记录调用栈信息:记录通过封装日志工具的函数调用栈信息
StackFrame。 - 添加回调方法:添加
UnityEditor.Callbacks.OnOpenAssetAttribute(0)的回调方法,用于处理从ConsoleWindow双击日志时的跳转操作。 - 获取日志行数信息:利用反射获取
ConsoleWindow的ListViewState的row(当前双击的行)和总行数。 - 匹配
StackFrame:根据获取的行数,通过反射获取LogEntry信息,并进行匹配,以获得对应的StackFrame。 - 打开指定代码:调用
AssetDatabase.OpenAsset()打开指定的代码文件。
在更新到 Unity 5.3 后,发现 Unity 提供了 Logger 类。原本以为可以利用这个类实现所需功能,但经过简单测试,发现并不可行。目前我还不清楚 Unity 构造 Logger 类的具体用途,因此将下面的类名改为 LoggerUtility。
代码实现
/*
* File: Assets/Scripts/Game/Utility/LoggerUtility.cs
* Project: ****
* Company: Lucky
* Code Porter: D.S.Qiu
* Create Date: 10/9/2015 10:11:53 PM
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
#if UNITY_EDITOR
using System.Reflection;
using UnityEditor;
using UnityEditor.Callbacks;
#endif
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace Utility
{
public class LogUtility
{
public enum LogLevel : byte
{
None = 0,
Exception = 1,
Error = 2,
Warning = 3,
Info = 4,
}
public static LogLevel logLevel = LogLevel.Info;
public static string infoColor = "#909090";
public static string warningColor = "orange";
public static string errorColor = "red";
public static void LogBreak(object message, UnityEngine.Object sender = null)
{
LogInfo(message, sender);
Debug.Break();
}
public static void LogFormat(string format, UnityEngine.Object sender, params object[] message)
{
if (logLevel >= LogLevel.Info)
LogLevelFormat(LogLevel.Info, string.Format(format, message), sender);
}
public static void LogFormat(string format, params object[] message)
{
if (logLevel >= LogLevel.Info)
LogLevelFormat(LogLevel.Info, string.Format(format, message), null);
}
public static void LogInfo(object message, UnityEngine.Object sender = null)
{
if (logLevel >= LogLevel.Info)
LogLevelFormat(LogLevel.Info, message, sender);
}
public static void LogWarning(object message, UnityEngine.Object sender = null)
{
if (logLevel >= LogLevel.Warning)
LogLevelFormat(LogLevel.Warning, message, sender);
}
public static void LogError(object message, UnityEngine.Object sender = null)
{
if (logLevel >= LogLevel.Error)
{
LogLevelFormat(LogLevel.Error, message, sender);
}
}
public static void LogException(Exception exption, UnityEngine.Object sender = null)
{
if (logLevel >= LogLevel.Exception)
{
LogLevelFormat(LogLevel.Exception, exption, sender);
}
}
private static void LogLevelFormat(LogLevel level, object message, UnityEngine.Object sender)
{
string levelFormat = level.ToString().ToUpper();
StackTrace stackTrace = new StackTrace(true);
var stackFrame = stackTrace.GetFrame(2);
#if UNITY_EDITOR
s_LogStackFrameList.Add(stackFrame);
#endif
string stackMessageFormat = Path.GetFileName(stackFrame.GetFileName()) + ":" + stackFrame.GetMethod().Name + "():at line " + stackFrame.GetFileLineNumber();
string timeFormat = "Frame:" + Time.frameCount + "," + DateTime.Now.Millisecond + "ms";
string objectName = string.Empty;
string colorFormat = infoColor;
if (level == LogLevel.Warning)
colorFormat = warningColor;
else if (level == LogLevel.Error)
colorFormat = errorColor;
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<color={3}>[{0}][{4}][{1}]{2}</color>", levelFormat, timeFormat, message, colorFormat, stackMessageFormat);
Debug.Log(sb, sender);
}
#if UNITY_EDITOR
private static int s_InstanceID;
private static int s_Line = 104;
private static List<StackFrame> s_LogStackFrameList = new List<StackFrame>();
// ConsoleWindow
private static object s_ConsoleWindow;
private static object s_LogListView;
private static FieldInfo s_LogListViewTotalRows;
private static FieldInfo s_LogListViewCurrentRow;
// LogEntry
private static MethodInfo s_LogEntriesGetEntry;
private static object s_LogEntry;
// instanceId 非 UnityEngine.Object 的运行时 InstanceID 为零所以只能用 LogEntry.Condition 判断
private static FieldInfo s_LogEntryInstanceId;
private static FieldInfo s_LogEntryLine;
private static FieldInfo s_LogEntryCondition;
static LogUtility()
{
s_InstanceID = AssetDatabase.LoadAssetAtPath<MonoScript>("Assets/Scripts/Game/Utility/LoggerUtility.cs").GetInstanceID();
s_LogStackFrameList.Clear();
GetConsoleWindowListView();
}
private static void GetConsoleWindowListView()
{
if (s_LogListView == null)
{
Assembly unityEditorAssembly = Assembly.GetAssembly(typeof(EditorWindow));
Type consoleWindowType = unityEditorAssembly.GetType("UnityEditor.ConsoleWindow");
FieldInfo fieldInfo = consoleWindowType.GetField("ms_ConsoleWindow", BindingFlags.Static | BindingFlags.NonPublic);
s_ConsoleWindow = fieldInfo.GetValue(null);
FieldInfo listViewFieldInfo = consoleWindowType.GetField("m_ListView", BindingFlags.Instance | BindingFlags.NonPublic);
s_LogListView = listViewFieldInfo.GetValue(s_ConsoleWindow);
s_LogListViewTotalRows = listViewFieldInfo.FieldType.GetField("totalRows", BindingFlags.Instance | BindingFlags.Public);
s_LogListViewCurrentRow = listViewFieldInfo.FieldType.GetField("row", BindingFlags.Instance | BindingFlags.Public);
// LogEntries
Type logEntriesType = unityEditorAssembly.GetType("UnityEditorInternal.LogEntries");
s_LogEntriesGetEntry = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public);
Type logEntryType = unityEditorAssembly.GetType("UnityEditorInternal.LogEntry");
s_LogEntry = Activator.CreateInstance(logEntryType);
s_LogEntryInstanceId = logEntryType.GetField("instanceID", BindingFlags.Instance | BindingFlags.Public);
s_LogEntryLine = logEntryType.GetField("line", BindingFlags.Instance | BindingFlags.Public);
s_LogEntryCondition = logEntryType.GetField("condition", BindingFlags.Instance | BindingFlags.Public);
}
}
private static StackFrame GetListViewRowCount()
{
GetConsoleWindowListView();
if (s_LogListView == null)
return null;
else
{
int totalRows = (int)s_LogListViewTotalRows.GetValue(s_LogListView);
int row = (int)s_LogListViewCurrentRow.GetValue(s_LogListView);
int logByThisClassCount = 0;
for (int i = totalRows - 1; i >= row; i--)
{
s_LogEntriesGetEntry.Invoke(null, new object[] { i, s_LogEntry });
string condition = s_LogEntryCondition.GetValue(s_LogEntry) as string;
// 判断是否是由 LoggerUtility 打印的日志
if (condition.Contains("][") && condition.Contains("Frame"))
logByThisClassCount++;
}
// 同步日志列表,ConsoleWindow 点击 Clear 会清理
while (s_LogStackFrameList.Count > totalRows)
s_LogStackFrameList.RemoveAt(0);
if (s_LogStackFrameList.Count >= logByThisClassCount)
return s_LogStackFrameList[s_LogStackFrameList.Count - logByThisClassCount];
return null;
}
}
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
public static bool OnOpenAsset(int instanceID, int line)
{
if (instanceID == s_InstanceID && s_Line == line)
{
var stackFrame = GetListViewRowCount();
if (stackFrame != null)
{
string fileName = stackFrame.GetFileName();
string fileAssetPath = fileName.Substring(fileName.IndexOf("Assets"));
AssetDatabase.OpenAsset(AssetDatabase.LoadAssetAtPath<MonoScript>(fileAssetPath), stackFrame.GetFileLineNumber());
return true;
}
}
return false;
}
#endif
}
}
通过以上的优化和代码实现,在 Unity 的 ConsoleWindow 中双击日志时,就可以直接跳转到调用封装日志工具的实际代码位置,提高了开发调试的效率。