为了对log做更多的自定义处理,在Unity中封装了自己的log类,所有的log都是用该类来输出,其他的都还好,就一个问题一直困扰着我,在编辑器console窗口输出的log日志,双击定位到代码后直接定位到自定义的log中了,这对于定位log触发地址产生了很大的麻烦,还要再手动去找相应的位置自己去定位。
一直都在找方法解决,有人提出过通过封装log类到独立的dll中,然后再在unity中调用就可以解决log定位的问题了,但是觉得这个方案还是很麻烦就没有尝试,具体效果如何也不得而知。
后来找到了另外一个方法,就是下面要介绍的方法了,基本可以完美解决这个问题,不过也会存在一些隐患,介绍完之后我会在加以说明。
UnityEditor中存在这么一个[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]属性。属性会在asset打开时触发,如双击asset或者想我们像我们想处理的这种双击log打开代码。属性的参数其实就是被调用的顺序,从0开始,如果需要按顺序多次调用不同函数就可以在往下排1、2等。属性需要用来修饰static函数,函数格式为bool step(int instanceID, int line)。返回值表示你是否处理了asset的打开,如果返回true说明你会打开这个asset,false说明你不处理转由其他方法处理,可能是默认方法。有了这个方法接下来就是要写相应的callback函数了。
// 需要用到的引用
#if UNITY_EDITOR
using System.Reflection;
using System.Text.RegularExpressions;
#endif
#if UNITY_EDITOR
// 处理asset打开的callback函数
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
static bool OnOpenAsset(int instance, int line) {
// 自定义函数,用来获取log中的stacktrace,定义在后面。
string stack_trace = GetStackTrace();
// 通过stacktrace来定位是否是我们自定义的log,我的log中有特殊文字[SDebug],很好识别
if (!string.IsNullOrEmpty(stack_trace) && stack_trace.Contains(“[SDebug]”)) {
// 正则匹配at xxx,在第几行
Match matches = Regex.Match(stack_trace, @”\(at (.+)\)”, RegexOptions.IgnoreCase);
string pathline = “”;
while (matches.Success) {
pathline = matches.Groups[1].Value;
// 找到不是我们自定义log文件的那行,重新整理文件路径,手动打开
if (!pathline.Contains(“SDebug.Log.cs”)) {
int split_index = pathline.LastIndexOf(“:”);
string path = pathline.Substring(0, split_index);
line = Convert.ToInt32(pathline.Substring(split_index + 1));
string fullpath = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf(“Assets”));
fullpath = fullpath + path;
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(fullpath.Replace(‘/’, ‘\\’), line);
break;
}
matches = matches.NextMatch();
}
return true;
}
return false;
}
static string GetStackTrace() {
// 找到UnityEditor.EditorWindow的assembly
var assembly_unity_editor = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
if (assembly_unity_editor == null) return null;
// 找到类UnityEditor.ConsoleWindow
var type_console_window = assembly_unity_editor.GetType(“UnityEditor.ConsoleWindow”);
if (type_console_window == null) return null;
// 找到UnityEditor.ConsoleWindow中的成员ms_ConsoleWindow
var field_console_window = type_console_window.GetField(“ms_ConsoleWindow”, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
if (field_console_window == null) return null;
// 获取ms_ConsoleWindow的值
var instance_console_window = field_console_window.GetValue(null);
if (instance_console_window == null) return null;
// 如果console窗口时焦点窗口的话,获取stacktrace
if ((object)UnityEditor.EditorWindow.focusedWindow == instance_console_window) {
// 通过assembly获取类ListViewState
var type_list_view_state = assembly_unity_editor.GetType(“UnityEditor.ListViewState”);
if (type_list_view_state == null) return null;
// 找到类UnityEditor.ConsoleWindow中的成员m_ListView
var field_list_view = type_console_window.GetField(“m_ListView”, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (field_list_view == null) return null;
// 获取m_ListView的值
var value_list_view = field_list_view.GetValue(instance_console_window);
if (value_list_view == null) return null;
// 下面是stacktrace中一些可能有用的数据、函数和使用方法,这里就不一一说明了,我们这里暂时还用不到
/*
var field_row = type_list_view_state.GetField(“row”, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (field_row == null) return null;
var field_total_rows = type_list_view_state.GetField(“totalRows”, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (field_total_rows == null) return null;
var type_log_entries = assembly_unity_editor.GetType(“UnityEditorInternal.LogEntries”);
if (type_log_entries == null) return null;
var method_get_entry = type_log_entries.GetMethod(“GetEntryInternal”, BindingFlags.Static | BindingFlags.Public);
if (method_get_entry == null) return null;
var type_log_entry = assembly_unity_editor.GetType(“UnityEditorInternal.LogEntry”);
if (type_log_entry == null) return null;
var field_instance_id = type_log_entry.GetField(“instanceID”, BindingFlags.Instance | BindingFlags.Public);
if (field_instance_id == null) return null;
var field_line = type_log_entry.GetField(“line”, BindingFlags.Instance | BindingFlags.Public);
if (field_line == null) return null;
var field_condition = type_log_entry.GetField(“condition”, BindingFlags.Instance | BindingFlags.Public);
if (field_condition == null) return null;
object instance_log_entry = Activator.CreateInstance(type_log_entry);
int value_row = (int)field_row.GetValue(value_list_view);
int value_total_rows = (int)field_total_rows.GetValue(value_list_view);
int log_by_this_count = 0;
for (int i = value_total_rows – 1; i > value_row; i–) {
method_get_entry.Invoke(null, new object[] { i, instance_log_entry });
string value_condition = field_condition.GetValue(instance_log_entry) as string;
if (value_condition.Contains(“[SDebug]”)) {
log_by_this_count++;
}
}
*/
// 找到类UnityEditor.ConsoleWindow中的成员m_ActiveText
var field_active_text = type_console_window.GetField(“m_ActiveText”, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (field_active_text == null) return null;
// 获得m_ActiveText的值,就是我们需要的stacktrace
string value_active_text = field_active_text.GetValue(instance_console_window).ToString();
return value_active_text;
}
return null;
}
#endif
这部分代码就是实现我们需要功能的全部代码,这里面会存在一下问题,因为我们获取stacktrace是通过反射从UnityEditor中的ConsoleWindow类获取的,一旦unity更新了相关的内容话,可能会使现在的功能失效,并且因为是反射,并不会有任何编译错误提示,只能通过后续调试解决,还需要了解ConsoleWindow的实现细节,这个可以通过反编译来实现。
最后在你自定义的log函数上加上[DebuggerNonUserCode][DebuggerStepThrough]两个属性,在之后的代码调试中,调试器也不会进入相关的代码,就更加接近原生的log了。这两个属性在System.Diagnostics中,需要using System.Diagnostics;