适用于Unity的日志增强模块

前言

最近在捣鼓Unity的各种小轮子,用来扩展自己的框架吧~

这次介绍一下用于Unity的日志增强系统

功能介绍

这个东西想加入现有的项目非常容易,单个cs文件复制即用,在游戏启动时初始化一次即可。

保存的日志文件位于Application.persistentDataPath路径的/GameLog目录中,以日志文件创建时间命名,方便后期查阅。

当然实现的功能暂时也很简单,基本上只有同步输出到文件, 之后会准备一个工具来实时监控这个日志文件

实现方法

首先创建工具类LoggingUtils,将其设计为单例模式,包含3个方法:

类型名称说明
方法public void Init游戏启动时初始化的工作
方法private void OnLogMessage捕获到系统Log时的回调函数
方法public void Update定期更新,暂定为FixedUpdate
字段Queue<LogItem> m_vLogs存储单条日志的队列
字段FileInfo m_logFileInfo要写入的外部文件

其中,LogItem是存储单条日志信息的数据结构,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public struct LogItem
{
/// <summary>
/// 日志内容
/// </summary>
public string messageString;

/// <summary>
/// 调用堆栈
/// </summary>
public string stackTrace;

/// <summary>
/// 日志类型
/// </summary>
public LogType logType;

/// <summary>
/// 记录时间
/// </summary>
public DateTime time;
}

初始化

先来看Init方法

签名:public void Init()

在游戏启动时调用,如果有多个场景可以用一个MonoBehavior在其Awake方法中调用,一次运行中仅第一次调用会生效,重复调用不会出问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void Init()
{
if (m_isInited)
{
return;
}
m_isInited = true;
// 创建文件
DateTime timeNow = DateTime.Now;
string path = Application.persistentDataPath + "/GameLog" + timeNow.ToString("yyyyMMddHHmmss") + ".txt";
m_logFileInfo = new FileInfo(path);
var sw = m_logFileInfo.CreateText();
sw.WriteLine("[{0}] - {1}", Application.productName, timeNow.ToString("yyyy/MM/dd HH:mm:ss"));
sw.Close();
Debug.Log("日志文件已创建:" + path);

// 注册回调
m_vLogs = new Queue<NPLogItem>();
Application.logMessageReceived += OnLogMessage;
Debug.Log("日志系统已启动");
}

核心在于“注册回调”这一步,Unity提供了一个方法用于用户自己扩展日志系统,注册完这个回调后,使用Debug.Log之类的方法输出日志时,Unity会同时调用用户自定义的其他方法。

这是Unity5.x的写法,在老版本中写法略有差异,想要兼容老版本Unity的话需要加判断Unity版本的宏,不过我们这个就不考虑老版本了。

回调方法

当Unity输出日志信息时,会调用OnLogMessage这个方法。

签名:private void OnLogMessage(string condition, string stackTrace, LogType type)

其中condition是日志信息本体的字符串。

stackTrace是调用Debug.Log之类方法代码位置的调用栈信息。

type顾名思义是这条日志的类型,分为Log,Warning,Error等

1
2
3
4
5
6
7
8
9
10
private void OnLogMessage(string condition, string stackTrace, LogType type)
{
m_vLogs.Enqueue(new LogItem()
{
messageString = condition,
stackTrace = stackTrace,
logType = type,
time = DateTime.Now
});
}

我们这里还加入了时间戳信息

输出到文件

本来是想把这个步骤直接写进上一个方法里的,不过有时候写入文件这个操作是会产生异常的,所以我就把这个步骤改成了类似异步的写法,OnLogMessage中只是把日志项加入队列,然后定时检查这个队列,把队列最前的一条信息写入文件,如果产生异常则这次Update不做任何操作,日志项也不会被移除,直到成功写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void Update(float dt)
{
if (m_vLogs.Count > 0)
{
try
{
var sw = m_logFileInfo.AppendText();
var item = m_vLogs.Peek(); // 取队首元素但先不移除
var timeStr = item.time.ToString("HH:mm:ss.ff");
var logStr = string.Format("{0}-[{1}]{2}", timeStr, item.logType, item.messageString);
sw.WriteLine(logStr);
sw.Close();
m_vLogs.Dequeue(); // 成功执行了再移除队首元素
}
catch (IOException)
{
}
}
}

可能产生的异常其实就是文件锁,当另外一个进程(比如说之后要做的一个实时日志查看器)已经在读写某个文件时,第二个进程就不能再次打开进行操作了。

使用方法

这里就不提供完整的文件了,自行把上面几个片段拼起来再改成单例模式就能用~

准备一个继承自MonoBehavior的脚本,挂载到每个场景的任意GameObject上(推荐用一个专用初始化的空GO吧)。

脚本的Awake方法中调用初始化方法:

1
2
3
4
5
void Awake()
{
LoggingUtils.instance.Init();
Debug.Log("初始化完成");
}

然后在FixUpdate里加入更新用的代码:

1
2
3
4
void FixedUpdate()
{
NPLoggingUtils.instance.Update(Time.fixedDeltaTime);
}

然后运行游戏,就算什么也不做的话,已经可以在日志文件中至少看到两条日志啦

1
2
17:13:06.42-[Log]日志系统已启动
17:13:06.42-[Log]基本数据初始化完成

日志文件的位置可以在Unity Console的第一条Log里找到

可以做的更好。。

还有很多想法没有实现,比如我们还没有用到调用栈信息,可以考虑当收到了一条Error级别的日志时,同时把调用栈也追加输出到文件中