github源码:https://github.com/Cysharp/ZLogger/tree/master
作者两篇文章1(部分旧的不适用):https://neuecc.medium.com/zlogger-zero-allocation-logger-for-net-core-and-unity-d51e675fca76
作者两篇文章2:https://neuecc.medium.com/zlogger-v2-architecture-leveraging-net-8-to-maximize-performance-2d9733b43789
C#10 自定义内插字符串:https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/
C# 源代码生成器简介:https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
概述
-
高性能;支持模板配置;可以动态生成日志代码的日志库。
-
可以解决常见unity日志问题:
-
根据级别开关日志。
-
字符串参数装箱问题。
-
日志屏蔽不全的参数导致没必要的调用。
-
线程异步的日志。
-
-
可扩展:输出多个文件,自动生成额外参数。
和传统对比
日志开关统一
- 平常开发打log没有统一日志库时,需要自己定义开关,通常也会漏开关,不受控。要么加宏开关,要么自己定义变量。
// 使用宏,需要代码头部定义宏,或者工程配置宏。
// 不用时,没有性能消耗,因为代码不会编译进去。就是不方便统一管理。
[Conditional("DEBUG_LOG")]
public static void DebugLog1(string str)
{
Debug.Log($"DebugLog: {str}");
}
// 编译时,如果宏没开启,这行直接不会编译。
DebugLog1($"hello {id}");
// 自定义开关,开关可以统一。
// 不用时,还是会编译,如果参数会执行,有额外不必要开销。
// 在外面调用if判断也行,但得在所有打印日志前加if,大概率会漏。
public static void DebugLog2(string str)
{
if (enable)
{
Debug.Log($"DebugLog: {str}");
}
}
// 无论是否enable,这里都已经进行了字符串拼接了
DebugLog2($"hello2 {id}");
- ZLogger加了一层日志包装,每个日志管理可以各自控制开关。
using var loggerFactory = LoggerFactory.Create(logging =>
{
// 设置输出日志的等级
logging.SetMinimumLevel(LogLevel.Warning);
});
// 普通Log不会执行,包括参数,在编译时会调整
logger.Log(LogLevel.Information, $"Log {id}");
字符串装箱问题
-
传统日志没有特殊处理,就是String.Format处理字符串,然后传给Debug.Log方法。
-
ZLogger,通过C#的自定义内插字符串(InterpolatedStringHandler),实现对特殊参数用泛型代替,避免装箱操作。源码在:
ZLoggerInterpolatedStringHandler.cs
实现原理
1、2:初始化日志信息,字符串信息,判断是否开启
3:从池子里取或创建字符串模板
-
$"ZLog Hello {name} your id is {id}"
的模板: -
["ZLog Hello ", null, " your id is ", null]
4:记录上下文,方法名,文件行号等
5:输出字符串
MagicalBox
- MagicalBox:用来避免装箱,装任意数据
编译时自动生成格式化日志代码
-
需要编译c#版本preview版本(csc.rsp文件),LangVersion.props 改版本 11
-
比起上面的日志生成,这个更高效,但需要额外写代码,也不保证调试正常,毕竟unity没直接支持。
-
ZLoggerMessage
:https://github.com/Cysharp/ZLogger/tree/master?tab=readme-ov-file#zloggermessage-source-generator -
类似.net支持的: https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator
-
https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
System.Span<T>
-
常用于直接取数组的一段数据,直接取得相同地址数据,不会额外分配内存。比如可以代替
String.SubString()
的功能,且不生成新的字符串。- 比如将 “DateTime.Now:@date:yyyy-MM-dd”,分前后两段
-
也可以用于说明数组只读性,
System.ReadOnlySpan<T>
: -
操作一些编码
byte[] buffer = new byte[1024];
Span<byte> header = buffer.AsSpan(0, 128); // 取前128字节
Span<byte> body = buffer.AsSpan(128);
- 分配临时栈内存
Span<int> stackData = stackalloc int[10]; // 栈上分配
for (int i = 0; i < stackData.Length; i++)
{
stackData[i] = i;
}