Unity日志记录和输出
代码如下:
// ***********************************************************************************
// FileName: LogHandlerRotateComposite.cs
// Description:
//
// Version: v1.0.0
// Creator: Jacky(jackylvm@foxmail.com)
// CreationTime: 2025-09-14 14:41:47
// ==============================================
// History update record:
//
// ==============================================
// *************************************************************************************
using System;
using System.IO;
using System.Linq;
using UnityEngine;
namespace JackyExtendedToolSet.Runtime.Logger
{
// 日志文件轮转
// 结合文件大小和文件时间进行轮转
public class LogHandlerRotateComposite : MonoBehaviour
{
// 每个日志文件的最大大小(64MB)
[SerializeField] private long maxLogFileSize = 64 * 1024 * 1024;
// 最大日志文件数量
[SerializeField] private int maxLogFileCount = 5;
// 日志文件保存路径
[SerializeField] private string logDirectory = "Logs";
// 日志文件前缀
[SerializeField] private string logFilePrefix = "GameLog_";
private string _logFilePath;
private StreamWriter _currentLogWriter;
private string _currentLogFilePath;
/// <summary>
/// Awake 函数会在游戏对象被实例化之后立即调用,而且在所有对象的 Start 函数调用之前执行。一般在 Awake 函数里进行以下操作:
/// <para>1. 初始化引用:查找并存储对其他组件或者游戏对象的引用,确保后续代码能够方便地访问这些对象。</para>
/// <para>2. 初始化设置:对脚本中的变量进行初始赋值。</para>
/// <para>3. 建立连接:比如和数据库或者网络服务建立连接。</para>
/// </summary>
private void Awake()
{
_logFilePath = Path.Combine(Application.persistentDataPath, logDirectory);
// 确保日志目录存在
EnsureLogDirectoryExists();
// 清理超出数量限制的旧日志
CleanupOldLogs();
// 初始化日志文件(使用现有最新的或创建新的)
InitializeLogFile();
DontDestroyOnLoad(gameObject);
}
/// <summary>
/// Start 函数会在脚本实例被启用时调用,并且只在第一帧更新之前执行一次。它一般用于:
/// <para>1. 依赖初始化:当某些初始化操作依赖于其他对象的 Awake 函数完成时,可以在 Start 函数里进行。</para>
/// <para>2. 数据加载:从文件或者服务器加载数据。</para>
/// </summary>
private void Start()
{
// 注册日志回调
Application.logMessageReceived += OnLogMessageReceived;
}
/// <summary>
/// OnDestroy 函数会在以下几种情况下被调用:
/// <para>1. 当你使用 GameObject.Destroy 方法明确销毁一个游戏对象时,OnDestroy 会在销毁操作执行前被调用。</para>
/// <para>2. 当场景切换时,如果某个游戏对象没有设置为 DontDestroyOnLoad,那么在场景切换过程中该对象被销毁时,OnDestroy 也会被触发。</para>
/// <para>3. 当游戏结束或应用程序关闭时,所有存在的游戏对象都会被销毁,此时也会调用它们各自的 OnDestroy 函数。</para>
/// OnDestroy 函数适用的场景:
/// <para>1. 资源释放:当游戏对象持有一些外部资源,如网络连接、文件句柄、数据库连接等,在对象销毁时需要确保这些资源被正确释放,避免资源泄漏。</para>
/// <para>2. 事件注销:如果在 OnEnable 或者其他地方注册了事件监听器,为了防止出现内存泄漏,需要在 OnDestroy 中注销这些事件监听器。</para>
/// <para>3. 数据保存:在对象销毁前,可能需要将对象的一些状态数据保存到文件或者服务器,以便后续恢复或分析。</para>
/// </summary>
private void OnDestroy()
{
// 注销回调(避免内存泄漏)
Application.logMessageReceived -= OnLogMessageReceived;
// 关闭当前的日志文件
CloseCurrentLogFile();
}
// 日志回调函数
void OnLogMessageReceived(string logString, string stackTrace, LogType type)
{
// 检查当前日志文件是否达到最大大小
if (IsCurrentLogFileTooLarge())
{
CreateNewLogFile();
}
// 确保日志写入器有效
if (_currentLogWriter == null)
{
CreateNewLogFile();
}
if (_currentLogWriter == null) return;
// 写入日志信息
try
{
var logEntry = $"[{DateTime.Now:HH:mm:ss}] [{type}] {logString}";
_currentLogWriter.WriteLine(logEntry);
// 如果是错误日志,同时写入堆栈跟踪
if (type is LogType.Error or LogType.Exception or LogType.Assert)
{
_currentLogWriter.WriteLine($"StackTrace: {stackTrace}");
}
_currentLogWriter.Flush();
}
catch (Exception ex)
{
Debug.LogError($"Failed to write to log file: {ex.Message}");
}
}
private bool IsCurrentLogFileTooLarge()
{
if (_currentLogWriter == null || string.IsNullOrEmpty(_currentLogFilePath) || !File.Exists(_currentLogFilePath))
{
return false;
}
try
{
var fileInfo = new FileInfo(_currentLogFilePath);
return fileInfo.Length >= maxLogFileSize || fileInfo.CreationTime.Date != DateTime.Now.Date;
}
catch (Exception ex)
{
Debug.LogError($"Failed to check log file size: {ex.Message}");
return false;
}
}
/// <summary>
/// 清理超出最大数量限制的旧日志文件
/// </summary>
private void CleanupOldLogs()
{
try
{
// 获取所有符合命名规则的日志文件
var logFiles = Directory.GetFiles(_logFilePath, $"{logFilePrefix}*.log")
.Select(path => new FileInfo(path))
.OrderBy(fi => fi.CreationTime) // 按创建时间排序( oldest first )
.ToList();
// 如果文件数量超过限制,删除最旧的
while (logFiles.Count > maxLogFileCount)
{
var oldestFile = logFiles.First();
try
{
File.Delete(oldestFile.FullName);
logFiles.RemoveAt(0);
Debug.Log($"Deleted old log file: {oldestFile.Name}");
}
catch (Exception ex)
{
Debug.LogError($"Failed to delete old log file {oldestFile.Name}: {ex.Message}");
break;
}
}
}
catch (Exception ex)
{
Debug.LogError($"Error during log cleanup: {ex.Message}");
}
}
private void EnsureLogDirectoryExists()
{
if (!Directory.Exists(_logFilePath))
{
Directory.CreateDirectory(_logFilePath);
}
}
/// <summary>
/// 初始化日志文件,如果存在最新的且未满,则继续使用,否则创建新文件
/// </summary>
private void InitializeLogFile()
{
// 查找最新的日志文件
var latestLogFile = Directory.GetFiles(_logFilePath, $"{logFilePrefix}*.log")
.Select(path => new FileInfo(path))
.OrderByDescending(fi => fi.CreationTime)
.FirstOrDefault();
// 检查最新的日志文件是否可以继续使用(未满)
if (latestLogFile != null)
{
var createdTime = latestLogFile.CreationTime;
var currentTime = DateTime.Now;
if (latestLogFile.Length < maxLogFileSize && createdTime.Date == currentTime.Date)
{
_currentLogFilePath = latestLogFile.FullName;
_currentLogWriter = new StreamWriter(_currentLogFilePath, true);
_currentLogWriter.WriteLine($"===== Log resumed at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");
}
else
{
// 创建新的日志文件
CreateNewLogFile();
}
}
else
{
// 创建新的日志文件
CreateNewLogFile();
}
}
private void CreateNewLogFile()
{
// 关闭当前的日志文件(如果存在)
CloseCurrentLogFile();
// 生成新的日志文件名(包含时间戳)
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
_currentLogFilePath = Path.Combine(_logFilePath, $"{logFilePrefix}{timestamp}.log");
// 创建新的日志文件
_currentLogWriter = new StreamWriter(_currentLogFilePath, true);
_currentLogWriter.WriteLine($"===== Log started at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");
// 检查是否需要清理旧日志
CleanupOldLogs();
}
private void CloseCurrentLogFile()
{
if (_currentLogWriter != null)
{
_currentLogWriter.WriteLine($"===== Log ended at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");
_currentLogWriter.Flush();
_currentLogWriter.Close();
_currentLogWriter.Dispose();
_currentLogWriter = null;
}
}
}
}