开发背景:游戏中日志也是很大的开销,虽然有些日志不打印但是毕竟有字符串的开销,甚至有字符串拼接的开销,有些还有装箱和拆箱的开销,比如Debug.Log(1) 这种
因此需要注释掉,当然还需要提供反注释的功能,需要的时候能立马找回来。
也就是说我们只需要打包的时候处理即可。
1.先检测代码中是否存在不规范代码,类似if(a > 1) Debug.Log("11") 这种代码如果注释掉日志会引起业务逻辑问题需要先找出来,类似的不限于if else for foreach while这些 所以尽可能的要做这个检测,有这类问题直接报错。然后才可以进行后续流程
2.注释Debug.Log或者UnityEngine.Debug.Log,但是有一个问题就是注释之后再注释的问题,所以在调用这个接口前需要调用反注释然后调用注释,这样就可以保证注释是没有问题的。
3.需要提供反注释的代码,主要是配合2的功能
注意事项:大家写代码的过程中想打日志就打日志这个没有限制,打包的时候会自动处理日志。
如果是特别的日志可以打错误日志或者警告,或者自定义日志那种日志不去处理即可。
未来规划是用代码分析给日志加标签,平常开发还是按unity的习惯来我绝对不改变大家的unity标准,在做扩展的时候尽量做到神不知鬼不觉,不让大家有额外操作不让大家有压力。
接着上面扩展 :如下图所示这种else日志不被允许
那么如何进行csharp的语法分析呢 ,我们建一个c#的控制台程序,使用nuget安装3个包
Microsoft.CodeAnalysis.CSharp 和 Microsoft.CodeAnalysis.CSharp.Workspaces 和Microsoft.CodeAnalysis
分析c#代码检测是否存在if else for while foreach 后直接接Debug.Log或者UnityEngine.Debug.Log完整控制台程序如下
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace cs_test
{
internal class Program
{
public static long getCurrentTimeMillis()
{
long milliseconds = System.DateTime.Now.Ticks / System.TimeSpan.TicksPerMillisecond;
return milliseconds;
}
static long firstTime = 0;
const float LogWritetimeOutTime = 2f;
static List<string> strList = new List<string>();
static void SetTimer(float time = LogWritetimeOutTime)
{
System.Timers.Timer timer = new System.Timers.Timer();
timer.Interval = time;
timer.AutoReset = false;
timer.Enabled = true;
timer.Elapsed += (a, b) =>
{
using (var fs = new FileStream(logPath, FileMode.Append,FileAccess.Write,FileShare.Write))
{
using (var sw = new StreamWriter(fs, Encoding.UTF8))
{
lock (strList)
{
foreach (var item in strList)
{
sw.WriteLine(item);
}
strList.Clear();
}
}
}
};
}
static void Log(string str,bool useFileSystem = true)
{
if(useFileSystem && isLogToFile)
{
lock (strList)
{
strList.Add(str);
}
if (firstTime == 0)
{
firstTime = getCurrentTimeMillis();
SetTimer(LogWritetimeOutTime);
return;
}
if (getCurrentTimeMillis() - firstTime > LogWritetimeOutTime)
{
firstTime = 0;
SetTimer(LogWritetimeOutTime);
}
}
else
{
Console.WriteLine(str);
}
}
public class LogStatementWalker : CSharpSyntaxWalker
{
public List<Issue> Issues { get; } = new List<Issue>();
public override void VisitIfStatement(IfStatementSyntax node)
{
CheckBlockStatement(node.Statement, node.GetLocation());
if (node.Else != null)
{
CheckElseStatement(node.Else);
}
base.VisitIfStatement(node);
}
public override void VisitForStatement(ForStatementSyntax node)
{
CheckBlockStatement(node.Statement, node.GetLocation());
base.VisitForStatement(node);
}
public override void VisitForEachStatement(ForEachStatementSyntax node)
{
CheckBlockStatement(node.Statement, node.GetLocation());
base.VisitForEachStatement(node);
}
public override void VisitWhileStatement(WhileStatementSyntax node)
{
CheckBlockStatement(node.Statement, node.GetLocation());
base.VisitWhileStatement(node);
}
private void CheckElseStatement(ElseClauseSyntax elseClause)
{
if (elseClause.Statement is IfStatementSyntax)
{
// Skip checking if it's an "else if" statement
return;
}
CheckBlockStatement(elseClause.Statement, elseClause.GetLocation());
}
private void CheckBlockStatement(StatementSyntax statement, Location location)
{
if (statement is BlockSyntax)
{
return;
}
var logStatements = statement.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(inv => inv.Expression is MemberAccessExpressionSyntax memberAccess &&
(memberAccess.Name.ToString() == "Log" || memberAccess.Name.ToString() == "Debug.Log"))
.ToList();
foreach (var logStatement in logStatements)
{
var lineSpan = logStatement.GetLocation().GetLineSpan();
Issues.Add(new Issue
{
LineNumber = lineSpan.StartLinePosition.Line + 1,
Message = $"Debug.Log or UnityEngine.Debug.Log found in a single-line {statement.Kind()} statement without braces."
});
}
}
}
public class Issue
{
public int LineNumber { get; set; }
public string Message { get; set; }
}
static string unityRoot;
static string logPath = "D:/tempLog.txt";
static bool isLogToFile = true;
static void Main(string[] args)
{
List<string> pathList = new List<string>();
if (args == null || args.Length == 0)
{
var path = System.Environment.CurrentDirectory;
path = path.Substring(0, path.IndexOf("tools"));
path = path.Replace("\\", "/");
args = new string[]
{
path + "Assets/HotUpdate"
};
}
unityRoot = args[0].Substring(0, args[0].IndexOf("Assets"));
logPath = unityRoot + "tools/tempLog.txt";
if(File.Exists(logPath))
{
File.Delete(logPath);
}
pathList.AddRange(args);
isLogToFile = bool.Parse(pathList[pathList.Count - 1]);
pathList.RemoveAt(pathList.Count - 1);
List<string> allCsharpFiles = new List<string>();
foreach(var path_item in pathList)
{
allCsharpFiles.AddRange(Directory.GetFiles(path_item, "*.cs", SearchOption.AllDirectories));
}
bool hasInsue = false;
strList.Clear();
foreach(var item in allCsharpFiles)
{
string sourceCode = File.ReadAllText(item);
SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceCode);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
var diagnostics = tree.GetDiagnostics();
if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
{
Log("Syntax errors found in the source code.");
return;
}
var walker = new LogStatementWalker();
walker.Visit(root);
foreach (var issue in walker.Issues)
{
hasInsue = true;
var itemName = item.Substring(item.IndexOf("Assets"));
Log($"请使用括号封装你的日志! at line {issue.LineNumber}: {itemName}");
}
}
if(!hasInsue)
{
Log("未发现不规范代码");
}
}
/// <summary>
/// 检测判断if语句后面是否带花括号,由于在for循环中经常会遇到if不带花括号的情况,故这个检测暂时放弃 不然
/// 会有非常多的代码提示
/// 这里的代码暂时封存
/// </summary>
/// <param name="args"></param>
static void Main2(string[] args)
{
if(File.Exists(logPath))
{
File.Delete(logPath);
}
File.Create(logPath).Dispose();
string directoryPath = "D:\\qiangsheng_wx\\Assets\\HotUpdate"; // 替换为你的目录路径
// 获取目录中的所有 .cs 文件
string[] files = Directory.GetFiles(directoryPath, "*.cs", SearchOption.AllDirectories);
foreach (string filePath in files)
{
Log($"Processing file: {filePath}");
string code = File.ReadAllText(filePath);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var walker = new IfElseSyntaxWalker();
walker.Visit(root);
}
Console.ReadLine();
}
class IfElseSyntaxWalker : CSharpSyntaxWalker
{
public override void VisitIfStatement(IfStatementSyntax node)
{
base.VisitIfStatement(node);
// 检查 if 语句是否有花括号
if (node.Statement is BlockSyntax) { }
else
{
var str = $"警告: if 语句 at line {node.GetLocation().GetLineSpan().StartLinePosition.Line + 1} does not use 花括号";
Log(str);
}
// 检查 else 语句是否有花括号
if (node.Else != null)
{
if (node.Else.Statement is BlockSyntax) { }
else
{
var str = $"警告: else 语句 at line {node.Else.GetLocation().GetLineSpan().StartLinePosition.Line + 1} does not use 花括号";
Log(str);
}
}
}
}
}
}
控制台程序做完之后还需要对接到unity项目中,我是直接将微信api的代码分析回调日志直接写入了某个文件,让unity调用这个exe文件,你可以封装成bat将参数传给exe执行,unity那边读取文件将文本内容Debug.LogError输出即可。
上诉工作做完了之后就可以提供注释和反注释的代码接口了,将下述代码封装到你的编辑器工具类中或者某个编辑器类下的对象中
private const string LogPattern = @"(?s)(Debug\.Log\s*\(.*?\);|UnityEngine\.Debug\.Log\s*\(.*?\);)";
private const string unLogPattern = @"(?s)/\*(\s*Debug\.Log\s*\(.*?\);\s*|UnityEngine\.Debug\.Log\s*\(.*?\);\s*)\*/";
public static void ForbidLog(string filePath)
{
string fileContent = File.ReadAllText(filePath,Encoding.UTF8);
// 正则表达式匹配 Debug.Log 调用及其相关代码
string pattern = LogPattern;
string replacement = @"/*$1*/";
if(!Regex.IsMatch(fileContent, pattern))
{
return;
}
string newContent = Regex.Replace(fileContent, pattern, replacement);
using (StreamWriter writer = new StreamWriter(filePath, false, new UTF8Encoding(false)))
{
writer.Write(newContent);
}
}
public static void UnForbidLog(string filePath)
{
string fileContent = File.ReadAllText(filePath, Encoding.UTF8);
// 正则表达式匹配被注释的 Debug.Log 调用及其相关代码
string pattern = unLogPattern;
string replacement = @"$1";
if (!Regex.IsMatch(fileContent, pattern))
{
return;
}
string newContent = Regex.Replace(fileContent, pattern, replacement);
using (StreamWriter writer = new StreamWriter(filePath, false, new UTF8Encoding(false)))
{
writer.Write(newContent);
}
}
[MenuItem("Tools/日志/注释所有日志", priority = -1999)]
public static void ReMoveLog()
{
var listFolder = new List<string>();
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/HotUpdate");
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/SDKTool");
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/DevWork");
var allCsharpFiles = new List<string>();
foreach(string folder in listFolder)
{
allCsharpFiles.AddRange(Directory.GetFiles(folder, "*.cs",SearchOption.AllDirectories));
}
string[] filters = new string[]
{
"InitCtrl.cs",
"Global.cs",
"LoginPanel.cs",
"LoginCode.cs",
"ProductModel.cs",
};
foreach(string csFile in allCsharpFiles)
{
var filename = Path.GetFileName(csFile);
if(filters.Contains(filename))
{
Debug.Log("跳过过滤文件" + csFile);
continue;
}
ForbidLog(csFile);
}
allCsharpFiles.Clear();
listFolder.Clear();
listFolder = null;
allCsharpFiles = null;
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
[MenuItem("Tools/日志/反注释所有日志", priority = -1999)]
public static void UnReMoveLog()
{
var listFolder = new List<string>();
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/HotUpdate");
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/SDKTool");
listFolder.Add(System.Environment.CurrentDirectory + "/Assets/DevWork");
var allCsharpFiles = new List<string>();
foreach (string folder in listFolder)
{
allCsharpFiles.AddRange(Directory.GetFiles(folder, "*.cs", SearchOption.AllDirectories));
}
foreach (string csFile in allCsharpFiles)
{
UnForbidLog(csFile);
}
allCsharpFiles.Clear();
listFolder.Clear();
listFolder = null;
allCsharpFiles = null;
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
运行截图: