在我自己的编码工作中,调bug的时间至少占总时间的50%。
有些时候修复一个的bug甚至会占用几天的时间,最后发现问题出在几天没加条件预防的语句,这太让人恼火了。
我总是在苦恼,为何我的程序有这么多闪退。
为什么就不能有一个安全的编程环境给我们,让我们这些菜鸟写的代码不闪退。
代码世界中危机丛生,程序跑着跑着就要歇菜。
随着发际线的慢慢后退,我开始认清一个令人感觉残酷的现实:
这是你的代码,你需要为其付所有责任,包括最基础的安全性检查。
慢慢的我们需要变得保守,不能轻易相信所有的数据,需要在使用前对其检查。
防御式编程
原则:就是对来源数据持怀疑、不信任的态度
对调用者含有敌意,他们可能会放入各种奇怪的未经检查的数据。
其中最常用的就是null的判断。
在面向对象的编程中,任何一个对象都初始为null(final和 static关键字声明的变量除外)
当然,在一般情况下,变量会在使用前被赋值。
当调用null对象的方法时,会抛出NullPointerException。
如果没有及时对Exception进行捕获,程序就会报错。
处理方法
这里有常用的两种处理方法,两者相互合作,相辅相成。
变量的预先检查
1 | void func(Model arg) { |
如果变量不符合要求,后续代码无法继续执行。
可以改成如下方式:
1 | void func(Model arg) { |
异常后处理
1 | void func(Model arg) { |
合理的使用Exception,能帮助我们更好的代码分层。
Exception它可以报错误上报给调用链的上层。
每个函数都有他的职责范围和处理错误的范围,底层函数不需要处理所有的Exception,他可以将自己无法处理的Exception交给上级。
效率
有人会有疑问:每个函数进入处都进行合理性检查,会不会特别降低效率。
对于一般的应用代码而言,这里的开销微乎其微,不用特别在意。
现有编译器都会帮用户做性能上优化。
如果对于底层或SDK的代码来说,还是需要注意下。
每次都检查,烦
有人表示:我倒是不纠结与性能,但是在一个深度调用链,每个函数都要写一遍参数检查,又繁琐又影响阅读。
我之前也一直在纠结这个问题,觉得很难控制这个度。后来在看Android源码,终于有点想明白了,可以靠函数名传递一些有用信息。(源码真是博大精深,里面有各种精巧的函数设计)
这是在ActivityManagerService启动Activity过程中,调用各种startActivityXXX函数。
下面简单罗列下
1 | startActivityAsUser() |
这里可以发现,函数中名有Checked和Locked。
checked表示已经检查过了各种权限,Locked表示处于线程安全的情况下。
如果我们需要写几个深度调用函数时,某些可以使用checked字样,就不用重复的做参数检查。
final关键字
对于某些赋值后内容不会改变的变量(读上去有点拗口),可以加上final关键字。
不用每次检查是否为null,直接去检查其构造函数,看看其中没有null检查。
IDE的warning
发展到如今,IDE已经上warning提醒已经做得非常完善。
有时候因为做得过于完善,所以写一上午的代码,IDE啪啪啪列出十几条warning,大家反正懒得去看。就像每个香烟上都写着“吸烟有害健康”,也没见烟民们戒烟不抽。
warning还是有用的,大家要理解IDE作者的良苦用心。
最好是写好小一段代码,就去看看新增的warning,能过滤到一些低级的错误。
断言
有人会说,这些安全性检查和处理,包括一些理论上不应该出现的情况。
本来出现这些情况,可以在开发过程中及时的闪退,在上线前修复。
别担心,语言开发者早就想到了这个问题,祭出了大杀器Assert断言。
而且现在的编译器,在release环境下会去掉断言,真是贴心。
工具检查
有时候难免有疏落,这个时候就需要lint去检查下。
在C/C++程序中,可以使用PC-lint,并且打开Pointer-parameter-may-be-NULL这个开关(+fpn)。选项假设所有传递到函数中的指针都有可能是NULL。
重大错误
如果当程序运行时出现重大错误,核心功能出现问题,程序还是会闪退的。
不要企图靠以上各种方法去续1s命。
这个时候就需要平时好好做好单元测试,以及好好对待测试人员,希望他们发现尽可能多的bug。
总结下,这篇文章核心就是
“对象使用前,要检查下其合理性”。
当然检查的力度,是一个仁者见仁智者见智的问题。
一千个人读者有一千个哈姆雷特,有代码洁癖者,也有收放自如的高手。
这里面没有谁的策略更优。
对自己来说,写的顺手bug少,最关键。