今天开发过程中遇到一个概率性发生的bug,时而出现时而消失,而且在测试工程中无法重现,即使在实际的开发工程中也无法稳定重现。

 

首先说一下bug的背景:开发界面时需要一个带有提示文本的TextBox,即在TextBoxText属性为空时,显示提示文本,比如经常见到的登录界面的用户名文本框,在没有输入任何文本时提示用户“请输入用户名...”这样的提示文本,姑且将其这个文本框命名为TipTextBox,该类从TextBox继承,TipTextBox逻辑比较简单,就是重写TextBoxTemplate,在TextBoxTemplate中加入了一个TextBlock用来显示提示文本,这个TextBlockTipTextBoxText属性不为空的情况下会被隐藏掉,这就需要监听TipTextBoxText属性,一旦该属性发生变化,就判断Text属性是否为空,如果不为空,就将TextBlock隐藏;在Silverlight中不直接支持对依赖属性的监听机制,因此就写了一个帮助类,姑且将其命名为PropertyChangeListener,这个类的原理也非常简单,首先这个类从DependencyObject继承,定义名为Value的依赖属性,将这个属性和你需要监听的控件的某一个依赖属性进行绑定,当要监听的控件的依赖属性发生变化时,我们的Value属性就会随之变化,Value属性的PropertyChangeCallBack这个回调就会被执行,由此也就监听到了要监听的那个控件的依赖属性的变化。

 

但是在运行工程以后,发现当TipTextBoxText属性变化以后,原本期望的Value属性的PropertyChangeCallBack有时竟然得不到调用,但是无法在测试工程中重现这个bug,也无法在实际开发工程中稳定的重现这个bug。反复检查代码,发现代码的逻辑没有问题;经过排查,发现极有可能是PropertyChangeListener创建的对象被垃圾回收导致的,因为PropertyChangeListener是在TipTextBox的构造函数中定义的局部变量,用来监听控件的依赖属性的变化;为了验证这个想法,在PropertyChangeListenerFinalize方法中打印调试信息(在Silverlight中是使用Debug.WriteLine方法),这样每次都观察PropertyChangeListener这个类如果执行了垃圾回收的话,bug是否会出现;运行以后发现,只要在VSOutput窗口看到了打印的调试信息,bug都会出现,而看不到打印的调试信息的话,bug就不会出现,在这里基本就可以断定是PropertyChangeListener被定义为局部变量,被垃圾回收以后这个类的Value属性的PropertyChangeCallBack不会被调用所引起的;为了进一步验证猜想,下一步就是稳定的重现这个bug,为了保证这个bug每次都会出现,就需要强制执行垃圾收集,具体做法如下,在每次TipTextBoxLoad事件中强制执行垃圾收集,之所以在这个点强制执行垃圾收集是因为PropertyChangeListener是在TipTextBox的构造函数中定义的,到TipTextBox开始Load时,PropertyChangeListener已经没有其他对象进行引用,从而保证这个类创建的对象可以被垃圾回收,再次运行证明了我们的猜想,现在可以稳定的重现这个bug,每次刷新界面以后,这个bug也都会出现,到这里就可以断定是bugPropertyChangeListener创建的对象被垃圾回收导致的;像这样的bug在测试工程极难重现,因为测试工程中创建的对象比较少,执行垃圾收集的次数非常少,或者在测试那么短时间内根本就不会执行垃圾收集,导致无法重现bug,而在实际的项目工程下,这个bug也是时而出现,时而消失,无法找到bug出现的稳定的规律,因为垃圾收集的执行时机是不确定的;发现这个bug以后,解决办法也非常简单,将PropertyChangeListener定义为TipTextBox的成员变量,这样在TipTextBox的生存期内PropertyChangeListener这个类创建的对象都不会被垃圾收集