如何合理地制造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》中“防护式编程”一章。这里简单的作了一点摘录,归纳其大意:函数
用错误处理代码来处理预期会发生的情况,用断言来处理毫不应该发生的情况。spa
避免把须要执行的代码放到断言中线程
用断言来注解并验证前条件和后条件
对于高健壮性的代码,应该先使用断言再处理错误
对来源于内部系统的可靠的数据使用断言,而不要对外部不可靠的数据使用断言,对于外部不可靠数据,应该使用错误处理代码。 而在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的因果性联系。
访问了一个已经被释放的对象,好比:NSObject * aObj = [[NSObject alloc] init]; [aObj release]; NSLog(@”%@”, aObj);
访问数组类对象越界或插入了空对象
访问了不存在的方法
字节对齐,(类型转换错误)
堆栈溢出
多线程并发操做
Repeating NSTimer
合理性假设第三条:尽量的查找就有可能性的具体缘由。
因果性分析
首先必须先说明的是,咱们要找的是“因果性”而不是“相关性“。这是两个极度被混淆的概念。并且,不少时候咱们错误的把相关性当成了因果性。好比,在解决一个多线程问题的时候,发现了一个数据混乱的问题,可是百思不得其解。终于,有一天你意外的给某个对象加了个锁,数据就正常了。而后你就说这个问题是这个对象没有枷锁致使的。
可是,根据上述你的分析,只可以得出该对象枷锁与否与数据异常有关系,而不能得出就是数据异常的缘由。由于你没能证实对象加锁是数据异常的充分必要条件,而只是使用了一个单因变量实验,变量是枷锁状态,取值x=[0,1],x为整形。而后实验结果是枷锁与否与数据异常呈现正相关性。
相关性:在几率论和统计学中,相关(Correlation,或称相关系数或关联系数),显示两个随机变量之间线性关系的强度和方向。在统计学中,相关的意义是用来衡量两个变量相对于其相互独立的距离。在这个广义的定义下,有许多根据数据特色而定义的用来衡量数据相关的系数。
因果性:因果是一个事件(即“因”)和第二个事件(即“果”)之间的关系,其中后一事件被认为是前一事件的结果。 错误的把相关性等价于因果性。不止是程序员,几乎全部人常见的逻辑错误。为了加深认识,能够看一下这篇小科普:相关性 ≠ 因果性。
因果性分析的首要问题就是,别被本身的逻辑错误欺骗,正确的分辨出相关性和因果性之间的区别。不要把相关性等价于因果性。
以后即是因果性分析的内容了,以前一直反复说,因果性分析的目的就是肯定特定缘由是BUG发生的充分必要条件。那么肯定这个事情,就须要两步:
充分性证实
必要性证实
关于充分性证实,这个基本上就是正常的逻辑推理。基本思路就是,可以还原出BUG出现的路径,从缘由到BUG发生处的代码,走了怎样的函数调用和控制逻辑。肯定了这个基本上就可以证实充分性。通常状况下根据Crash的堆栈信息可以,很是直接的证实充分性。
关于必要性证实,这个就比较困难了。充分性和必要性的定义以下:当命题“若A则B”为真时,A称为B的充分条件,B称为A的必要条件。那么必要性就是,BUG可以做为致使BUG的缘由的缘由。这个说法比较拗口。换种说法,就是你得确认这个BUG可以解释缘由,这个BUG就是并且只是这个缘由形成的。
只有证实了充分必要性,才能算是真正找到了BUG的缘由。