在Unity中,Application.logMessageReceived 是一个非常实用的事件委托,用于捕获和处理Unity运行时输出的日志信息(包括普通日志、警告和错误)。通过监听这个事件,开发者可以自定义日志的处理逻辑,比如将日志保存到本地文件、上传到服务器,或在游戏内UI中显示等。

基本定义

Application.logMessageReceived 的官方定义如下:

public static event Application.LogCallback logMessageReceived;

其中,Application.LogCallback 是一个委托类型,定义为:

public delegate void LogCallback(string condition, string stackTrace, LogType type);

委托的三个参数分别表示:

  • condition:日志的具体内容(如 Debug.Log("Hello") 中的 "Hello")。
  • stackTrace:日志产生的堆栈跟踪信息(主要用于定位错误位置)。
  • type:日志类型,是 LogType 枚举值(包括 LogWarningErrorAssertException 等)。

核心作用

监听 Application.logMessageReceived 事件后,所有通过 Debug.LogDebug.LogWarningDebug.LogError 等方法输出的日志,以及Unity内部产生的错误(如空引用异常),都会触发该事件的回调函数。

这一机制的典型应用场景包括:

  1. 日志持久化:将运行时日志保存到本地文件,方便后续调试(尤其适用于移动端或发布后的版本)。
  2. 远程日志上报:将关键错误日志上传到服务器,用于监控线上版本的稳定性。
  3. 游戏内日志显示:在游戏UI中实时显示日志(如调试面板),方便测试人员查看。
  4. 自定义日志过滤:根据日志类型(如只处理错误日志)执行特定逻辑(如弹出错误提示)。

使用示例

以下是一个简单的示例,演示如何监听 logMessageReceived 并将日志保存到本地文件:

using UnityEngine;
using System.IO;

public class LogHandler : MonoBehaviour
{
    private string logFilePath;

    void Start()
    {
        // 初始化日志文件路径(如Application.persistentDataPath目录)
        logFilePath = Path.Combine(Application.persistentDataPath, "game_log.txt");
        
        // 注册日志回调
        Application.logMessageReceived += OnLogMessageReceived;
    }

    // 日志回调函数
    void OnLogMessageReceived(string condition, string stackTrace, LogType type)
    {
        // 构建日志内容(包含时间、类型、信息和堆栈)
        string log = $"[{System.DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{type}] {condition}\n";
        if (type == LogType.Error || type == LogType.Exception)
        {
            // 错误日志附加堆栈信息
            log += $"StackTrace: {stackTrace}\n";
        }
        
        // 写入文件(追加模式)
        File.AppendAllText(logFilePath, log);
    }

    void OnDestroy()
    {
        // 注销回调(避免内存泄漏)
        Application.logMessageReceived -= OnLogMessageReceived;
    }
}

注意事项

  1. 线程安全logMessageReceived 的回调函数运行在主线程,因此可以直接操作Unity的API(如UI更新)。如果需要处理多线程日志,可使用 logMessageReceivedThreaded(在后台线程触发,需注意线程安全)。

  2. 注销事件:务必在对象销毁时(如 OnDestroy 中)注销事件(-=),否则可能导致内存泄漏(因为事件会持有对象的引用,阻止其被GC回收)。

  3. 日志类型区分:通过 LogType 可以区分日志级别,例如只处理错误日志:

    if (type == LogType.Error || type == LogType.Exception)
    {
        // 处理错误逻辑(如弹窗提示)
    }
    
  4. 移动端权限:在移动端保存日志文件时,需确保应用有写入权限(Application.persistentDataPath 目录通常是安全的)。

  5. 替代方案:Unity 2017+ 引入了 ILogHandler 接口,可通过自定义日志处理器实现更灵活的日志管理,但 logMessageReceived 仍是简单场景下的首选。

扩展:logMessageReceivedThreaded

Application.logMessageReceivedThreadedlogMessageReceived 功能类似,但回调函数运行在后台线程(而非主线程)。适用于处理大量日志或耗时操作(如网络上传),但需注意:

  • 不能在回调中直接操作Unity的API(如 GameObjectUI),否则会导致崩溃。
  • 需要通过线程同步机制(如 lock)处理共享资源(如文件写入)。

完整的代码示例:

using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Text;

public class LogHandler : MonoBehaviour
{
    // 单例模式,便于全局访问
    public static LogHandler Instance { get; private set; }

    [Header("配置选项")]
    [SerializeField] private bool logToFile = true;
    [SerializeField] private bool logToUI = true;
    [SerializeField] private int maxUILogEntries = 20;
    [SerializeField] private string logFileName = "game_log.txt";

    private string logFilePath;
    private readonly List<string> uiLogEntries = new List<string>();
    private StringBuilder logBuilder = new StringBuilder();

    private void Awake()
    {
        // 确保单例唯一性
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);

        // 初始化日志文件路径
        logFilePath = Path.Combine(Application.persistentDataPath, logFileName);
        
        // 注册日志回调
        Application.logMessageReceived += HandleLog;
    }

    private void OnDestroy()
    {
        // 注销日志回调,防止内存泄漏
        Application.logMessageReceived -= HandleLog;
    }

    private void HandleLog(string logString, string stackTrace, LogType type)
    {
        // 构建完整的日志条目
        string logEntry = $"[{System.DateTime.Now:HH:mm:ss}] [{type}] {logString}";
        
        // 如果是错误或异常,添加堆栈信息
        if (type == LogType.Error || type == LogType.Exception || type == LogType.Assert)
        {
            logEntry += $"\nStackTrace: {stackTrace}";
        }

        // 记录到文件
        if (logToFile)
        {
            WriteLogToFile(logEntry);
        }

        // 更新UI显示
        if (logToUI)
        {
            UpdateUILogs(logEntry);
        }
    }

    private void WriteLogToFile(string logEntry)
    {
        try
        {
            // 追加日志到文件
            File.AppendAllText(logFilePath, logEntry + "\n");
        }
        catch (System.Exception e)
        {
            Debug.LogError($"无法写入日志文件: {e.Message}");
        }
    }

    private void UpdateUILogs(string logEntry)
    {
        // 添加到日志列表
        uiLogEntries.Add(logEntry);
        
        // 限制日志数量
        if (uiLogEntries.Count > maxUILogEntries)
        {
            uiLogEntries.RemoveAt(0);
        }
    }

    // 用于在UI上显示日志
    private void OnGUI()
    {
        if (!logToUI) return;

        // 构建UI显示的日志文本
        logBuilder.Length = 0;
        foreach (string entry in uiLogEntries)
        {
            logBuilder.AppendLine(entry);
        }

        // 创建一个滚动视图显示日志
        GUILayout.BeginArea(new Rect(10, 10, Screen.width - 20, Screen.height - 20));
        scrollPosition = GUILayout.BeginScrollView(scrollPosition);
        GUILayout.TextArea(logBuilder.ToString(), GUI.skin.textArea, GUILayout.ExpandHeight(true));
        GUILayout.EndScrollView();
        GUILayout.EndArea();
    }

    private Vector2 scrollPosition = Vector2.zero;

    // 获取日志文件路径(用于调试或导出)
    public string GetLogFilePath()
    {
        return logFilePath;
    }

    // 清除UI日志
    public void ClearUILogs()
    {
        uiLogEntries.Clear();
    }

    // 清除日志文件
    public void ClearLogFile()
    {
        try
        {
            if (File.Exists(logFilePath))
            {
                File.Delete(logFilePath);
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"无法清除日志文件: {e.Message}");
        }
    }
}