文章目录

最近一个月,一直处于一种调试状态。郁闷,困顿,愉悦,兴奋各种感受兼而有之。如果说编程有什么乐趣的话就是那种解决问题后的成就感。现在作个总结,让自己重新体会编程中的酸甜苦辣。好,故事是这样子的:

前因:公司产品的一个版本要 release了,到最后关头QC发现一个非功能性问题,有Memory Leak(注:内存泄漏,因为很多地方用到这个词,为方便计,简写为ML)问题,而且比较严重。然后定位到一个子系统。我先对该子系统的历史做简单的说明:该系统最先是由一个同事开发的,后来他被调到别的部门,另一个兄弟(为方便计简称L)接手之后,又增加了许多新的功能。该系统的表现形式既是一个WIN32 Service又是一个Component,提供接口供ASP 页面调用,使用VB Script作为脚本语言。因为时间比较紧,而且L对C++不是很熟,俺当时刚好已经完成了自己的系统,于是由我加入和L一起来解决ML问题。这里其实已经有一些Pair Programming和Refactoring的味道。我自己感觉PP的好处是可以给两个人都提精神,保持清醒的头脑,我想大家都有过一个人孤军奋战BUG的经历吧,有时候真的恨不得砸显示器。同时两个人的思路肯定比一个人要开阔的多。在实际的调试过程中我也确实闻到了很多Bad Smell。

在开始之前,我要说一句话:如果一个程序员能有好的编程习惯,那么不只是对他自己,对后来者都是多么愉快的事情。对公司而言,也节约了许多不必要的成本和时间。

后果:我会分几部分说一下我在此中得到的感悟和教训

  1. 该系统的表现形式是Service,注定了我们堕入痛苦的调试深渊。
    不明白此意的同志可以看看<>如何编写及调试Service程序。好在我们主要解决的是Memory Leak的问题,不会牵扯到太多的逻辑。当然后面说到,很多ML都是由于逻辑不清引起的。既然测试的是ML问题,而又不能定位到哪个功能块,所以当时的做法是进行代码隔离,独立的进行测试。同时通过加大测试粒度,即对模块做大容量的测试来尽快的暴露问题。因为最终的表现形式的WEB页面,所以我们通过Rational Robot来做自动测试。问题也很快的暴露出来。

总结:

  • 对Memory Leak,使用压力测试尽快的暴露问题点。
  • 如果不确定问题点,采用代码隔离的方法,逐步逼近。

PS:顺便赞一下ATL,太棒了,一个Service框架只要使用Wizard就搞定。同时为了定时检测系统内存的变化,我自己写了一个查看某一进程内存信息的小工具。这个小工具在我们的调试中起了较大的作用。因为ML问题不是很容易表现出来的,需要长时间的监测。所以在下班后自动获取内存变化的值就很有意义了。

  1. 然而痛苦的根源是代码的风格和设计问题。
    该子系统有数W行代码,40多个类,20多个COM接口。首先是类:其中一个类有30个方法,50个数据成员。真的是头大了。我粗略看了一下该类,它承担了太多的责任,唉,可怜的SRP. 其次是函数:典型的C语言风格,很多函数都有2~300行,变量的声明全在头部,知道Lazy Evalution的同志应该知道这样的坏处,而且变量在头部,代码又长,总是要跑到头部去看变量的意义。后来碰到要修改的代码,我都尽量在声明时定义或需要时才定义。我无奈的对L说,看来他是生活在C语言时代的人。抛开一些逻辑问题,我们最后发现ML一个很大的根源是在对一个函数调用时的契约不清晰,此函数分配的内存应该由外部释放,但调用它的地方都没有遵守该契约。而且该函数的实现也没有很好的表现它所要表达的语义。而该函数在整个系统中是很基础的代码,大概有数百个地方对该函数有调用。虽然我们知道对任何一个地方的修改都可能引发新的问题,但根已经烂了,没办法,只有谨慎行事,对该函数的语义清晰的了解后进行了Refactor,编译,运行。Refactoring不可避免的引入新的BUG,幸好是小BUG,我们及时的解决了。经过修改后,新增的功能的逻辑和ML问题基本解决,而且功能上没有什么问题。但到此时已经花去我们3个星期的时间。

总结:

  • 修改代码之前先Check Out;
  • 对代码的修改都要做全面的测试
  • 好的编码习惯是非常重要的。
  1. 在解决了内部的问题后,我们自己写了一个小的测试工具,发现情况良好,终于看到天亮前的一丝光明。
    然而黎明前通常是最黑暗的。我们自己测试无误的问题,在给ASP调用时却又遇到了ML问题。没办法,只好把ASP的调用拿来研究,问题的根源还是调用契约不清晰。内部分配的内存,ASP获得接口后,没有适当的释放。

  2. 最后的BUG?
    在我们欣喜若狂的时候,又传来了Memory Leak的声音,但对该函数分析数十遍,可以完全肯定该函数没问题的时候,几乎累的要放弃了。因为明明看不出BUG来,可就是有Leak,真的欲哭无泪了。最后没办法,找另一个同事帮我们看,因为他直接从该函数的类的构造看起的,所以一下子发现问题了,原来是在构造里面赋初值的时候使用了一个动态分配内存的函数,而调用该函数后没有释放内存导致的。

总结 : 总的来说,

  • Memory Leak出现的大部分地方是由于程序员的粗心大意,而导致粗心大意的根由是由于一个类或函数实现了太多的功能。同时也说明Unit Test的重要性。
  • 注释不明,导致理解偏差。
  • 函数,接口之间的调用契约不清晰。重要的函数没有注释着重的说明。

最后,作为我的自我安慰,我在VC的post-bulid step里面写下如下说明:命苦不怨政府。编程路漫漫修远,吾将上下而捉虫。

文章目录