阅读目录html
DotNet程序的调试,是DotNet程序员必备的技能之一,开发出稳定的程序、解决程序的疑难杂症都须要很强大的调试能力。DotNet调试有不少方法和技巧。如今本文就介绍一下借助DebugView工具进行调试的方法,以及由DebugView引伸出来的知识点。程序员
DebugView是一个查看调试信息的很是棒的工具,支持Debug、Release模式编译的程序,甚至支持内核程序,并且可以定制各类过滤条件,让你只看到关心的输出信息,并且能够定制高亮显示的内容等等,很是方便。编程
捕捉Release模式的Win32程序输出的调试信息,须要选中Capture Global Win32选项:网络
能够经过include、exclude设置过滤条件,包含指定字符串的输出信息将会被过滤。还能够经过exclude条件过滤掉对应进程ID的调试信息。多个条件使用“;”分隔,并且支持“*”通配符。框架
DebugView支持远程捕捉调试信息。首先在远程机器上经过以下命令启动DebugView:函数
DebugView.exe /a /t /g /s
这样,DebugView就会以服务的方式运行,以下图:工具
而后在本地机器上启动DebugView,并经过Connect链接到远程机器的DebugView,当远程机器中有调试信息输出时,本地就会捕获到,并展现出来:post
DebugView的一些功能是否是让你心动了呢。俗话说心动不如行动,可是在行动以前,首先要知道C#如何将调试信息输出到DebugView中。学习
经过编程输出一些调试信息到DebugView中,一共有三种方式:测试
经过Debug.WriteLine能够将调试信息写入到DebugView中,以下:
Debug.WriteLine("这是调试信息");
效果以下:
不过此方式只能在Debug模式下有效。具体缘由下面会详细介绍。
Debug.WriteLine已经很好用了,惟一的缺点就是在Release模式下无效。那么在Release模式下就可使用Debugger.Log方法,示例以下:
Debugger.Log(0, null, "这是Debugger.Log输出的调试信息");
作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能作的事情,我用log4Net,NLog等日志框架也能作,为何要用DebugView呢?
问的好,那么我就根据平时使用DebugView的经验来给你一个用DebugView的理由:
这些理由应该足以让你使用DebugView了吧。使用DebugView的理由确定还不止这些,若是你有更好的理由,还请分享出来。
固然,DebugView与日志框架,每一个都有每一个的用途。经过DebugView的方式,只适合短暂的调试,而正式发布的网站或者软件,须要一套记录程序长期以来的运行状态的工具,那么就非日志框架莫属了。因此DebugView与日志框架,要在合适的地方,发挥他们最大的功效。
Log4Net等日志框架,功能足够强大,也足够丰富,相信上面说到的DebugView的功能,也能够经过日志框架来实现。可是和DebugView比较起来,会相对复杂一些。因此上面说到的使用DebugView的理由是基于方便性的比较,DebugView有足够的方便性来让你选择使用他。
说到调试,那么确定有开发人员遇到这种状况,开发产品的时候,遇到一些问题,就在代码中加入了大量的调试输出信息,好比经过Console.WriteLine、MessageBox.Show或者经过Ilog.Log记录日志,甚至临时改变逻辑来验证逻辑的正确性等。通过这些调试信息的帮助,终于解决了产品的问题。但此时又遇到了新的问题,产品最终发布的时候,确定是不能有这些调试信息的,但是已经加了这么多调试信息,难道要所有删除掉吗。这显然不是一个好办法,这么多代码,手一抖,很容易就删除了不相关的代码,形成不可预估的后果。
作过C/C++开发的,能够从各类跨平台的开源库中看到,一堆一堆的#if....#else....#endif,这就是条件编译,这也是C/C++跨平台所依赖的最基本的东西,在Linux系统,编译这段代码,在Windows系统又编译那段代码,最终实现了代码级别的跨平台。
那么C#中有没有相似的功能呢,答案固然是有,并且有两种:
下面是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模式下无效的缘由了。
C/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 和 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很快知道哪些代码会被编译:
虽然条件编译的代码能够很直观的看出来,可是Conditional修饰的方法就看不出来了,这时就要借助神器Resharper了,以下图:
从图中看出,release模式,因为没有定义DEBUG、缘生梦两个常量,因此,调用Test、Debug.WriteLine方法的地方就会变暗,这样很直观就知道这些代码不会被编译。再次说明一个道理,神器对于开发有很大的帮助做用。
上面老是说,有些代码会被编译,有的则不会,那么真正编译后的效果是怎样的,咱们不妨使用另一个比较强大的工具,反编译工具Reflector,来查看一下反编译后的代码:
从图中的反编译后的代码能够看出,知足条件的代码会真正编译到生成的程序集里,而不知足的代码则不会生成到程序集里。
Conditional修饰的方法,会有方法的实现,可是没有方法的调用。不适合在方法里作一些变量的修改。
从上面多幅图中,能够看到,在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类,他们均可以经过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的区别:
每一个人大脑的空间都是有限的,零散的知识很容易忘掉,输出调试信息到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.WriteLine(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 { public abstract void WriteLine(string message); }
原来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就全都联系到了一块儿,他们之间有了联系,是否是就更容易记忆了。
从上面对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类与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的效果,使Release程序达到Debug的效果。固然,仍是那句话,虽然能够实现这样的效果,可是建议绝对不要这样作,否则程序维护起来,确定会遇到不少问题。
有人说,互联网这么发达,遇到什么问题直接谷歌百度,内事不决问百度,外事不决问谷歌。可是即便你经过网络解决了问题,若是不消化吸取,通过总结变成本身的东西,那么下次还会遇到一样的问题。书到用时方恨少,一回首,已白了少年头。若是不总结,任他虐我千百遍,我却视他如初见,从网络上找到的方法、代码、解决方案,那是别人的知识,只有通过实践去验证,经过总结,消化吸取,才能将这些知识“据为己有”,为我所用。
那么至少从如今作起,把天天学到的知识,遇到的问题,获得的经验,总结下来,相信本身,这是大牛的节奏。