最近IPhone项目中的一个bug困扰了我两天多,我把解决的过程分享出来,便于我自己整理思考问题的方式,也希望其中一些problem solving的方法能够对大家有所借鉴。
现象:
在程序前后导航时(UINavigationController),上一个页面的导航栏会残留,点击其button会响应上一个页面的事件或Crash。
这是在我刚刚Update到ios sdk 4.01时产生的,并且只在IPhone 2G 上重现,在模拟器和3GS上都正常工作。 我的直接想法就是,这是IPhone sdk的问题,因为新版本的sdk对2G不兼容导致的。但后来在Hailiang的IPod touch 3G上也能重现,我意识到这个bug的优先级比较高,需要解决。
首先,从现象入手,精确还原重现的条件。
- IPhone 2G 3.1.2 重现
- IPhone 2G 3.1.3 重现
- IPod Touch 3.1.2 重现
- IPhone 3Gs 3.1.3 不重现
- IPhone 4G 4.1 不重现
- IPhone simulator 不重现
分析:
从现象上分析,跟OS无关,因为在3.1.2 , 3.1.3上都能重现,还是跟硬件相关,在2G和ipod上重现,直觉还是新版SDK跟旧版硬件不兼容导致的,跟程序代码无关。
那我就来验证一下是否有这个兼容性问题,拿新版的SDK写一个最简单的sample导航程序在2G上跑,看能否重现。 结果是不能重现。 这说明之前的猜想不对,没有兼容性问题,问题还是在我们的项目中。
那是什么问题呢? 代码逻辑的问题吗?但是在3GS上是工作的,应该不是代码逻辑的问题。为了能把代码逻辑问题独立出来,我修改了项目的main函数,不执行我们的程序入口,而只是简单创建两个导航页面。问题仍然存在。看来代码逻辑是没有问题的。
那极有可能是项目编译配置的问题,于是我把项目所有的配置跟sample程序一一比较,将所有的关于代码生成/优化等设置得跟sample一样, 结果bug仍然存在,看来跟项目配置无关。为了确认跟项目配置无关,我把项目中的所有文件删除,只留那两个简单导航页面,问题不重现。 看来这个猜想是正确的。
那是什么问题呢? 考虑到我们用了好几个第三方的库。我觉的可能是第三方的库的编译设置问题或者是第三方的库用到了某些库跟旧版本的系统sdk冲突。 于是我把第三方的库一一剥离出来,结果仍然重现。 这里顺便提一下一个比较好的实践,如果用到了第三方的库,最好把用到的接口放到一个独立的文件里,这样你怀疑第三方的库有问题时,可以做一个stub,去掉第三方库,方便验证你的猜想。
既然第三方的库也没有问题,那看来是我们自己代码的问题了。 没有什么太好的办法,我只好把我们的代码一个个文件从项目中剥离,验证bug是否重现。运气不太好,当我剥离到最后两个文件时,才确认bug是出在那两个文件里。 这里也顺便提一下,保证项目中文件结构和依赖层次的清晰是非常重要的。比如A依赖B,B依赖C,C又依赖A,那在剥离的时候就很难下手。
Bug的root cause:
最后的bug出在Queue.h 和Queue.m中,这是我之前写的一个通用的Queue类(Objective-c 不提供Queue和Stack)。在Objective-C 中有Category的概念,跟C#中的partial class 概念类似,就是一个类的定义可以在两个文件中。这样就可以已这种方式对已有的类进行扩展,比如添加你自己的方法。我的Queue类 就是在Array类的基础上 加了pop和push两个方法,悲剧的是,在UINavigationController(IPhone导航)的实现里,它内部是用一个stack来维护每个导航页面,而这个stack的实现方式我猜跟我实现Queue一样,也是扩展了Array类,它也取名叫pop和push,两者发生冲突。 我把push跟pop改为 enqueue和dequeue,问题解决。
@interface NSMutableArray (QueueAdditions)
- (id)pop;
- (void)push:(id)obj;
@end
结论:
- 不要轻易怀疑SDK或者编译器。 对于一些莫名其妙的bug,起初怀疑时系统的问题,往往还是你自己代码的问题。
- 大胆假设,小心求证。对于所有难解的bug,从现象出发,根据你的经验假设问题的所在,然后一步步验证,抽丝剥茧,最后总能找出问题所在。
另外的一个结论是, Apple 应该对编译器做改进,在Category(partial class)里对重名的symbol,应该在链接的时候报错,否则在运行时是很难去定位bug的。