文章出自:http://gracelancy.com/blog/2012/11/27/debug-objc-code/
作者简介:蓝晨钰(Lancy)目前在猿题库(yuantiku.com)任 iOS 研发工程师。
从一段奇葩的objc代码看代码规范的重要性
背景介绍
昨天,在和我一个朋友讨论,到底是用self.propertyName还是_propertyName来访问property,我认为应该使用self.propertyName,因为我在听Stanford Open Course的时候,苹果的工程师告诫要使用self.propertyName,不要使用_propertyName。而朋友认为应该使用_propertyName,因为google objc code style认为最好不要用self.propertyName。
我没看过google objc code style,我只看过objective c programming guide。在我的理解里property的作用在于根据参数生成相应的getter和setter。self.propertyName本质上既是调用getter函数的,而_propertyName直接访问成员函数,因为相应参数生成的getter和setter是不会被调用的。
再说,我还是决定相信apple,而不是google,毕竟Objc还是apple在支持和维护。
上代码
问题
这段代码是无效的,按下按钮之后,setupData被调用了,已经log确定dataArray已经改变,tableview的delegate和datasource都设置正确,确定numberOfRowsInSection被调用,奇葩的是cellForRowAtIndexPath没有调用,故而tableview没有改变。
奇葩的来了
朋友跟我说,你只要把[self.tableview reloadData]改成[_tableview reloadData],他就生效了。是的,他就生效了。你设一个断点在这个地方,然后把self.tableview和_tableviewpo出来,发现他们的指针是一样的。朋友说写这个代码的那货折腾了一天,百思不得其解,最后得出结论self.propertyName就是坑爹。
生效的修改方法
朋友提供的:
前面说的讲把[self.tableview reloadData]改成[_tableview reloadData]
把tableview的getter函数的init里面的self.view.bounds改成CGReckMake(0,0, 320, 480)
朋友试图用这个两个方法来说明,self.property是坑爹的。
我在初步debug的时候,由于我是property的拥护者,property自动生成setter和getter函数,我是不支持重写getter函数的,所以我将getter函数删掉,把初始化代码移到viewdidload里面。然后代码就生效了。
但是即使代码生效了,还是没有找到问题的关键,仍然没办法解释为什么[self.tableview reloadData]改成[_tableview reloadData]就能运行了,因为po出来的指针是完全一样的,这不科学。
真正的问题所在
在各种Stackoverflow,google无果之后,我还是着手准备深入debug。
通过各种断点和gdb,最后打印函数调用栈才让我发现了真正的问题所在。
整个程序的执行顺序是这样的:
initWithNibName(执行到[self setupData],没执行完) –>
第一次setupData(执行到[self.tableView reloadData],没执行完) –>
第一次执行tableview getter(到init,调用self.view,没执行完)->
viewDidLoad(到addSubview:self.tableView, 没执行完) –>
第二次执行tableview getter(问题在这里!第一次执行的时候没有init玩,所以又会执行一次!)->
回到4.viewDidLoad,这是add的subview是第二次的init而先init完的tableview –>
回到3.第一次执行getter,(又alloc了一次tableView,这是self.property指向的是第一次init而后init完成的tableview))
所以,显示在界面上的tableview根本不是self.tableview指向的tableview,故而根本没法刷新(cellForRowAtIndexPath,是当需要显示的时候才会调用的)。
那为什么把[self.tableview reloadData]改成[_tableview reloadData]就能生效了呢?因为这样在initWithNibName的第一次调用setupData,就不会在reload的时候调用tableview getter,也就不会有后面一连串的连锁反应。之后顺利在viewdidload的时候只调用一次,完成init。
知道了问题的关键,还能有各种各样让他生效的方法,就不吐槽了。
正确的写法
这段奇葩代码带给我最大的感触就是,不好好写规范的代码,各种问题都会坑死你。我认为规范的写法应该是
不要重写getter和setter函数,使用property生成的getter和setter
不要在vc的init的函数里面初始化,尤其是初始化视图。而应该在viewdidload里面初始化,保证self.view已经生成。(非ARC环境下还需要注意memory warning导致的viewdidload多次加载而多次初始化所带来的内存泄露问题。最安全的做法是lazy instantiation)
应该使用自顶向下的程序设计方法,保证程序的顺序执行和层次关系。不应该出现如上程序的跳来跳去的调用。
后记
帮人debug还是有好处的,让我结识了这位bug兄。也让我更加深入的了解了cocoa的变量访问机制,debug的时候顺带还测试了KVO。
Edit
我又重新去看了property和getter,setter的资料,也看了苹果对property的解释。最后我修正关于不要重写getter和setter函数的观点,更正为可以重写getter和setter,目的可以为lazy instantiation, UI updating, consistency checking,等。但需要注意如上程序的连锁反应。代码的灵活性和安全性
关于@property,经过和大家的讨论也有了一个结论:
Why property?
Most importantly, it provides safety and subclassablility for instance variables. Also provides “value” for lazy instantiation, UI updating, consistency checking, etc.
Lancy
11.27.2012