前几个月完成对MVVM/RAC的学习之后,最近一直在默默地对项目代码进行重构,写码比较多,过了一段时间回头发现自己的代码风格还有代码质量都有大大的改善。过去几年在一家小公司负责iOS客户端后来负责客户端的研发工作,被杂乱的事情分神比较多,所以到去年的时候,写码已经不太多了。在新公司待了大半年,目前只是写码的小角色,所以精力基本上在写业务代码和业余学习乱七八糟的技术上面。
最近一个月除了专门抽时间和精力重构之外,还有就是遇到需要添加功能的模块的时候,由于项目中的代码历史因素比较多,***件干的事情往往是重构整理代码,发现很多之前的代码写的时候没有注意的事情特别多,比如全局变量乱用;方法没有层次感,胡乱添加;对业务不了解的情况下,通过打补丁的方式实现功能等等。所以我决定写一篇文章,把自己的觉得实践中需要注意的一些事项,具体总结一下分享给大家。
减少对象属性
这个是最容易改善代码质量的一个点,很多代码一眼看上去就会让人感觉很凌乱,一上来就是几十个不同的对象变量定义在里面,这让不同逻辑之间莫名其妙没法分开。一个是定义的方式不对,很多莫名其妙的内部变量暴露在头文件中,让外部调用者根本不知道哪些才是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的请求。