Unity3d的Log系统重构

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

编者注

由于要重写Unity3d的Log系统,变更为自定义方式,按照Log4j的显示的内容方法

Unity3d的Log

一般在Unity3d中编写日志入下代码

Debug.Log("hello message");

在UnityEditor和UnityEngine当中除了打印message以外,还会打印堆栈信息。性能低下,根据有经验的人讲解,在客户端打印大量日志,会严重降低渲染性能。

Unity3d的Debug原理

原理分析

在Rider中查看Debug.Log的实现,我们可以看到如下内容

public static void Log(object message)
{
    Debug.unityLogger.Log(LogType.Log, message);
}

我们能够了解到,实质是调用了Debug.unityLogger

public static ILogger unityLogger
{
    get
    {
        return Debug.s_Logger;
    }
}

unityLogger实质是调用了Debug.s_Logger,而在下面就定义了s_Logger的实现

internal static ILogger s_Logger = (ILogger) new Logger((ILogHandler) new DebugLogHandler());

DebugLogHandler实质是调用的静态方法,根据UnityEngine各个平台的实现进行调用

using System;
using System.Runtime.CompilerServices;
using UnityEngine.Scripting;

namespace UnityEngine
{
  internal sealed class DebugLogHandler : ILogHandler
  {
    [ThreadAndSerializationSafe]
    [GeneratedByOldBindingsGenerator]
    [MethodImpl(MethodImplOptions.InternalCall)]
    internal static extern void Internal_Log(LogType level, string msg, [Writable] Object obj);

    [ThreadAndSerializationSafe]
    [GeneratedByOldBindingsGenerator]
    [MethodImpl(MethodImplOptions.InternalCall)]
    internal static extern void Internal_LogException(Exception exception, [Writable] Object obj);

    public void LogFormat(LogType logType, Object context, string format, params object[] args)
    {
      DebugLogHandler.Internal_Log(logType, string.Format(format, args), context);
    }

    public void LogException(Exception exception, Object context)
    {
      DebugLogHandler.Internal_LogException(exception, context);
    }
  }
}

最终实现仅仅只有两个方法

internal static extern void Internal_Log(LogType level, string msg, [Writable] Object obj);
internal static extern void Internal_LogException(Exception exception, [Writable] Object obj);

代码测试

Debug.unityLogger.Log(LogType.Log,"hello message");

UnityEditor打印

hello message
UnityEngine.Logger:Log(LogType, Object)
InitController:Start() (at Assets/Scripts/Controller/InitController.cs:14)

结论:无法解决减少堆栈信息打印的问题

UnityEngine.Application

根据UnityEngine.Application的LogCallback文档,我们能够了解和LogCallback相关的 Application.logMessageReceivedApplication.logMessageReceivedThreaded:

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

由于LogCallback使用的是delegate委托方法,则需要指定实现方法,如下Unity官方推荐实现方法

using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    public string output = "";
    public string stack = "";
    void OnEnable() {
        Application.logMessageReceived += HandleLog;
    }
    void OnDisable() {
        Application.logMessageReceived -= HandleLog;
    }
    void HandleLog(string logString, string stackTrace, LogType type) {
        output = logString;
        stack = stackTrace;
    }
}

原理分析

logMessageReceived仅仅在main线程工作。也就是Unity的主线程当中。并且文档描述实现方法非线程安全。logMessageReceivedThreaded实现的代码必须是线程安全,支持从主线程之外进行访问。

    public static event Application.LogCallback logMessageReceived
    {
      add
      {
        Application.s_LogCallbackHandler += value;
        Application.SetLogCallbackDefined(true);
      }
      remove
      {
        Application.s_LogCallbackHandler -= value;
      }
    }

    public static event Application.LogCallback logMessageReceivedThreaded
    {
      add
      {
        Application.s_LogCallbackHandlerThreaded += value;
        Application.SetLogCallbackDefined(true);
      }
      remove
      {
        Application.s_LogCallbackHandlerThreaded -= value;
      }
    }

CallLogCallback从CSharp的注解来看,就是需要本地代码进行调用。直接调用的,也就是说,会优先调用主线程的logCallbackHandler实现,然后无论是否主线程都调用callbackHandlerThreaded的实现

    [RequiredByNativeCode]
    private static void CallLogCallback(string logString, string stackTrace, LogType type, bool invokedOnMainThread)
    {
      if (invokedOnMainThread)
      {
        Application.LogCallback logCallbackHandler = Application.s_LogCallbackHandler;
        if (logCallbackHandler != null)
          logCallbackHandler(logString, stackTrace, type);
      }
      Application.LogCallback callbackHandlerThreaded = Application.s_LogCallbackHandlerThreaded;
      if (callbackHandlerThreaded == null)
        return;
      callbackHandlerThreaded(logString, stackTrace, type);
    }

SetLogCallbackDefined

    [GeneratedByOldBindingsGenerator]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetLogCallbackDefined(bool defined);

日志系统设计

需求

Log4Net

根据之前Java的方式,Log4j很好用,首先决定仿照slf4j的接口方式进行使用。其次使用Log4net的实现,实现需求,只要不影响Unity运行即可。实际测试并未影响Unity运行。

接口设计

LoggerFactory

using log4net;

namespace Assets.Scripts.Utils.Log4Unity
{
    public class LoggerFactory
    {
        public static Log4Unity getLogger(string name)
        {
            return new Log4Unity(LogManager.GetLogger(name));
        }
    }
}

Log4Unity:原本想直接使用logger,但是被Unity占用了

using System;
using System.IO;
using log4net;
using log4net.Config;
using UnityEngine;

namespace Assets.Scripts.Utils.Log4Unity
{
    public class Log4Unity
    {
        private ILog log;
        public Log4Unity(ILog log)
        {
            this.log = log;
        }

        public void debug(string message)
        {
            this.log.Debug(message);
            LogConfigurator.refresh();
        }

        public void info(string message)
        {
            this.log.Info(message);
            LogConfigurator.refresh();
        }

        public void warning(string message)
        {
            this.log.Warn(message);
            LogConfigurator.refresh();
        }

        public void error(string message)
        {
            Debug.LogError(message);
            if(!LogConfigurator.lazy_mode)
                this.log.Error(message);
            LogConfigurator.refresh();
        }

        public void exception(Exception exception)
        {
            Debug.LogException(exception);
            if(!LogConfigurator.lazy_mode)
                this.log.Warn(exception.ToString());
            LogConfigurator.refresh();
        }

        public void fatal(string message)
        {
            Debug.LogAssertion(message);
            if(!LogConfigurator.lazy_mode)
                this.log.Fatal(message);
            LogConfigurator.refresh();
        }
    }

    
}

Log4Unity的初始化

Log4Unity初始化,使用Unity的MonoBehaviour来完成,同时打印些简单日志,检查日志文件位置

using System.IO;
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using UnityEngine;

namespace Assets.Scripts.Utils.Log4Unity
{
public class LogConfigurator : MonoBehaviour
    {
        private static readonly string config_path = "log4unity.properties";
        private static readonly ILog logger = LogManager.GetLogger("LogConfigurator");
        private static bool config_load = false;
        public static bool refresh_realtime = true;
        public static bool lazy_mode = true;

        private static FileInfo fileInfo = new FileInfo(config_path);
        
        private void Awake()
        {
            if (fileInfo.Exists)
            {
                XmlConfigurator.Configure(fileInfo);
                IAppender appender = LogManager.GetRepository().GetAppenders()[0];
                if (appender.Name.Equals("FileAppender"))
                {
                    FileAppender fileAppender = (FileAppender) appender;

                    Debug.Log("[logpath]:" + fileAppender.File);
                }
                
                
                config_load = true;
            }
            else
            {
                Debug.LogError("class Log4Unity method Awake configfile " + fileInfo.FullName + " is not existed.");
            }
        }

        private void OnEnable()
        {
            logger.Debug("method OnEnable");
            if(config_load == true)
            {
                Application.logMessageReceivedThreaded += ThreadLog;
            }
        }

        private void ThreadLog(string condition, string stackTrace, LogType type)
        {
            if (LogType.Warning.Equals(type))
            {
                logger.Warn(condition);
            }
            else if(LogType.Log.Equals(type))
            {
                logger.Info(condition);
            }
            
            // fixed:double print
            if(lazy_mode)
                if (LogType.Exception.Equals(type))
                {
                    logger.Warn(condition);
                }
                else if (LogType.Error.Equals(type))
                {
                    logger.Error(condition);
                }
                else if (LogType.Assert.Equals(type))
                {
                    logger.Fatal(condition);
                }

            refresh();
        }

        public static void refresh()
        {
            if(refresh_realtime)
                fileInfo.Refresh();
        }

        private void OnDisable()
        {
            logger.Debug("method OnDisable");
            if(config_load == true)
            {
                Application.logMessageReceivedThreaded -= ThreadLog;
            }
        }

        private void OnDestroy()
        {
            logger.Debug("method OnDestroy");
            fileInfo.Refresh();
        }
    }
}

其他配置

log4unity.properties

注意放到exe

<?xml version='1.0' encoding='UTF-8'?>
<log4net>
	<appender name="FileAppender" type="log4net.Appender.FileAppender">
		<file value="log4unity.log" />
		<appendToFile value="true" />
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%date %-5level --- [%-5thread]  %-20logger : %message%newline" />
		</layout>
	</appender>
	
	<root>
        <level value="DEBUG" />
		<appender-ref ref="FileAppender" />
    </root>
</log4net>

log4net.dll

注意:Unity在Windows上有两种运行时DotNet2.0和DotNet4.6,都需要加载正确的dll版本。还有UnityEditor的行为在两个DotNet版本,运行的目录不同,注意日志的输出位置。build之后不会出现问题。

使用方法

public class FpsCounter : MonoBehaviour
    {
        private static readonly Log4Unity logger = LoggerFactory.getLogger("FpsCounter");
        
        ....
        
        private void Start()
        {
            logger.info("method Start");
            ....
        }

吐槽

最近开的坑有点多,先把这个补上

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看