三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity日志系统重构:基于GameFramework实现分级、结构化与文件输出

Unity日志系统重构:基于GameFramework实现分级、结构化与文件输出

1. 项目概述:为什么Unity的日志系统需要“动手术”?

如果你在Unity项目里待过一段时间,尤其是参与过稍具规模的团队开发,肯定对Debug.Log又爱又恨。爱它,是因为它简单到只需要一行代码,就能把任何信息吐到控制台,是开发初期最快速的调试工具。恨它,则是在项目迭代几个月后,当控制台被成百上千条来源不明、格式混乱的日志信息淹没时,那种“大海捞针”般的绝望感。这不仅仅是视觉上的混乱,更是工程管理上的灾难:线上版本崩溃了,你拿到的只有玩家手机里一个模糊的截图,或者一句“游戏闪退了”,你根本不知道崩溃前一刻,游戏内部到底发生了什么。

这就是我们为什么要对Unity的原生日志系统“动手术”的核心原因。Debug.Log本身只是一个简单的输出管道,它缺乏几个对生产环境至关重要的特性:分级过滤(哪些是错误,哪些只是普通信息?)、结构化输出(日志里包含时间、场景、对象名吗?)、持久化存储(游戏发布后,日志能写入本地文件吗?)、以及运行时可控性(能否在不重启游戏的情况下动态调整日志级别?)。而GameFramework(后文简称GF)作为一个优秀的Unity游戏框架,其内置的日志组件恰好为解决这些问题提供了绝佳的“手术刀”和“缝合线”。

我接手过不少从“Demo状态”过渡到“产品状态”的项目,第一步往往就是重构日志系统。一个健壮的日志系统,就像是给游戏装上了“黑匣子”和“健康监测仪”。它不仅能让你在开发期快速定位Bug,更能在测试期、甚至线上运营期,当用户遇到问题时,提供第一手、可追溯的现场信息。这次,我就把手把手带你,基于GF打造一个既能在编辑器里清晰可读,又能在真机上稳定输出文件,并且支持分级、分类的完整日志解决方案。文末会提供可直接集成使用的核心源码文件。

2. 核心需求解析:从Debug.Log的痛点出发

在动手之前,我们必须明确要解决的具体问题。盲目套用框架只会增加复杂度。让我们把Debug.Log的“罪状”一条条列出来,并转化为我们新日志系统的明确需求。

2.1 Debug.Log的四大核心缺陷

  1. 无分级管理:所有信息,无论是至关重要的错误(Error)、值得警惕的警告(Warning),还是普通的调试信息(Info),全都用Debug.Log一股脑输出。在查找一个具体Bug时,你无法快速过滤掉无关的Info信息,导致关键错误被海量日志淹没。
  2. 信息结构缺失:一条典型的Debug.Log(“Player HP is: ” + hp)输出,你只知道HP值,但不知道这条日志是何时(时间戳)、在哪个游戏场景、由哪个游戏对象(或脚本)发出的。在复杂的异步逻辑或对象池频繁创建销毁的场景下,这简直是噩梦。
  3. 发布后失效:在Unity的发布版本(Development Build除外)中,Debug.Log的输出默认是不可见的。游戏在真机上崩溃了,你无法看到崩溃前的任何日志线索,调试变成了“盲人摸象”。
  4. 性能隐忧Debug.Log内部会进行字符串拼接和系统调用,即便在发布版本中日志不显示,这些操作依然会发生,产生不必要的性能开销。大量高频的日志输出在移动设备上可能成为性能瓶颈。

2.2 新日志系统的设计目标

针对以上缺陷,我们为新系统设定清晰的目标:

  • 目标一:分级日志。必须支持至少四个标准级别:Debug(调试)、Info(信息)、Warning(警告)、Error(错误)。允许在运行时根据需求(如开发期、测试期、线上期)动态设置输出级别,例如线上版本只输出ErrorWarning
  • 目标二:结构化与可追溯。每条日志必须自动携带核心上下文信息,包括但不限于:精确到毫秒的时间戳、当前场景名称、发出日志的类名/方法名(可选)、线程ID(对于多线程任务)。这能让我们像看侦探小说的“时间线”一样复盘问题。
  • 目标三:多输出渠道。日志不能只输出到Unity编辑器控制台。核心是要支持文件输出,将日志实时写入到设备的持久化存储路径(如Android的Application.persistentDataPath),确保发布后日志可获取。同时,最好能保留编辑器内的彩色输出,提升可读性。
  • 目标四:性能与可控性。日志系统本身应轻量高效。对于Debug级别的日志,应提供条件编译或运行时开关,确保在最终发布版本中,这些调试日志的代码和调用开销可以被完全移除或关闭,实现“零成本”。
  • 目标五:与GameFramework生态集成。既然使用GF,日志系统应能无缝接入GF现有的流程管理、配置管理和资源管理模块,例如通过GF的GameEntry组件便捷获取,通过配置表来初始化参数。

3. 技术选型与GameFramework日志模块深度解析

为什么选择GameFramework作为基础?市面上当然有优秀的独立日志库,如log4netNLog的Unity版本,或者UnityLogger的扩展。但GF的日志模块有一个无可替代的优势:它与GF框架深度绑定,设计理念高度统一。对于已经或计划使用GF管理游戏生命周期、资源、UI、场景的项目来说,使用其内置组件能减少依赖冲突,学习成本更低,并且能享受到GF提供的统一初始化、销毁管理。

3.1 GameFramework日志模块架构剖析

GF的日志系统核心位于GameFramework程序集的GameFramework命名空间下,主要包含以下几个关键接口和类:

  • ILogHelper:日志辅助器接口。这是整个日志系统的“心脏”,定义了日志实际被记录和输出的行为。GF默认提供了一个向Unity控制台输出的辅助器(UnityLogHelper),但我们要做的就是实现一个自定义的ILogHelper,将日志同时输出到控制台和文件。
  • Log:静态日志管理器类。我们通常通过Log.Info,Log.Warning,Log.Error等静态方法来记录日志。它内部持有一个ILogHelper实例,负责将日志调用转发给辅助器执行。
  • LogLevel:枚举类型,定义了Debug,Info,Warning,Error,Fatal等多个日志级别。
  • GameFrameworkLog:GF框架内部使用的日志类,一般我们不需要直接操作。

其工作流程可以简化为:Log.Info(“message”)->Log静态类 -> 当前设置的ILogHelper实例 -> 执行具体的输出逻辑(如UnityEngine.Debug.Log或写入文件)。

3.2 我们的增强方案:自定义LogHelper

GF默认的UnityLogHelper只解决了向编辑器控制台输出的问题,没有文件功能,信息也不够结构化。因此,我们的核心任务就是实现一个自定义的CustomLogHelper。这个辅助器需要完成:

  1. 格式化:将传入的日志级别、消息、异常等信息,组合成一条包含时间、场景、线程等丰富上下文的格式化字符串。
  2. 多路输出
    • 控制台输出:继续调用UnityEngine.Debug.Log(或LogWarning,LogError),并利用富文本标签为不同级别的日志着色,提升编辑器内辨识度。
    • 文件输出:将格式化后的字符串,异步、高效地写入到本地的一个文本文件中。这里必须考虑文件大小滚动、写入性能(避免阻塞主线程)、多线程安全等问题。
  3. 级别过滤:在辅助器内部或Log管理器层面,根据当前设置的日志级别,过滤掉不需要输出的低级别日志。

3.3 关键设计决策:同步写入 vs 异步队列

文件写入是一个I/O操作,如果每次调用日志都直接同步写文件,可能会因为磁盘I/O速度而阻塞游戏主线程,尤其是在移动设备上,可能引发卡顿。因此,一个成熟的设计是采用生产者-消费者模型

  • 日志调用方(生产者):将格式化好的日志字符串放入一个线程安全的队列(如ConcurrentQueue<string>)。
  • 独立的写入线程或协程(消费者):在后台定期(例如每0.5秒)或当队列达到一定长度时,批量将队列中的日志取出,一次性写入文件。

这种方式将耗时的I/O操作与游戏主逻辑解耦,保证了游戏运行的流畅性。在Unity中,我们可以使用System.Threading.Tasks.Task或一个独立的MonoBehaviour协程来充当消费者。考虑到GF的整体风格和Unity的兼容性,使用协程进行定时批量写入是更稳妥的选择。

4. 手把手实现:自定义LogHelper与文件输出器

理论讲完,我们开始动手编码。我会分步骤解释关键代码,完整的源码文件可以在文章末尾找到并下载。

4.1 第一步:定义日志数据结构和配置类

在实现辅助器之前,我们先定义一些基础结构。

// LogData.cs - 封装一条日志的完整信息 public struct LogData { public LogLevel Level { get; set; } public string Message { get; set; } public string StackTrace { get; set; } // 可选的堆栈信息 public DateTime Time { get; set; } public string SceneName { get; set; } // 你可以根据需要添加更多字段,如对象实例ID、线程ID等。 } // LogConfig.cs - 日志系统运行时配置 public class LogConfig { public LogLevel MinLogLevel { get; set; } = LogLevel.Debug; // 允许输出的最低日志级别 public bool EnableConsoleLog { get; set; } = true; // 是否启用控制台输出 public bool EnableFileLog { get; set; } = true; // 是否启用文件输出 public string LogFileDirectory { get; set; } // 日志文件存放目录 public string LogFileNamePrefix { get; set; } = "GameLog"; // 日志文件前缀 public int MaxLogFileSizeKB { get; set; } = 1024; // 单个日志文件最大大小(KB),超过则滚动 public int MaxBackupFiles { get; set; } = 5; // 保留的旧日志文件数量 }

这个LogConfig类非常有用,我们可以通过GF的配置组件(如BaseComponent的配置读取)来初始化它,实现不修改代码即可调整日志行为。

4.2 第二步:实现核心的CustomLogHelper

这是最核心的类,它继承并实现GameFramework.ILogHelper接口。

// CustomLogHelper.cs using GameFramework; using System; using System.Collections.Concurrent; using System.IO; using System.Text; using UnityEngine; public class CustomLogHelper : ILogHelper { private readonly LogConfig _config; private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>(); private StreamWriter _logFileWriter; private string _currentLogFilePath; private bool _isWriting = false; private float _lastWriteTime = 0f; private const float WRITE_INTERVAL = 0.5f; // 每0.5秒批量写入一次 public CustomLogHelper(LogConfig config) { _config = config ?? throw new ArgumentNullException(nameof(config)); InitializeFileLogging(); // 启动一个MonoBehaviour协程来处理队列写入,这里需要挂载到某个GameObject上。 // 通常我们会在GameEntry启动时,将一个专用的LoggerRunner挂载上去。 } private void InitializeFileLogging() { if (!_config.EnableFileLog) return; try { string dir = string.IsNullOrEmpty(_config.LogFileDirectory) ? Application.persistentDataPath : _config.LogFileDirectory; if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); _currentLogFilePath = Path.Combine(dir, $"{_config.LogFileNamePrefix}_{DateTime.Now:yyyyMMdd_HHmmss}.log"); // 使用UTF-8编码,支持中文。FileShare.ReadWrite允许其他进程(如日志查看工具)同时读取。 _logFileWriter = new StreamWriter(_currentLogFilePath, true, Encoding.UTF8, 8192) { AutoFlush = false // 我们手动批量Flush,性能更好 }; _logFileWriter.WriteLine($"=== Log Session Started at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} ==="); } catch (Exception e) { UnityEngine.Debug.LogError($"Failed to initialize file logging: {e}"); _config.EnableFileLog = false; } } // 实现ILogHelper接口的核心方法 public void Log(GameFramework.LogLevel level, object message) { InternalLog(level, message?.ToString(), null); } public void Log(GameFramework.LogLevel level, object message, Exception exception) { InternalLog(level, message?.ToString(), exception); } private void InternalLog(GameFramework.LogLevel level, string message, Exception exception) { // 1. 级别过滤 if ((int)level < (int)_config.MinLogLevel) return; // 2. 构建结构化日志数据 var logData = new LogData { Level = level, Message = message, StackTrace = exception?.StackTrace, Time = DateTime.Now, SceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name }; // 3. 格式化日志字符串 string formattedLog = FormatLog(logData, exception); // 4. 控制台输出(带颜色) if (_config.EnableConsoleLog) { OutputToConsole(level, formattedLog); } // 5. 文件输出(入队) if (_config.EnableFileLog && _logFileWriter != null) { _logQueue.Enqueue(formattedLog); } } private string FormatLog(LogData data, Exception exception) { // 示例格式:[2023-10-27 14:30:25.123] [INFO] [MainMenu] Player health changed to 50. // Exception: NullReferenceException: Object reference not set... StringBuilder sb = new StringBuilder(); sb.Append($"[{data.Time:yyyy-MM-dd HH:mm:ss.fff}] "); sb.Append($"[{data.Level.ToString().ToUpper()}] "); sb.Append($"[{data.SceneName}] "); sb.Append(data.Message); if (exception != null) { sb.AppendLine(); // 换行显示异常 sb.Append($"Exception: {exception.GetType().Name}: {exception.Message}"); if (!string.IsNullOrEmpty(data.StackTrace)) { sb.AppendLine(); sb.Append($"StackTrace: {data.StackTrace}"); } } return sb.ToString(); } private void OutputToConsole(GameFramework.LogLevel level, string message) { // 利用Unity富文本为不同级别日志着色 string colorMessage; switch (level) { case GameFramework.LogLevel.Debug: colorMessage = $"<color=#888888>{message}</color>"; // 灰色 UnityEngine.Debug.Log(colorMessage); break; case GameFramework.LogLevel.Info: UnityEngine.Debug.Log(message); // 默认白色 break; case GameFramework.LogLevel.Warning: colorMessage = $"<color=yellow>{message}</color>"; UnityEngine.Debug.LogWarning(colorMessage); break; case GameFramework.LogLevel.Error: case GameFramework.LogLevel.Fatal: colorMessage = $"<color=red>{message}</color>"; UnityEngine.Debug.LogError(colorMessage); break; } } // 这个Update方法需要由挂载的MonoBehaviour在Update中调用 public void Update(float deltaTime) { if (!_config.EnableFileLog || _isWriting) return; _lastWriteTime += deltaTime; if (_lastWriteTime >= WRITE_INTERVAL && !_logQueue.IsEmpty) { _lastWriteTime = 0f; // 可以在这里启动一个协程或Task来执行实际的写入,避免阻塞Update。 WriteQueueToFile(); } } private void WriteQueueToFile() { _isWriting = true; try { int count = 0; while (count < 100 && _logQueue.TryDequeue(out string logEntry)) // 每次最多写100条 { _logFileWriter.WriteLine(logEntry); count++; } _logFileWriter.Flush(); // 批量刷新到磁盘 // 检查文件大小,如果超过限制,进行滚动 CheckAndRollLogFile(); } catch (Exception e) { UnityEngine.Debug.LogError($"Error writing log to file: {e}"); } finally { _isWriting = false; } } private void CheckAndRollLogFile() { if (_logFileWriter?.BaseStream == null) return; if (_logFileWriter.BaseStream.Length <= _config.MaxLogFileSizeKB * 1024) return; // 关闭当前文件 _logFileWriter.Close(); _logFileWriter = null; // 文件重命名归档,例如 GameLog_20231027_143025.log -> GameLog_20231027_143025.1.log RollExistingFiles(_currentLogFilePath); // 创建新的日志文件 InitializeFileLogging(); } private void RollExistingFiles(string basePath) { // 实现日志文件滚动逻辑,保留最新的N个文件 // 例如,将.4.log删除,将.3.log重命名为.4.log,...,将当前.log重命名为.1.log // 此处代码略,文末源码中提供完整实现。 } public void Shutdown() { // 游戏退出时,将队列中剩余日志写入文件并关闭流 WriteQueueToFile(); // 最后刷一次 _logFileWriter?.Close(); _logFileWriter = null; } }

注意:上面的Update方法需要由一个MonoBehaviour驱动。我们通常会创建一个名为LogManagerComponent的GF游戏框架组件,它继承自GameFrameworkComponent,在其Update方法中调用CustomLogHelper.Update。同时,Shutdown方法也需要在游戏退出时(如OnDestroy)被调用。

4.3 第三步:创建LogManagerComponent集成到GameFramework

为了让我们的日志系统被GF框架管理,我们需要创建一个框架组件。

// LogManagerComponent.cs using GameFramework; using UnityEngine; public class LogManagerComponent : GameFrameworkComponent { private CustomLogHelper _logHelper; private LogConfig _config; protected override void Awake() { base.Awake(); // 1. 初始化配置(可以从资源或配置表加载) _config = new LogConfig { MinLogLevel = LogLevel.Debug, EnableConsoleLog = true, EnableFileLog = true, LogFileDirectory = Application.persistentDataPath + "/Logs", MaxLogFileSizeKB = 2048, // 2MB MaxBackupFiles = 3 }; // 2. 创建并设置自定义LogHelper _logHelper = new CustomLogHelper(_config); GameFramework.Log.SetLogHelper(_logHelper); // 3. 输出一条系统启动日志 Log.Info($"Log system initialized. File path: {_config.LogFileDirectory}"); } private void Update() { // 驱动日志辅助器的更新,用于处理文件写入队列 _logHelper?.Update(Time.deltaTime); } protected override void OnDestroy() { // 关闭日志系统 _logHelper?.Shutdown(); base.OnDestroy(); } // 提供外部API,用于运行时动态修改日志级别(例如通过调试菜单) public void SetMinLogLevel(LogLevel level) { _config.MinLogLevel = level; Log.Info($"Log level changed to: {level}"); } }

将这个LogManagerComponent的脚本挂载到GameFramework启动场景中GameFramework游戏对象下(与BaseComponent,ResourceComponent等并列)。这样,在GF启动时,我们的日志系统就会自动初始化。

4.4 第四步:在项目中使用新的日志API

现在,你可以在项目的任何地方,像以前使用Debug.Log一样使用新的日志系统,但功能强大得多。

// 在任何MonoBehaviour或普通C#类中 using GameFramework; public class PlayerHealth : MonoBehaviour { private int _health = 100; public void TakeDamage(int damage) { _health -= damage; // 使用分级日志 Log.Info($"[{gameObject.name}] Took {damage} damage. Current health: {_health}"); if (_health <= 0) { Log.Warning($"[{gameObject.name}] Health is zero or below! Player died."); Die(); } } private void Die() { try { // ... 一些可能抛出异常的逻辑 throw new System.InvalidOperationException("Respawn point not set."); } catch (System.Exception e) { // 记录异常,它会自动包含堆栈信息 Log.Error("Failed to process player death.", e); } } }

在Unity编辑器中,你会看到带有颜色和完整信息的日志。在移动设备上运行后,你可以在对应的持久化数据路径(如Android的/Android/data/<package名>/files/Logs/)下找到生成的.log文本文件,用任何文本编辑器即可查看完整的、按时间排序的日志记录。

5. 高级特性与性能优化实战

一个基础的、可用的日志系统已经完成了。但对于追求极致和适应复杂项目的团队,我们还需要考虑更多。

5.1 日志文件滚动与归档策略

上面代码中提到了CheckAndRollLogFile方法。一个健壮的策略是:

  • 按大小滚动:当当前日志文件超过设定大小(如2MB)时,关闭它,并重命名为带编号的备份文件(如GameLog.1.log)。
  • 按日期滚动:除了大小,还可以每天或每小时生成一个新的日志文件,方便按时间维度排查问题。这可以通过在InitializeFileLogging中根据当前时间生成不同的文件名来实现。
  • 清理旧文件:在滚动时,检查备份文件数量,如果超过MaxBackupFiles(如5个),则删除最旧的那个(如GameLog.5.log)。这能防止日志文件无限增长,占用过多磁盘空间。

5.2 条件编译与发布优化

在开发阶段,我们需要大量的DebugInfo日志。但在发布版本中,这些日志不仅无用,其字符串拼接和函数调用还会产生开销。我们可以利用C#的条件编译符号来彻底移除它们。

首先,在Unity的Player Settings->Scripting Define Symbols中为发布版本添加一个符号,例如RELEASE

然后,修改我们的日志调用方式:

// 定义一个条件编译的快捷类 public static class GameLogger { [System.Diagnostics.Conditional("DEBUG"), System.Diagnostics.Conditional("UNITY_EDITOR")] public static void Debug(object message) { Log.Debug(message); } // Info、Warning、Error通常保留,因为Warning和Error在线上也需要。 public static void Info(object message) => Log.Info(message); public static void Warning(object message) => Log.Warning(message); public static void Error(object message) => Log.Error(message); public static void Error(object message, System.Exception exception) => Log.Error(message, exception); }

在代码中,对于仅用于调试的日志,使用GameLogger.Debug(“…”);。当使用RELEASE模式编译时,这些GameLogger.Debug方法的调用会被编译器完全移除,就像这行代码从未写过一样,实现了零开销。

5.3 集成ELK等日志分析系统(高级话题)

对于大型在线游戏或需要集中分析大量客户端日志的场景,将日志输出到本地文件只是第一步。更高级的做法是建立一个客户端日志上报机制

  1. 设计:在自定义LogHelper中,除了写入本地文件,还可以增加一个“网络通道”。当发生特定级别的日志(如ErrorFatal)时,或者玩家主动提交反馈时,将最近一段时间(如最近100条)的日志文件压缩加密,通过HTTP接口上报到服务器。
  2. 服务器端:服务器接收日志后,可以将其存入数据库,或直接送入如ELK Stack(Elasticsearch, Logstash, Kibana)这样的日志分析平台。
  3. 价值:在Kibana中,你可以对所有玩家的错误日志进行聚合、搜索、可视化。你可以快速发现某个版本更新后,NullReferenceException的错误率是否飙升,并且能直接看到触发该错误的设备型号、操作系统、游戏场景等上下文信息,极大地加速线上问题的定位和解决。

这个功能实现较为复杂,涉及网络通信、数据压缩、加密和服务器端搭建,超出了本文的范围,但它指出了专业级日志系统的发展方向。

6. 常见问题排查与实战技巧

在实际集成和使用过程中,你可能会遇到以下问题。这里是我踩过坑后总结的排查清单和技巧。

6.1 问题排查速查表

问题现象可能原因解决方案
真机上找不到日志文件1. 路径权限问题。
2. 日志未成功初始化。
3. 文件写入被系统拦截。
1. 确保使用Application.persistentDataPath,这是Unity推荐的、应用有写权限的路径。
2. 在AwakeStart时用Log.Info输出一条日志,确认系统已工作。
3. 检查CustomLogHelperInitializeFileLogging方法是否有异常被捕获并禁用文件日志。
日志文件内容为空或不更新1. 日志队列消费者未启动。
2.StreamWriter未正确Flush
3. 日志级别过滤太严格。
1. 确认LogManagerComponentUpdate方法被正常调用(挂载对象需激活)。
2. 检查WriteQueueToFile方法中是否调用了_logFileWriter.Flush()
3. 检查LogConfig中的MinLogLevel,确保你输出的日志级别高于或等于它。
游戏运行时出现卡顿1. 同步文件写入阻塞主线程。
2. 单次写入的日志条目过多或字符串过大。
1.务必确保使用异步队列模式,如示例所示。避免在InternalLog方法中直接调用File.WriteAllText
2. 限制单条日志的长度,对于过长的消息(如打印整个配置表),考虑截断或分条输出。
编辑器内日志无颜色Unity控制台默认不支持脚本中的富文本颜色。我们的OutputToConsole方法已经使用了<color>标签。确保在Unity Console窗口查看,这些标签会被正确解析为颜色。如果无效,检查Unity版本是否支持。
日志文件过大过快1.Debug级别日志过多。
2. 文件滚动策略未生效。
1. 在测试或发布版本中,将MinLogLevel调整为InfoWarning
2. 检查CheckAndRollLogFile方法中的文件大小判断逻辑和滚动重命名逻辑是否正确执行。

6.2 实战技巧与心得

  1. 为日志分类:除了级别,可以为日志打上“标签”或“频道”(Channel),例如”Network”,”UI”,”Audio”。你可以扩展LogDataLog方法,支持频道参数。然后在LogConfig中配置每个频道独立的输出级别和输出目标(如网络日志只输出到文件,UI日志输出到控制台)。这能实现更精细的日志控制。
  2. 关键操作必打日志:对于资源加载/卸载、场景切换、网络请求发起/完成、重要的状态机转换、异常捕获处,务必记录InfoWarning级别的日志。这能在出问题时帮你快速还原操作路径。
  3. 日志信息要足够:一条好的日志应该能让人在不看代码上下文的情况下理解发生了什么。避免”Error happened.”这种日志,而应该是”[NetworkManager] Failed to connect to server ‘192.168.1.100:8080’ after 3 attempts. Last error: Timeout.”
  4. 善用异常日志Log.ErrorLog.Fatal的重载方法可以接受Exception对象。一定要传递这个参数!它会自动记录异常的MessageStackTrace,这是定位崩溃点的最关键信息。
  5. 在测试阶段验证文件日志:在打测试包时,不要只盯着编辑器控制台。定期将测试设备上的日志文件拉取到电脑上查看,确保文件格式正确、内容完整,并且滚动清理机制工作正常。
← 返回列表