什么是BUG,简单点说是,程序没有按照我们预想的方式运行。我比较喜欢把BUG分成两类:
  Crash掉的
  没有Crash掉的

  可能在平时的编程实践中,往往简单的把BUG与Crash基本等价了。而且我们很多精力也都放在解决Crash的Bug上面。而对于没有Crash掉的BUG,似乎没有过多的关注。但是,实际情况上那些让人痛彻心扉的“天坑”往往是那些没有Crash掉的BUG造成的,比如前一段时间OpenSSL心脏大出血。为什么这么说呢?且听我慢慢道来。
  如何合理地制造BUG?
  Crash掉的BUG,用程序的死证明了你的程序存在问题,你必须抓紧时间来解决程序的问题了。而没有Crash掉的Bug,像是一个善于撒谎的人,伪装成可以正常运转的样子,让整个程序运行在一个不稳定的状态下。虽然外表看起来好好地(没有crash),但是里子早烂透了,一旦报露出问题往往是致命的,比如OpenSSL的心脏大出血。这是前人总结的“死程序不说谎”。
  Crash不可怕,可怕的是程序没有Crash而是运行在一个不稳定的状态下,如果程序还操作了数据,那带来的危害将是灾难性的。
  所以放心的让程序Crash掉吧,因为当他Crash掉的时候,你还有机会去修正自己的错误。如果没有Crash,那有可能要给整个程序和产品收尸了。因此合理制造“BUG”的原则之一,也是大的原则是:尽量制造Crash的BUG,减少没有Crash的BUG,如果有可能将没有Crash掉的Bug转换成Crash的BUG以方便查找。
  NSAssert
  这个应该都比较熟悉,他的名字叫做“断言”。断言(assertion)是指在开发期间使用的、让程序在运行时进行自检的代码(通常是一个子程序或宏)。断言为真,则表明程序运行正常,而断言为假,则意味着它已经在代码中发现了意料之外的错误。断言对于大型的复杂程序或可靠性要求极高的程序来说尤其有用。而当断言为假的时候,几乎所有的系统的处理策略都是,让程序死掉,即Crash掉。方便你知道,程序出现了问题。
  断言其实是“防御式编程”的常用的手段。防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。这种思想是将可能出现的错误造成的影响控制在有限的范围内。断言能够有效的保证数据的正确性,防止因为脏数据让整个程序运行在不稳定的状态下面。
  关于如何使用断言,还是参考《代码大全2》中“防御式编程”一章。这里简单的做了一点摘录,概括其大意:
  用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。
  避免把需要执行的代码放到断言中
  用断言来注解并验证前条件和后条件
  对于高健壮性的代码,应该先使用断言再处理错误
  对来源于内部系统的可靠的数据使用断言,而不要对外部不可靠的数据使用断言,对于外部不可靠数据,应该使用错误处理代码。 而在IOS编程中,我们可以使用NSAssert来处理断言。比如:
  - (void)printMyName:(NSString *)myName  
  {  
  NSAssert(myName == nil, @"名字不能为空!");  
  NSLog(@"My name is %@.",myName);  
  }
  我们验证myName的安全性,需要保证其不能为空。NSAssert会检查其内部的表达式的值,如果为假则继续执行程序,如果不为假让程序Crash掉。
  每一个线程都有它自己的断言捕获器(一个NSAssertionHanlder的实例),当断言发生时,捕获器会打印断言信息和当前的类名、方法名等信息。然后抛出一个NSInternalInconsistencyException异常让整个程序Crash掉。并且在当前线程的断言捕获器中执行handleFailureInMethod:object:file:lineNumber:description:以上述信息为输出。
  当时,当程序发布的时候,不能把断言带入安装包,你不想让程序在用户机器上Crash掉吧。打开和关闭断言可以在项目设置中设置assert ,在release版本中设置了NS_BLOCK_ASSERTIONS之后断言失效。
  尽可能不要用Try-Catch
  并不是说Try-Catch这样的异常处理机制不好。而是,很多人在编程中,错误了使用了Try-Catch,把异常处理机制用在了核心逻辑中。把其当成了一个变种的GOTO使用。把大量的逻辑写在了Catch中。弱弱的说一句,这种情况干嘛不用ifelse呢。
  而实际情况是,异常处理只是用户处理软件中出现异常的情况。常用的情况是子程序抛出错误,让上层调用者知道,子程序发生了错误,并让调用者使用合适的策略来处理异常。一般情况下,对于异常的处理策略是Crash,让程序死掉,并且打印出堆栈信息。
  而在IOS编程中,抛出错误的方式,往往采用更直接的方式。如果上层需要知道错误信息,一半会传入一个NSError的指针的指针:
  - (void) doSomething:(NSError* __autoreleasing*)error
  {
  ...
  if(error != NULL)
  {
  *error = [NSError new];
  }
  ....
  }
  而能够留给异常处理的场景极少了,所以在IOS编程中尽量不要使用Try-Catch。
  (PS:见到过使用Try-Catch来防止程序Crash的设计,如果不是迫不得已,尽量不要使用这种策略)
  尽量将没有Crash掉的BUG,让它Crash掉
  上面主要讲的是怎么知道Crash的“BUG”。对于合理的制造“BUG”还有一条是尽量把没有Crash掉的“BUG”,让他Crash掉。这个没有比较靠谱的方法,靠暴力吧。比如写一些数组越界在里面之类的。比如那些难调的多线程BUG,想办法让他Crash掉吧,crash掉查找起来比较方便了。
  总之,是抱着让程序“死掉”的心态去编程,向死而生。
  如何查找BUG?
  其实查找BUG这个说法,有点不太靠谱。因为BUG从来都不需要你去找,他在那里,只增不减。都是BUG来找你,你很少主动去找BUG。程序死了,然后我们得加班加点。其实我们找的是发生BUG的原因。找到引发BUG的罪魁祸首。说的比较理论化一点是:在一堆可能的原因中,找到那些与BUG有因果性的原因(注意,是因果性,不是相关性)。
  于是解决BUG一般可以分两步进行:
  合理性假设,找到可能性高的一系列原因。
  对上面找到的原因与BUG之间的因果性进行分析。必须确定,这个BUG是由某个原因引起的,而且只由改原因引起。即确定特定原因是BUG的充分必要条件。 找到原因之后,剩下的事情比较简单了,改代码解决掉。
  合理性假设
  其实,BUG发生的原因可以分成两类:
  我们自己程序的问题。
  系统环境,包括OS、库、框架等的问题。 前者找到了,我们可以改。后者比较无能为力了,要么发发牢骚,要么email开发商,后能不能被改掉不得而知了。比如IOS制作framework的时候,category会报方法无法找的异常,到现在都没有解决掉。
  当然,一般情况下导致程序出问题的原因的99.999999%都是我们自己造成的。所以合理性假设第一条:
  首先怀疑自己和自己的程序,其次怀疑一切
  而程序的问题,其实是开发者自己的问题。毕竟BUG是程序员的亲子亲孙,我们一手创造了BUG。而之所以能够创造BUG,开发者的原因大致有三:
  知识储备不足,比如IOS常见的空指针问题,发现很多时候是因为对于IOS的内存管理模型不熟悉导致。
  错心大意,比较典型的是数组越界错误。还有在类型转化的时候没注意。比如下面这个程序:
  //array.count = 9
  for (int i = 100; array.count - (unsigned int)i > 10 ; )
  {
  i++
  .....
  }
  按道理讲,这应该是个可以正常执行的程序,但是你运行的话是个死循环。可能死循环的问题,你改了很多天也没解决。直到同事和你说array.count返回的是NSUInterge,当与无符号整形相间的时候,如果出现负值是回越界的啊。你才恍然大悟:靠,类型的问题。
  逻辑错误
  这个是思维方式的问题,但是也是问题严重的。一旦发生,很难查找。人总是难怀疑自己的思维方式。比如死循环的问题,严重的是函数间的循环引用,还有多线程的问题。 但是庆幸的是绝大多数的BUG都是由于知识储备不足和粗心大意造成的。所以合理性假设的第二条:
  首先怀疑基础性的原因,比如自己知识储备和粗心大意等人为因素,通过这些原因查找具体的问题。之后再去怀疑难处理的逻辑错误。 有了上面的合理性怀疑的一些基本策略,也不能缺少一些基本的素材啊。是常见的Crash原因,后我们还是得落地到这些具体的原因或者代码上,却找与BUG的因果性联系。