C#调试Debug/Release入门篇
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
DotNet程序的调试,是DotNet程序员必备的技能之一,开发出稳定的程序、解决程序的疑难杂症都需要很强大的调试能力。DotNet调试有很多方法和技巧。现在本文就介绍一下借助DebugView工具进行调试的方法,以及由DebugView引申出来的知识点。 DebugViewDebugView是一个查看调试信息的非常棒的工具,支持Debug、Release模式编译的程序,甚至支持内核程序,而且能够定制各种过滤条件,让你只看到关心的输出信息,而且可以定制高亮显示的内容等等,非常方便。 捕捉Release模式的Win32程序输出的调试信息,需要选中Capture Global Win32选项: 过滤与高亮功能可以通过include、exclude设置过滤条件,包含指定字符串的输出信息将会被过滤。还可以通过exclude条件过滤掉对应进程ID的调试信息。多个条件使用“;”分隔,而且支持“*”通配符。 远程调试DebugView支持远程捕捉调试信息。首先在远程机器上通过如下命令启动DebugView: DebugView.exe /a /t /g /s 这样,DebugView就会以服务的方式运行,如下图: 然后在本地机器上启动DebugView,并通过Connect连接到远程机器的DebugView,当远程机器中有调试信息输出时,本地就会捕获到,并展示出来: 输出信息到DebugView的几种方式DebugView的一些功能是不是让你心动了呢。俗话说心动不如行动,但是在行动之前,首先要知道C#如何将调试信息输出到DebugView中。 通过编程输出一些调试信息到DebugView中,一共有三种方式:
一、Debug.WriteLine通过Debug.WriteLine可以将调试信息写入到DebugView中,如下: Debug.WriteLine("这是调试信息"); 效果如下: 不过此方式只能在Debug模式下有效。具体原因下面会详细介绍。 二、Debugger.LogDebug.WriteLine已经很好用了,唯一的缺点就是在Release模式下无效。那么在Release模式下就可以使用Debugger.Log方法,示例如下: Debugger.Log(0, null, "这是Debugger.Log输出的调试信息"); 三、Kernel32.dll中的OutputDebugString方法做C++开发的应该知道可以通过OutputDebugString这个API开实现输出调试信息到DebugView中吧。那么C++能做的,C#也能做。可以通过PInvoke的方式引入此方法,这个API属于Kernel32.dll,如下声明: [DllImport("kernel32.dll", CharSet=CharSet.Auto)]public static extern void OutputDebugString(string message); 然后就可以通过调用此方法,将调试信息输出到DebugView中。 DebugView与日志框架比较可能有人会说,DebugView能做的事情,我用log4Net,NLog等日志框架也能做,为什么要用DebugView呢? 问的好,那么我就根据平时使用DebugView的经验来给你一个用DebugView的理由:
这些理由应该足以让你使用DebugView了吧。使用DebugView的理由肯定还不止这些,如果你有更好的理由,还请分享出来。 当然,DebugView与日志框架,每个都有每个的用途。通过DebugView的方式,只适合短暂的调试,而正式发布的网站或者软件,需要一套记录程序长期以来的运行状态的工具,那么就非日志框架莫属了。所以DebugView与日志框架,要在合适的地方,发挥他们最大的功效。 声明Log4Net等日志框架,功能足够强大,也足够丰富,相信上面说到的DebugView的功能,也可以通过日志框架来实现。但是和DebugView比较起来,会相对复杂一些。所以上面说到的使用DebugView的理由是基于方便性的比较,DebugView有足够的方便性来让你选择使用他。 ConditionalAttribute详解与条件编译说到调试,那么肯定有开发人员遇到这种情况,开发产品的时候,遇到一些问题,就在代码中加入了大量的调试输出信息,比如通过Console.WriteLine、MessageBox.Show或者通过Ilog.Log记录日志,甚至临时改变逻辑来验证逻辑的正确性等。经过这些调试信息的帮助,终于解决了产品的问题。但此时又遇到了新的问题,产品最终发布的时候,肯定是不能有这些调试信息的,可是已经加了这么多调试信息,难道要全部删除掉吗。这显然不是一个好办法,这么多代码,手一抖,很容易就删除了不相关的代码,造成不可预估的后果。 做过C/C++开发的,可以从各种跨平台的开源库中看到,一堆一堆的#if....#else....#endif,这就是条件编译,这也是C/C++跨平台所依赖的最基本的东西,在Linux系统,编译这段代码,在Windows系统又编译那段代码,最终实现了代码级别的跨平台。 那么C#中有没有类似的功能呢,答案当然是有,而且有两种:
ConditionalAttribute特性下面是ConditionalAttribute的构造函数: public ConditionalAttribute(string conditionString 构造函数中的参数conditionString,是一个区分大小写的条件编译符号的字符串。 上面提到Debug.WriteLine时,说到这个功能只在Debug模式下才有用,Release下就不起作用了。 我们从MSDN中看一下Debug.WriteLine的说明: [ConditionalAttribute("DEBUG")]public static void WriteLine(string message 由此也就明白了Debug.WriteLine只能在Debug模式下使用,而在Release模式下无效的原因了。 条件编译#if..#else..#endifC/C++中有#if..#else..#endif,C#中也有这些,他们都被称为预处理器。通过预定义的条件编译符号,来控制编译时编译哪部分代码。如下: public bool HasPermission(string userName) { #if DEBUG //Debug模式下,不需要做权限判断,直接返回true return true; #else //Release模式下,只有sa用户才有权限 if (!string.IsNullOrEmpty(userName) && userName == "sa") { return true; } else { return false; } #endif } 预定义的Debug宏在什么地方说到条件编译,是不是只有DEBUG 和 RELEASE两种情况呢,如果是这种情况的话,那也就是说DEBUG和RELEASE两种情况是定义好了的,俗话说就是“做死了”,这是作死的节奏啊。不作死就不会死,至少VS在这点上还没有作死。 让我们来一步步揭开DEBUG的面纱。 既然是条件编译,那么就应该和编译选项有关。我们知道C#项目,有一个属性页,可以设置很多编译的选项,如下: 从图中看到,条件编译是用的DEBUG常量,或者称为DEBUG条件编译符号,是在这个编译生成选项中定义的,如果去掉这个定义,那么编译后的HasPermission方法就会根据用户名进行权限检查,程序中通过Debug.WriteLine输出的调试信息也会输出到DebugView中,也就相当于Release模式下的效果。 其实DEBUG常量与Debug、Release模式并无太大的关系,唯一的关系就是,VS生成的项目中,Debug模式下,默认会选中“定义DEBUG常量”,而Release模式下,默认不会选中。也就是说,Debug模式,默认会定义DEBUG常量,而Release不会,如下图: 既然DEBUG常量与Debug模式无本质上的关联,那么为什么说到Debug,就认为DEBUG呢。道理其实很简单,世上本无路,走的人多了,便成了路。本来这个DEBUG常量只是Debug模式下的默认预定义的常量,只是因为大家习惯了,并且对它的这种预定义还比较认可,时间久了,就自然而然认为DEBUG就代表Debug模式。 虽然我们可以通过去掉DEBUG常量,来使条件编译在Debug模式下达到Release模式的效果,但是建议最好不要这样做,因为这就像是大家普遍都认可的一个约定,如果你一反常态,不遵守这个约定,对于程序,编译没有问题,但是后续维护肯定会相当麻烦,所以还请大侠手下留情。 使用自定义的编译常量DEBUG常量作为一种普遍的约定,最好不要打破。如果有除DEBUG外的条件编译需要,可以使用自定义的编译常量。 自定义编译常量有两种方法:
我们可以在条件编译的输入框中,定义自己的编译常量,多个常量之间用分号“;”隔开,而且支持中文常量,如下: 当然,我们也可以在代码中通过#define预处理来定义,比如: #define 缘生梦#define hbccdf 但是有一点需要注意,define定义常量必须在using 命名空间之前,否则会造成编译错误。 通过VS和Resharper改变颜色来查看哪些代码真正会被编译引入条件编译后,我们可以通过VS很快知道哪些代码会被编译: 虽然条件编译的代码可以很直观的看出来,但是Conditional修饰的方法就看不出来了,这时就要借助神器Resharper了,如下图: 从图中看出,release模式,由于没有定义DEBUG、缘生梦两个常量,所以,调用Test、Debug.WriteLine方法的地方就会变暗,这样很直观就知道这些代码不会被编译。再次说明一个道理,神器对于开发有很大的帮助作用。 通过反编译查看生成后的代码上面总是说,有些代码会被编译,有的则不会,那么真正编译后的效果是怎样的,我们不妨使用另外一个比较强大的工具,反编译工具Reflector,来查看一下反编译后的代码: 从图中的反编译后的代码可以看出,满足条件的代码会真正编译到生成的程序集里,而不满足的代码则不会生成到程序集里。 Conditional修饰的方法,会有方法的实现,但是没有方法的调用。不适合在方法里做一些变量的修改。 总结Conditional和条件编译
TRACE常量从上面多幅图中,可以看到,在Debug和Release模式下都会定义一个TRACE常量。 现在我们知道DEBUG常量是用来控制调用Debug.WriteLine的语句是否被编译。那么TRACE常量呢。 相信很多人也用过System.Diagnostics.Trace类,由DEBUG常量与Debug有关,可以想到TRACE常量与Trace有关,我们来看一下MSDN对于Trace的定义。 从MSDN中,看到Trace类的定义以及可以调用的方法与Debug类似,都有WriteLine方法。下面是Trace的WriteLine方法定义: [ConditionalAttribute("TRACE")]public static void WriteLine(string message 由此可以知道,TRACE常量是用来控制Trace类中WriteLine等方法是否被编译的作用。 Debug类与Trace类的区别到现在为止,我们渐渐的了解了Debug类与Trace类,他们都可以通过WriteLine方法输出调试或跟踪信息,从他们的定义和暴露的方法中,可以看出他们非常相似,那么他们有什么区别呢。 有些人会根据使用的效果总结为:Debug类只能在Debug模式下执行,在Release模式下无效果,而Trace类在Debug和Release模式下都可以执行。 确实,在VS中新建一个项目,分别调用Debug.WriteLine和Trace.WriteLine方法,只有Trace.WriteLine输出的信息,在Release模式下有效。这也看似验证了上面的区别。 但这是他们两个真正的区别吗。我们一点一点来分析。 首先看这两个方法的定义: 由图看到,每个方法都通过ConditionalAttribute关联了一个条件编译常量,Debug关联DEBUG常量,Trace关联TRACE常量。再来看一下这两个常量的定义: 从图中看到,TRACE在Debug和Release模式下都会定义,这样在Debug和Release模式下都会执行,而DEBUG只在Debug模式下才会定义,Debug.WriteLine只在Debug模式下执行。从而也验证了上面的结论。 但DEBUG与TRACE只是在默认的情况下的定义。当改变这些定义后,上面的结论就不再正确。 我们来做这样的实验:Debug模式下只定义TRACE常量,Release模式只定义DEBUG常量 然后在Debug和Release模式下分别执行Debug.WriteLine方法和Trace.WriteLine方法,Debug.WriteLine方法只在Release模式下有效,而Trace.WriteLine方法只在Debug模式下有效。上面的结论不再成立。 为了更好的验证一下我们的结论,对他们进行反编译: 由图中的反编译代码看到,除了关联的条件编译常量不同外,内部调用的方法均为TraceInternal.WriteLine,实现完全一样。 那么下面来总结一下Debug与Trace的区别:
Debug、Debugger、Kernel32的联系每个人大脑的空间都是有限的,零散的知识很容易忘掉,输出调试信息到DebugView中的三种方法,由于关联性不是很强,很容易会忘掉其中的一两种,那么既然实现相同的功能,他们之间有什么关联吗。 这就要从Debug的MSDN文档说起。 我们知道Debug编译的程序,运行的时候可以通过Debug.WriteLine方法输出调试i信息到DebugView,而MSDN中的解释 将后跟行结束符的消息写入 Listeners 集合中的跟踪侦听器。 并没有说是输出到DebugView中,而是写入到Listeners集合中的跟踪侦听器。为了弄明白原理,有必要深入的研究一下。这时就要依赖反编译工具Reflector了。 从整体看上去,Debug类的每一个方法都通过Conditional与DEBUG常量管理,也就是默认情况下,Debug类的所有方法在Debug模式下均不会编译。 我们再来具体看一下Debug.WriteLine方法: [Conditional("DEBUG"), __DynamicallyInvokable]public static void WriteLine(string message) 再来看一下TraceInternal方法: public static void WriteLine(string message) { foreach (TraceListener listener in Listeners) { listener.WriteLine(message); if (AutoFlush) { listener.Flush(); } } return; } 上面的代码是精简后的代码。从代码中看到会调用集合中的每一个listener的WriteLine方法。 那么这些listener的WriteLine又做了什么呢: public abstract class TraceListener : MarshalByRefObject, IDisposable 原来TraceListener的WriteLine是抽象类的抽象方法,那么我们得到的listener是具体类的一个抽象,相当于接口,这是微软的一贯做法,再继续下去,就需要知道是哪些具体的TraceListener了。 继续上面的线索,我们是从Listeners属性中获取到的TraceListener,那么就去看Listeners的get实现: public static TraceListenerCollection Listeners { get { InitializeSettings(); if (listeners == null) { lock (critSec) { if (listeners == null) { SystemDiagnosticsSection systemDiagnosticsSection = DiagnosticsConfiguration.SystemDiagnosticsSection; if (systemDiagnosticsSection != null) { //从配置文件获取listener,但是由于此处没有设置配置文件,所以先不研究这个地方 listeners = systemDiagnosticsSection.Trace.Listeners.GetRuntimeObject(); } else { //这里new了一个TraceListener的集合 listeners = new TraceListenerCollection(); //这里我们看到了TraceListener的具体实现类DefaultTraceListener TraceListener listener = new DefaultTraceListener { IndentLevel = indentLevel, IndentSize = indentSize }; listeners.Add(listener); } } } } return listeners; } } 从代码中,找到了具体的实现类DefaultTraceListener,那么就快点看看他的WriteLine方法吧,有点迫不及待了。 实际上,WriteLine方法会调用内部的Write方法: private void Write(string message, bool useLogFile) { if (base.NeedIndent) { //写缩进,实际是空格 this.WriteIndent(); } if ((message == null) || (message.Length <= 16384)) { //输出消息 this.internalWrite(message); } else { //当消息很长时,会通过internalWrite方法将消息多次输出 int startIndex = 0; while (startIndex < (message.Length - 16384)) { this.internalWrite(message.Substring(startIndex, 16384)); startIndex += 16384; } this.internalWrite(message.Substring(startIndex)); } if (useLogFile && (this.LogFileName.Length != 0)) { //输出到日志文件中 this.WriteToLogFile(message, false); } } 与输出信息有关的有两个地方,一个是调用internalWrite,另外一个是WriteToLogFile。从WriteToLogFile是有执行条件的。感兴趣的可以研究一下。重点来看一下internalWrite方法。 private void internalWrite(string message) { if (Debugger.IsLogging()) { //调用Debugger.Log方法,这个方法可以输出信息到DebugView中 Debugger.Log(0, null, message); } else if (message == null) { SafeNativeMethods.OutputDebugString(string.Empty); } else { //调用Native方法 SafeNativeMethods.OutputDebugString(message); } } internalWrite中调用了两个比较重要的方法。其中Debugger.Log方法是不是很熟悉呢。我们刚刚在上面总结了三种输出调试信息到DebugView的方法,其中就包含了Debugger.Log,而Debug.WriteLine方法中,又会调用到Debugger.Log方法,这样,这两个方法就建立起了联系。 再来看SafeNativeMethods.OutputDebugString,看到这个类的命名,以及对Win32API的了解,就能想到,这肯定是通过PInvoke等方法对Win32API的封装,看其定义: [DllImport("kernel32.dll", CharSet=CharSet.Auto)]public static extern void OutputDebugString(string message); 果然,这就是对kernel32.dll中的OutputDebugString的封装调用,而这又是上面三种方法其中的一种。好了,这样Debug、Debugger、OutputDebugString就全都联系到了一起,他们之间有了联系,是不是就更容易记忆了。 配置TraceListener从上面对Debug.WriteLine的一步步跟踪分析的过程中,我们看到了对于TraceListener的获取,一种方式是DefaultTraceListener,另外一种就是如下: SystemDiagnosticsSection systemDiagnosticsSection = DiagnosticsConfiguration.SystemDiagnosticsSection; if (systemDiagnosticsSection != null) { //从配置文件获取listener,但是由于此处没有设置配置文件,所以先不研究这个地方 listeners = systemDiagnosticsSection.Trace.Listeners.GetRuntimeObject(); } 这几行代码就是从配置文件的system.diagnostics中获取TraceListener。 关于配置文件具体的读取和解析过程,本文就不再详细介绍了,感兴趣的朋友可以自行研究,或者等到我的后面博文详细介绍。 那么现在主要说一下如何通过配置文件来设置TraceListener,如下: <configuration> <system.diagnostics> <trace autoflush="true" indentsize="2"> <listeners> <add name="myListener" type="System.Diagnostics.ConsoleTraceListener" /> </listeners> </trace> </system.diagnostics> </configuration> 同样,有配置文件,那么就可以通过代码来实现同样的功能,如下: Debug.Listeners.Add(new ConsoleTraceListener());Debug.AutoFlush = true;Debug.WriteLine("这是Debug.WriteLine输出的调试信息"); Debug编译与Release编译的区别Debug与Release的区别,这个问题,相信很多人都会有疑问,也会有很多人有自己的答案,我听到过的答案有这些:
这些答案看上去好像都对,但是为什么Debug与Release有这么大的区别呢,这就需要深入的思考一下。 在Debug类与Trace类的区别一节中,相信有些朋友已经明白,Debug编译与Release编译的区别其实是编译配置的不同,DEBUG、TRACE常量的定义就是其中不同的地方。那么Debug编译与Release编译还有什么不同呢。针对这个问题,我总结了一下,主要区别有下面几点:
接下来,我们详细的看一下。 常量定义不同这个相信大家已经非常清楚了,Debug模式下会定义DEBUG、TRACE常量,而Release模式下只定义TRACE常量,如下图所示 优化代码不同Debug模式下,默认不会进行代码的优化,而Release模式下,由于默认选中了“优化代码”选项,所以编译器会对生成的代码进行优化,以提高运行速度。 包含的调试信息不同编译生成的属性页中有一个“高级”按钮,点击后会弹出一个对话框,然后可以对编译生成进行一些设置,如下 Debug与Release的高级选项对比: 从图中看到,Debug模式下默认会生成全部的调试信息,而Release模式下只生成pdb文件。 输出路径不同这一点详细大家都很清楚了,Debug模式下会输出到Debug目录,Release模式下会输出到Release目录,如图: Debug编译与Release编译的区别总结说了这么多,我们再来思考本节开始时说到的关于Debug编译与Release编译的区别的答案,可以得出这样的结论:
知道这些区别后,我们就可以通过修改编译配置来使Debug程序达到Release的效果,使Release程序达到Debug的效果。当然,还是那句话,虽然可以实现这样的效果,但是建议绝对不要这样做,不然程序维护起来,肯定会遇到很多问题。 总结的重要性有人说,互联网这么发达,遇到什么问题直接谷歌百度,内事不决问百度,外事不决问谷歌。但是即使你通过网络解决了问题,如果不消化吸收,经过总结变成自己的东西,那么下次还会遇到同样的问题。书到用时方恨少,一回首,已白了少年头。如果不总结,任他虐我千百遍,我却视他如初见,从网络上找到的方法、代码、解决方案,那是别人的知识,只有经过实践去验证,通过总结,消化吸收,才能将这些知识“据为己有”,为我所用。 那么至少从现在做起,把每天学到的知识,遇到的问题,得到的经验,总结下来,相信自己,这是大牛的节奏。 该文章在 2021/3/8 15:03:17 编辑过 |
关键字查询
相关文章
正在查询... |