以下内容皆为从实战中总结出来的经验,并非道听途说,至于是否适用,见仁见智
一、语言的选择 这个问题首先要说明的是本人绝非swift的死忠,相反,我在OC上使用了多年,但也正是因为如此,才发现OC有很多先天缺陷,例如: 1.异常抛出机制不够成熟 回想一下我们调用系统API来完成异常的处理手段,无非是两种情况,一是返回值,二是error之类的入参。这两种方式都有个致命的问题,就是程序员可以忽略他们,返回值可以不处理,error参数也可以传空,这样程序也能正常编译运行。但swift就不一样了,如果内部有抛出异常,那外面调用的时候就必须try…catch,其次,声明了返回值,你就必须接收他,即便你忽略,也得加上一个_明确表示你不处理。这对于普通程序员来说虽然麻烦了点,但能避免潜在的问题。当然,OC也可以执行断言,或者直接抛出exception,但并不是所有人都有这个觉悟,所以这方面选择swift是明智之举。 2. 参数非空及类型的判断 我已经记不起有多少个问题跟参数判断有关了,如果要按bug 产生的原因来排名,这类问题绝对可以排进前三。就说判空这个问题吧,有多少人会习惯在写一个函数时检查入参是否为空,反正我是没有,如果不是编译器警告,我一般都会默认他有值,这方面swift对于可选值的强制检查我觉得是很有必要的。 然后再说参数类型,这个问题相对没有判空严重,一般传错了类型编译器会警告或直接报错,但必须注意一点的是,对于默认值编译器有时候没那么智能。比如一个属性,原本你是写成一个string或者nsnumber类型,然后用!来判断是否为空,这看起来并没有什么问题。但是,当你把类型改为int或者float的时候,问题就冒出来了,原本的!也还能用,但当这个属性的值为0的时候,就不是你所期望的那样判断结果为true,而是false了。好吧,原来!作为判断条件是具有二义性的,也许用是否=nil来判断可以规避这个问题,但请相信我,你坚持不了多久又会用回!的,因为这样写起来太麻烦了。所以最好的方式就是让编译器或者代码扫描工具来帮你完成这个检查。
二、慎重使用多线程 多线程引起的线上问题想必大多数开发者都遇到过,而且大多会引起崩溃且难以排查。最直接的就是多线程同时访问或修改一个数据,造成exc_bad_access,或者是造成数据不一致,触发系统断言。但这并不是说多线程就极其可怕,我们要避免使用他,而是说我们要在性能和稳定性方面做一些权衡。大部分开发者,包括我在内,都是偏好在一个线程内完成大多数工作的,但这样也容易引起性能问题,比如需要读写缓存的时候,或者是从socket读数据的时候,如果放在主线程势必会引起卡顿,这时候就不得不开多线程了。那多线程的使用有什么讲究呢?这里我提几点建议;
1.如果要做的任务是比较频繁的,而且涉及到数组字典这样的容器,最好是用gcd 单独创建一个串行的队列,并且通过异步栅栏函数和同步函数来访问,这样可避免多线程同时访问同一个容器。如果在dispatch sync的block内有可能调用到另一个同步函数的,还请务必在调用dispatch sync前判断下当前的执行队列是不是自己本身,否则就有可能死锁。 如果任务是一次性的,且不存在对容器的操作,那可以使用global queue,或者是detachthread,不用考虑太多其他的问题。 如果任务可能需要随时中止,那建议使用nsthread,可以cancel,虽然不是那么实时。
2.使用多线程时,务必要考虑并发访问的问题。多数情况下,使用bool类型的变量或者NSObject的cancelPreviousPerform方法就可以解决函数并发问题,在关键步骤前加锁也可以解决,但容易引发其他问题。
3.非到必要情况不要加锁,如果必须要用,建议使用递归锁,可避免多次加锁导致的死锁。并且记得在函数中途return的时候释放锁,否则可就一直锁住了。
4.不要在dealloc中异步调用方法,包括使用多线程,因为对象一旦释放,再访问他的属性就会内存访问异常
三、慎重使用Category 我一直觉得category不是一个很好的设计,用来扩展方法可以,但是要用来增加属性或者重构代码就要慎重点了。一方面,分类里面没有成员变量,所有的属性都得通过关联对象来实现读写方法,麻烦不说,还增添了一些风险。比如要实现一个属性的懒加载,通常我们只需要读出来变量值为空,就初始化他,然后返回这个变量,但是分类中不能这么做,你通过get_assoicate_object读出来后发现值为空,还得在初始化后再通过set_associate_object把值写回关联对象,否则你这个初始化就当是白做了。 另一方面,分类覆盖主类的同名方法很容易导致调用方出现异常,特别是系统类的方法不能随便覆盖。反观swift的extension,就没有这些问题。
四、慎重使用KVO KVO相比其他的属性监听方式(RAC、rxswift等)优点在于无需block,没有循环引用问题,且能监听私有属性的变化,而缺点就比较多了。例如观察者对象释放前必须移除监听,这点除了增加代码行行数外,更容易产生内存问题,一旦忘记移除就会导致内存泄漏,而未添加观察者就移除则会导致崩溃。有时候移除和添加观察者的时机未必对等,比如在viewDidload中添加,在dealloc中移除,而即便两个方法是对等的,也不一定会按顺序调用到。例如添加写在viewDidAppear,移除写在viewWillDisappear,在从后面的页面侧滑返回到前页面一半再取消时,前一个页面就没有调用viewDidAppear,却调用了viewWillDisappear.
五、慎重使用懒加载 如果一个属性只有内部使用,并且不会在他所属的对象初始化完成前或即将释放前被访问到,则懒加载是安全的,否则就不要轻易使用懒加载。举个例子,通常我们会在viewcontroller的view will disappear 和view did disappear 或者dealloc方法中做一些页面释放前的清理工作,比如移除KVO,停止定时器等,这些代码如果用到了懒加载的属性,而这些属性又本该置空的话,则会出现属性再次被初始化的现象。为了安全起见,尽量显式创建对象或者使用实例变量来代替self.xx。
六、在dealloc中不能做的事: 1.不要处理延迟或异步任务 由于dealloc时,对象即将被清空,所以在这时候再dispatch一些异步任务会导致任务执行时对象已经被清空,从而引发App崩溃。最好的做法是在更早的时机去做这件事。 2.不要处理耗时任务 耗时任务会推迟对象的释放,从而引发一些意外情况发生 3.不要使用self.访问属性 如前所述,有可能会触发属性的懒加载
七、对于异步任务尽量少使用block,多使用代理来进行回调或者使用协程 过量使用block不但容易触发循环引用而导致内存泄漏,也容易使代码看起来很混乱。即便强弱引用写正确了,中间传递的某些参数也有可能因为弱引用而变空。 其实代理是一种很容易使用且不易出错的方式,虽然会多写一点代码,但协议的引入会使代码层级看起来更清晰,而且也便于代码的抽象和复用。 协程也是一种改善异步任务可读性的方式,相比代理会更简单,但可惜直到swift5.5版本官方才正式支持协程,在此之前只能使用第三方库,不得不说iOS在语言特性落后其他平台太多了
八、适当重构代码 1.复用性。保持良好的代码复用性不仅有助于提升开发效率,也能间接规避掉一些潜在的风险,比如一个需求要改很多地方,如果是复制粘贴的代码很容易改漏改错,但是代码写在一个地方就好的多。但是,也要注意公共代码修改之后所有关联的业务都要测一遍,除非这段公共代码已经做好了参数检查、各种逻辑分支都考虑全了。 2.渐进式重构。当涉及的业务模块较多时,一部分业务先重构,上线一个版本后看质量情况再决定后面的重构进度,这样可以减小风险。另外,重构的具体方法上,也可以采用先从业务代码中抽取公共基类,然后再拆分基类到各个模块,最后把模块重新组合到各个业务中去的路径来进行重构。这样每一步都可以完整运行,避免一次改动太大,从而遗漏内容。 3. 重构时遇到不熟悉的业务最好找到做过的人问一下,如果实在找不到,就要自己多测一下,避免把业务改错背大锅
九、代码review与单元测试 这个放在最后讲,并不是不重要,而是执行起来比较费时间,效果又不是那么显著,属于性价比较低的那种。对于稍大点的团队,有多人协作开发一个功能模块的,一般都有代码review。但是各个团队的review标准不一样,导致效果也不同。最基本的就是检查代码风格,命名,代码行数等,这类其实完全可以交给脚本去做,人工检查的意义不大。我认为比较实用的还是检查上述1-4项的问题,以及模块和架构设计是否合理,是否有内存泄漏点,性能是否有隐患等,这些才是review的重点,有一些可以被机器替代的工作也可以给机器去做。 单元测试对于底层的公共模块来说,是很有必要的,而到了业务层,则视情况而定了,采用mvvm模式,view model如果逻辑比较复杂的话可以做一下,数据可以mock,重点是业务逻辑是否正确,而对于view部分则多半不做要求,因为UI的变化相对业务逻辑的变化更频繁一些。单测相对codereview要难以实施的一个重要原因就是某些数据需要与服务器或者外部设备交互才能获得,如果全部mock那测试的范围就小了很多,这样单测的意义就没那么大了,而如何编写合适的测试用例对于开发来说也是一个挑战。