小公司iOS客户端代码优化之路

1,142 阅读13分钟

疫情基本要结束了,此间有时间,我总结了一下,近两年来我们在iOS客户端上的代码优化历程。我们先后经历了模块化,组件化,动态化实战演进。本文总结一下整体思路与所遇到的坑。

先说一说,小公司何时优化代码,为什么进行代码重构?

代码深层的重构,伤筋动骨,在业务高速飞速发展时,需要兼顾线上代码稳定性,同时进行有效的迭代,对于小公司来说注重业务的发展,所以前期很可能没有资源,做架构上升级。另一方面,一个项目如果一个人维护甚至不需要做架构,因为整个代码即使很乱,但是只要在架构者头脑范围之内或者说此时业务还不足够复杂到创作者都不能掌握,系统都是稳定的,只管快速向前跑就行。而一旦两个人以上维护一个项目,相互交叉开发时,维护者如果不能梳理清架构者整体的思路,亦或代码质量不高等原因,就会出现bug频现的情况,而一旦出现bug频现的情况,那就说明系统的可维护依然很差,维护者无法分清改动一行代码的影响范围。进一步,维护者为了保证系统的稳定性和自身的安全,依然不敢对项目进行彻底优化,只能进行if else抢救性开发时,导致系统进一步的臃肿。一旦出现这种情况,说明系统该重构了,代码架构依然阻碍了系统稳定性,进一步影响了业务的发展。这就是代码重构的一个重要原因,代码无法适应业务的发展。而本文所讲的模块化或组件化是降低代码复杂度的有效办法,让业务代码分块隔离松耦合,分而治之,这样开发人员面对的代码逻辑会有效降低。

这是未改造前的示例简图:

第一步:实现模块化

模块化,这个适合做代码改造的开始,这个是自上而下进行的,整体下来投入少,收益大。也是小公司最应该实践的。

首先从业务层面考虑,涉及哪些相对独立的业务模块,按大的模块进行划分。比方我们的APP的首页模块,个人中心模块,登录模块,信用模块,下单模块,还款模块,商城模块,会员中心都是相对独立的模块。我们把这些模块的引用加入路由中心,让模块都依赖路由中心,不要依赖彼此。

这是模块化之后的示例简图:

业务上的收益:
  1. 实现运营方利用通知或短信,通过scheme,打开App,并打开指定的原生页面或web页面。
  2. 进一步拓展可以实现更多页面内容的跳转,后端配置化。
  3. 增强h5与原生的深度交互能力,提升h5的更多价值。
技术上的收益:
  1. 一个模块的代码的进来放在一起,减少与其他模块的引用,加强代码的内聚性。最主要解决的问题防止出现改一个模块的一处代码,引发其他模块的多个bug,目的上减少修改代码的影响范围。
  2. 底层的代码进行抽取,相互没有任何依赖关系的库抽离出来,如工具方法,类拓展,公共UI组件,为组件化做了准备。
  3. 重要模块的跳转以及功能都统一由路由中心来管理
  4. 原生页面遇到问题,可以降维到h5页面,保障业务顺利进行;拓展了与hybird的交互,实现支持h5与原生的混合跳转,两者导航栏的任意回退,提高了hybird的能力。
路由中心其主要承担三部分功能:
  1. 普通的url,直接push并在webview中打开。
  2. 页面的跳转,嗅探当前的导航栏,进行push或present下一个页面。和1一起主要方便服务端动态配置。
  3. 调用独立功能,比如清空当前导航栈到首页,跳转到导航栈中的指定页面,跳转到指定tab,拨打电话,跳出APP到Appstore,等等,还有一些我们自己封装的一些和业务无关的功能。这部分功能不少是为了h5准备的。
  4. 后端接口上,配合自定义路由进行升级,同时将页面信息进一步配置化,从页面跳转到各种文案,图片都让服务端下发,客户端做好缓存策略,优化用户体验。

后期维护者也比较方便,比如增加两个路由,wzry://app.wangzherongyao.com/laofuziwzry://app.wangzherongyao.com/good_test,只需要两步走:

  1. 路由表的plist文件中增加url和对应的target/aciton

  1. 在target对应的类中,根据action增加对应的方法即可。

  1. 在其他控制器中调用

路由上的核心实现大家可以参考CTMediator的target-action模式

  1. 根据CTMediator的架构,一个组件需要建一个分类并制作私有pod。由于这个阶段我们业务相对简单,人员少。我们直接将url映射到一个类名和方法,runtime直接执行类对应的方法。
  2. 另外我们的路由,综合了更多的功能,支持分发普通的url,接管了页面的跳转行为,加强和了h5的交互

第二步:实现组件化

经过模块化,我们的代码规整了不少,稳定性也好多了。后来有时间我们又进行了组件化。

组件化,对业务上的收益来说可以利用写好的中低层组件,更快的搭建其他功能类似的APP。技术上的收益来说,首先代码上的进一步物理上的隔离,每个模块都放在一个仓储中,搭建私有pod来统一管理,让模块与模块之间的依赖,实现真正意义上的物理解耦,代码稳定性更加稳定。其次独立功能封装成了独立组件,更方便复用,代码在pod中,就不用再使用copy文件的方式了,并且依赖什么的都不用care了,一个pod install就搞定。我的经验此时最好从下而上的进行,因为越底层模块,越要求稳定性,一个底层模块不兼容性接口的修改,可能涉及很多上层组件都要修改,特别涉及多个APP的时候。

具体实现上来说,在控制对业务影响的前提下,我们主要做了两件事,一个就是路由的升级;另一个是组件的分层与粒度的划分,其中颗粒度的划分,根据具体的实际情况来拆分,并且随着业务的发展可能分久必合,合久必分,主要说下分层,分层相对更有参考价值。

组件的分层来说:
  • 第一层:第一步的模块业务组件,模块化中涉及的独立模块。
  • 第二层:业务基础组件,这块的组件主要用于支持上层业务组件,里面包含和业务相关性较强,如用户模块,日志模块,升级模块,web模块,人脸识别等
  • 第三层:功能组件,和业务没有强联系,如网路请求组件,Debug组件,主题配置组件,设备信息组件等
  • 第四层:基础组件,最纯粹的无相互依赖的组件,主要是对第三方库和工具类。此处说一下,由于组件化后,大家对于组件会涉及很多pod install,update等,遇到网络弱的情况,比较耽误时间。因为公司没有github的镜像或cache,我们依赖不是很多,就采用了最笨的方法,大家每人几个把库搬到公司自己的gitlab上,搬得过程中使用mirror也很方便。

下图为组件化分层,示例简图:

路由的升级

我们结合模块化中的经验对路由进行了标准化升级。搭建了面向协议的路由中心,组件对外暴漏的方法action,都抽像为协议。这样做的一方面是实现对组件方法的明晰和规范,各个组件的所有协议可以放到一起便于查阅,使用pod进行统一的管理,大家对组件依赖这个协议库就好,按照设计的原则,高层模块不应该直接依赖底层模块,而应该依赖两者的接口。另一方面内部组件的调用可以直接使用协议的方式,防止了没必要的url解析过程;并且内部组件调用更多,如果每个调用方法都设置url,会产生很多url,url本身可读性不强,有不少局限性。所以最后我们使用protocal的方式。

具体实现上来说,以前是直接通过路由表,直接找到target-acion并执行,组件只需要实现一个方法就好了;现在来说,下面的组件要实现一个协议方法,并且要提前通过load方法将自己的类信息和满足的协议注册到router中。

现在来看看具体的添加过程:

1.路由表为兼容以前的target-action,在老的plist文件中增加一个protocal键值对。新的直接使用省去target键值对。

2.定义一个协议

3.实现协议,并挂载到路由中心

4.具体调用,可以使用url方式,也可以使用协议的方式

第三步:实现动态化

经过组件化后,整个项目从上到下,已经处于相对松耦合的状态。此时我们正好业务上需要动态化。动态化业务上收益在于无需等待发版就可以更新APP;技术上,虽然我们通过testflight实现了代码的灰度测试,但是有些问题是大量用户才能暴露的问题,这时随时修复线上bug的功能就非常有必要了。我们选择了使用reactnative,性能比hybird的方式好一些。而在组件化的基础上,对单个模块进行动态化,对整个项目的影响降到了最小。当然也要付出代价拿出资源来踩坑,解决技术上的问题。新启一个以RN主导的项目相对好一些。但是如果涉及混合开发,特别是在原有项目中,增加RN模块的这种情况,就会遇到一些问题。我们的路径是先拿一个全新的项目的用RN来做,后面再加到现有稳定项目中。我们也在探索的阶段,就简单说遇到的问题与解决办法。

  1. 刚开始的时候,根据RN脚手架构建出的项目,不支持cocopod安装依赖,使用pod方式安装组件的时候,就遇到各种编译错误。所幸的是0.60.0后,rn支持了pod方式的注入,0.61.0后更是支持use_framework,方便了很多。
  2. 混合开发时,第一个是网络请求的问题,使用RN来发送还是原生发送,最后我们选择原生发送。一方面我们测试使用原生页面发送请求比RN发送请求请求,速度快很多;另一方面,因为RN只是整个项目中某一个模块的一部分,使用一套底层的网络框架,如用户状态,数据解析都不单独处理。这样RN页面只需要处理页面相关的逻辑即可。
  3. 混合开始时需要原生页面与RN页面的相互跳转,这个时候就需要考虑RN页面承载的问题,我们采用类咸鱼的flutter_boost的方案,即在原生页面打开一个rn页面时,使用一个viewcontroller作为rn容器,rn页面的跳转都在这个容器内进行。
  4. 打包与升级更新的问题。打包系统需要进行升级支持RN的打包,同时升级时为应对动态的大小与安全问题,需要进行了diff运算,保证下发的最小的包,同时支持全包更新与回滚。

RN总体使用下来,最好还是和原生搭配使用,感觉性能还是其发展的瓶颈,js的单线程不能高效的处理并发问题,同样一个请求,我们用原生发送请求回传给RN页面,比使用RN自己请求回来,要快10倍以上。另外在混合开发时,会遇到很多莫名奇妙的问题,需要不断的填坑,这可能也是跨平台的代价吧。

总结

我们开始的模块化就是在业务逻辑越复杂,代码耦合严重,bug不断增多的情况下,进行了代码的重构。后面的组件化时是我们有足够时间情况下,进行的代码预先升级,为后面的业务做准备。当然如果一个项目一开始就以整洁的代码要求自己,可能后面不需要伤筋动骨的改造;并且代码的优化是有各个维度的,任何时候都可以开始。总之架构上之改进,即可以是业务受影响,技术上更新换代的战略反击;也可以是主动出击,占领优势地位,为后续更大的战斗创造机会的提前布局,预则立不预则废。我感觉小公司至少要实现模块化,一是业务上的需要,二是为组件化做准备。不必操之过急的去实现组件化,因为组件化之后,必然需要一些额为的维护工作,还需要一些自动化的配套设施来辅助才好,比如我们搞了组件的验证与上传脚本,打包发版本自动化构建。中大公司自然要会去做组件化,团队人比较多,代码耦合越来越严重,协作的时间远大于组件化维护的时间,这个时候组件化可以显著提升业务上开发效率和稳定性。至于动态化更是看业务需要,综合考虑性能等各种因素,来统一规划。

参考文章:

手机淘宝客户端架构探索实践

iOS应用架构谈 组件化方案

蘑菇街 App 的组件化之路

模块化与解耦