减少对象属性
这个是最容易改善代码质量的一个点,很多代码一眼看上去就会让人感觉很凌乱,一上来就是几十个不同的对象变量定义在里面,这让不同逻辑之间莫名其妙没法分开。一个是定义的方式不对,很多莫名其妙的内部变量暴露在头文件中,让外部调用者根本不知道哪些才是public可以操作的方法。另外实际上,经过我自己这段时间的重构经验来看,大多数是可以通过局部变量或者__block变量来代替的。
1. 头文件中尽可能少暴露变量或方法,而要使用extension或者category放在.m文件,或者专门的private头文件中
头文件中暴露的信息越少越好,一切不必要的信息都不要暴露出来
m文件的extension中,定义conforms protocol和对象属性,对于对象属性的定义,使用getter/setter 来定义。
2. 使用局部变量或者__block变量代替
局部变量不需要多说,需要写码的时候思路清晰一些,写完之后在commit之前即使review一定要check一遍,对自己的代码质量负责,code review往往检查不出来冗余或者废弃的代码。不添加一个多余的对象属性,不留注释掉的代码,不留没有用途的代码,这些都是基本功,但是很多开发者就是做不到,或者说对写码没有爱,所以很多废弃的代码,我重构代码的时候,虽然对业务不熟悉,但是大多数模块都能删除掉十分之一的代码和大量的对象属性,这个是单纯的不够用心。
关于使用__block变量,这个是Android开发中我感觉到最不满意的地方,这个特性简直太他妈爽了。
比如这里,使用block的时候回传一些变量
再比如这里,我需要记录一个pan手势开始时,headerView的顶部坐标,结合RAC之后,本来需要全局变量来记录的值,使用__block变量即可搞定
3. 可以尽可能避免循环引用
有个地方很多开发者会疏漏,在block中使用_XXX对象变量的时候,block会retain self指针,一不小心就会造成循环引用的出现。所以使用局部变量的话,就能扼杀这种问题在摇篮之中。
减少和模块化对象消息
1. 减少对象消息
减少UI的action类消息,感谢block和RAC,或者blockskit,让我们得以通过hook来把之前target-action模型换为block来实现,UI和action的代码终于可以一起了,使整个逻辑变得紧凑,在查看代码的时候终于不用跳来跳去了。还有就是日常开发中,把自己写的各种protocol或者传递target/selector的地方,尽量使用block来代替,相信我,这个会使代码好读很多。
2. 模块化
使用”#pragma mark - XXX”进行分割不同逻辑之间的界限,让整个文件阅读起来更加结构化。还有一个我现在最常用的就是是设置Xcode的快捷键,把Ctrl + 6 显示文档结构的快捷键改为:Command + J ,搜索来快速跳转到对应的消息和模块,要尽量避免文档结构显示超过两屏幕,超过两屏幕说明有点多了,你肯定考虑一下重构了。
我个人习惯一般划分的模块有: life cycle,ui helper,datasource/delegate,依据功能进行划分的模块等等,如下是我最近重构的一个ViewController的文档结构
MVVM && RAC
我自己使用MVVM思路的感觉是太爽了,说一下,MVVM不一定需要使用RAC,但是data binding少不了,在iOS中也就是KVO了,建议大家都去尝试一下,我自己感觉这个基本上MVVM的最核心的东西了,连Android SDK也不得不引入这个特性。把数据部分的逻辑抽取放在ViewModel中,然后让UI和ViewModel中的数据binding,这个不会减少代码量,但是绝对可以大大简化开发时逻辑的复度,再也不用重写-setXXX:方法来update一大堆不相关的UI了,关于UI开发,后面会专门再讲讲新的。这里说一下我自己的理解,有人说RAC影响性能,回调栈太深,这个的确是会有的,但是个人感觉RACObserver是基于KVO实现的,调用的时候是同步调用的,所以对性能的影响有限,也不会出现调用顺序的问题,所以我敢在列表开发中使用data binding,实践之后还好,对用户体验没什么影响。
关于RAC,即使你不使用RAC,有一些东西也是绝对值得你在项目中引入的,比如@weakify(self)/@strongify(self),通过预编译查看的话,这个的做法是设置一个局部变量self来覆盖全局的self,进而避免循环引用的,需要注意的是block层次较深的时候使用的问题,http://stackoverflow.com/questions/21716982/explanation-of-how-weakify-and-strongify-work-in-reactivecocoa-libextobjc。
RAC/MVVM,我刚开始学习的时候,写了两篇文章,算是我自己的总结,理解上面还有不足,跟大家参考一下:http://blog.csdn.net/colorapp/article/details/46524893,http://blog.csdn.net/colorapp/article/details/46537729。大家可以通过我博客中文章的参考链接学习。
UI开发
1. 重写setter方法和Code Block Evaluation C Extension语法
重写UI的getter方法,把UI的初始化放在getter中,减轻 -viewDidLoad的负荷,同时可以使整个页面变得清晰;同时,可以通过使用使用GCC Code Block Evaluation C Extension ({…})语法,结构化局部变量初始化和处理的逻辑。关于这个语法,参考我之前的博客:http://blog.csdn.net/colorapp/article/details/47006771。关于setter代码风格,可以参考别人写的一篇文章,http://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html,这个问题之前在我们Q群里探讨之后我也非常认同这种方式写UI。
举一个例子,-viewDidLoad中,做为逻辑的入口,代码会变少但是变清晰,代码如下:
然后重写bgView的getter方法,包括View和frame这些都可以使用({…})语法使代码结构化层次化:
2. 复杂UI的开发
有时候我们开发业务的时候,产品需求往往非常复杂,酷炫的UI加上各种考虑全面的逻辑,这个的结果就是,码农的超长代码,而我们平时工作面对的也大多数都是这类问题。关于这个问题,我的解决方式,组合式UI / custom view / child view controller来解决。
(1) 组合式view
这个概念是从Android中借鉴而来。重构时查看项目中的代码,发现大家用的做UI的时候,对这个概念不是很强烈,感觉是对UIView的view hierarchy理解不够。比如一个复杂的UI,直接把所有的subviews直接堆积到super view上面,这样的结果就是,调整subview的frame非常困难。我个人的做法是,首先对复杂UI进行分块,从左到右或者从上倒下,把各个UI元素放到不同的container view上面,然后组合这些container view放到super view上面,这样的好处非常明显,首先UI干净清晰,阅读起来不那么费劲。其次就是你计算坐标或者设置约束会变得很简单,因为你调整一个UI元素的时候,只需要考虑它与包含它的container view的坐标关系即可,而不是通过一大堆无趣计算跟最外层super view关联起来。还有就是可以充分利用Auto Layout和autoresiziingmask这些UI利器,使用的时候会非常方便。再有就是结合RACObserver这个利器之后,你能很容易做到根据data来update ui。
举个例子,是我们项目中前一段时间我重构的一个页面,这个首页列表,性能要求比较高。并没有使用Auto Layout来实现,但是不使用Auto Layout并不是不把它写的很干净的理由。
这是我对一个UITableViewCell的分层,最外层由 icon view / right view / bottom view这些container view组成,而right view这个container view则又是由right top view / right middle view /right bottom view 这些 sub container view组合而成,而具体的UI元素则是放在这些sub container view之中。这样UI代码就会以一种层次化样式展示出来,init/layoutsubviews只需要维护self与container view的关系即可,而具体展示数据的UI元素也只跟sub container view存在坐标关系。我们看一下right view这个container view的代码实现:
关于性能的话,感谢iOS,我们不存在Android中页面层次较深性能卡顿的问题,放心把UI层次化就行
(2) custom view
对于非常复杂并且相对独立或者可以重用的UI,及时使用custom view子类化。对于单纯的展示UI,我们只需要简单通过组合式view就可以实现了。但是有时候,我们会遇到一些包含无论是动画,逻辑都比较复杂的情况,这个时候使用组合式View去实现,一方面容易把逻辑弄混乱,会把文件的文档结构变得很复杂,简单来说就是对象的消息数量很多。这个时候,我们可以通过custom view来实现,实际上这个也是组合式view,但是我们是把这些组合式view变成了一个类而已,只暴露少量的接口给外部调用。如果这个custom view会出现在多个业务模块中,那么有必要使用一个单独的文件来容纳这个类,如果仅仅是这个模块一个使用的话,可以直接写在这个业务模块的文件中即可,没有必要对所有的类都单独一个文件,我们就当作这个“内部类”来弄了。
什么时候使用custom view而不是组合view,我想了很久,你觉得组合式view的代码很乱的时候,别客气,包装为一个custom view就行了。我这边最近遇到的几个问题是使用UICollectionView来做部分UI的时候,同时还有其他很多UI元素,我会写一个custom view。比如下面这个文件,把一个左右滑动查看图片的UI使用PhotoView这个custom view进行包装,内部使用UICollectionView实现一部分相对独立的模块,这个时候这个控件实际上是可以包装为一个相对独立的模块的,用子类我感觉比较合适一些。
(3) container view controller
这个用法很多开发者不熟悉或者说是用的不多,但实际业务中,这个技术非常有用途,可以大大提高开发效率。对这部分知识不熟悉的,可以参考我之前的博客:http://blog.csdn.net/colorapp/article/details/45765601。对于有相对独立业务逻辑以及生命周期要求的业务,使用child view controller进行包装,如果parent view contrller与child view controller之间非常密切,则使用View Model以及block来对parent view controller和 child view controller 进行衔接。
使用child view controller来开发UI而不是custom view的优势很多,我个人认为最大优势在于可以方便利用View Controller的生命周期以及View Controller Hierarchy,比如在-viewWillAppear/-viewDidDisappear中做一些操作,再比如直接获取UINavigationController指针等等。之前的做法一般是在View Controller的对应生命周期内调用custom view的方法,传递self.navigationController指针给custom view等。所以可以不仅仅把UI相关的代码包装进入这个child view controller,也可以把网络请求,数据处理这些这些逻辑放到child view controller中,这样下来就能避免那种动不动超过1k行的view controller的出现了。
利用MVVM之后,还有一个比较有好处的用法,比如公用一些数据的时候,之前我们是把对象传递来传递去,这样的问题是很容易出现混乱,这个时候我们是传递ViewModel就可以避免这个问题,ViewModel既负责网络请求又负责数据处理,而parent view controller与child view controller所需要做的事情就是跟ViewModel进行binding而已。
Auto Layout/Masonry
在一些性能要求不是那么强烈的非列表页,我们可以大量使用Auto Layout来开发UI,充分利用UI根据数据的自适应能力,连在container view中调整UI的步骤都不需要了。之前有一段时间我根本不想开发iOS,原因很简单,Android的布局式以及可见式的开发方式非常方便,再加上AS这样的神器,我自己感觉效率不比iOS低。自从项目最低支持变到iOS6之后,我才开始使用Auto Layout,虽然比较费劲,但是感觉这个对UI开发来说是个解脱。
至于Masonry这个框架,之前我对这个抱有一定的怀疑不敢使用,所以我把源码读了一遍,发现这个包装很薄很巧妙,很多设计思路也值得借鉴,对源码有兴趣的可以参考我的博客:http://blog.csdn.net/colorapp/article/details/45030163。我读完源码之后,尝试着完全使用Mansory来开发一个展示信息的页面,感觉太爽了!
这个的优势就是你设置UI的数据之后,不需要再考虑去update ui了,这样世界瞬时就清净了。。。。,下面是我一个简单的示例,结合({….})语法和RAC,可以使用最简单的label这样的命名来对UI设置数据,这个对我们开发UI来说,绝对是一种解脱。
说一下Auto Layout的问题:
1. 首先一个问题,是如果一个view不是leaf view的话,那么这个UIView如果hidden的话,它的约束仍然是work的,所以会留下空白,不会像Android中那样设置GONE那么方便。国内sunny大神开源一个不错的解决方式,https://github.com/forkingdog/UIView-FDCollapsibleConstraints。这里说一下我之前的解决方式,比较土逼,直接子类化:
2. 动画的问题
使用Auto Layout有一个比较大的问题在于动画,通过更改约束来进行动画,一直是我比较头疼的问题,所以一般遇到这类问题的时候,我都会尽量避免使用Auto Layout来解决,而是使用frame的方式来做。可以参考objc.io上面的一篇文章:http://www.objc.io/issues/3-views/advanced-auto-layout-toolbox/。
3. 多行UILabel的问题
iOS7以及以下的操作系统上,UILabel显示多行文本是又不足的,你需要设置UILabel的preferredMaxLayoutWidth为一个固定值才能显示多行文本。在iOS8以后就不再需要设置这个了。
4. UIScrollView的问题以及约束歧义和其他问题
参考我的文章:http://blog.csdn.net/colorapp/article/details/47007143
这个地方,我的建议是根据具体问题来选择实现方式 :spring & structs也好,Auto Layout也好,那种解决问题较为简洁快速就用那种,不一定非要固定于一种行为,尤其是开发的页面有大量动画的时候。
注释
不要写一堆中文注释,代码不要出现大量的中文,OC已经够啰嗦,不要这么啰嗦地写码。除了提供服务的public功能或者方法,业务代码仅在某些关键点上注释一下就行,不需要一大堆中文,这样太low,代码自注释即可,需要注释的,可以通过喵神的Xcode插件来实现,https://github.com/onevcat/VVDocumenter-Xcode。
而对于出现拼音命名代码的人,能做主的话,别犹豫,开掉吧。这里吐一下槽,之前的公司就有这样的哥们,不是我招进来的,老板硬塞给我的。
善用OC的新语法
OC有很多新的语法糖,可以大大提高我们的效率,参考Apple Guide:https://developer.apple.com/library/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html。
比如打印数字的时候,我们可以用@(xxx)来打印,定义枚举的时候使用typedef NS_ENUM,使用instancetype而不用id等等。而最近又到了每年技能槽刷新的日子了,iOS 9发布了,OC又有了一些新语法,去学习一下多用用吧。
JSON数据的处理
新手往往会被这个稍微困惑一下,比如服务器返回的数据格式不正确啦,包含null啦,都很容易引起项目崩溃。这个问题可以使用Mantle来解决,很多兄弟都在使用这个,我自己倒是一直没有用过。之前写了一个小框架放在了github上面,https://github.com/lihei12345/CYJSONValidator,这个在我们项目内部也在使用,效果不错,用来解析数据的时候,对数据的类型以及是否为null等进行校验,确保解析出来数据类型的正确性。对于可能不存在key的时候,还可以设置一些默认值。
举个例子:
block
使用block代替delegate,这个没啥可多说的,把代码变得非常紧凑,减少文件的消息数量,最主要的是关系没那么紧密了。对于有大量的delegate方法才考虑使用protocol实现,这个时候block太多也影响阅读。
同时,对于传递target/selector,也尽量使用block吧,这种阅读查找起来太不方便了。
提交代码
及时stage,这个非常重要,开发过程中经常需要经常比对上一步的代码,这样才能最大程度上确保自己的改动是正确的。如果有一些小问题,也可以即使找到历史版本。
及时commit,每完成一个相对完整的需求,就commit,小提交是个好习惯。
PR code review要做好,要花大量的时间做,有条件的话,最好每个版本开一次总结会。
RAC封装网络请求
返回的signal要避免多次出现side effect,但不使用replay/replayLazily,因为dispose不会被调用。
使用RACCommand封装请求,查看这几篇文章:http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/,https://github.com/ReactiveCocoa/ReactiveCocoa/issues/963,https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326。
结合RACCommand和takeUntil:来封装一个可以cancel的请求。