深入谈一谈iOS模块独立运行

2,332 阅读8分钟

背景

最近一直在团队推进关于iOS模块独立运行相关的事项,想把最近的一些想法和实施情况通过这篇文章做一个记录。

如果在一个项目中,某一块代码足够独立(功能、业务上),就会倾向于将他通过Cocoapods抽离为一个pods文件。通过一个podspec文件描述这个pod的信息。

最直接的方式就是把相关文件组织好迁移到一个目录下,通过podspec对源码位置,资源位置等信息的描述,完成一个最简单直白的pod创建。

并且在Podfile中通通过

pod 'XXX', :path => '~/dev/XXX'

这种方式,集成到主工程进行开发。开发自测完成后通过私有repo发布,并且将Podfile中的指向版本

pod 'XXX', '1.0.0'

然后我们就会说,抽离了一个库,项目的代码结构变得更合理更清晰了。

但这么做,在我看来跟在项目里直接用group把这些代码区别开来没什么区别,反而要修改的时候还要重新git pull、pod install变得更加麻烦了。

因为这么做被分离的代码无法独立运行,而且由于依赖不清晰,没办法共享给其他项目使用,导致这种分离方式是一种伪解耦,该有的益处没有展现出来,修改的时候反倒更加麻烦,这绝对不是我们想要的效果。

由此引出对模块独立运行

对iOS模块独立运行的思考

Pod带来的受益的预期

个人心目中完美的Pod应该在这四个维度上做到最好。

  • 依赖足够清晰 清楚的描述自己的依赖状况。说到这个不得不吐槽一个iOS项目与Cocoapods结合之后一个奇怪的现象,就是Pod如果在主工程中通过path引入的,那么,在不声明清楚自身以来的情况下,可以使用主工程内所有类,甚至是主工程的类,并且不会得到任何提示,这种情况很普遍,并且事后想要理清依赖的成本极高,这个Pod算是废了(没有了抽离Pod的意义了)。

  • 方便共享给其他项目使用 我的理解,抽离Pod的很大一个目的不就是为了共享吗?如果不是为了这个目的,其实没必要抽离Pod的,反而更加麻烦了,只跟一个项目绑死的Pod在我看来,完全没有必要做成Pod,项目里放在Group就可以了。而为了能达到这个目的需要克服很多的困难。

  • 方便快速修改验证 随着主工程越来越大,编译速度越来越慢,开发效率也在无形之中慢慢降低。Pod从主工程中脱离出来独立运行,单独编译,隔绝对主工程的依赖,完全自给自足,这样代码编译量就会大大降低,达到开发效能提高的目的。

  • 自身质量保障 在第三点的基础上,带上完全针对这个Pod的单元测试UI测试,完美颗粒化代码的同时,还能很好的保障自身的质量,并且清晰易维护,这块如果配合Xcode Server,会发挥强大的优势。

然而要做到这些维度上的最好,需要克服很多问题,接下来慢慢道来。

两种类型的代码

类似AFNetworking、SDWebImage这样的功能型代码,分离的一个只需要确保自己的依赖清晰,被依赖的时候使用方便就可以了。而对于偏业务型的代码就不那么容易了,通常会有界面,还会有各种业务带来的附加产物,例如打点,例如网络库各种规则。

因为功能型代码本身对于以上四点门槛不高,我就不展开讨论了,主要还是展开说一下业务型代码。

业务型代码独立运行的看法

推进业务型代码独立运行过程中遇到的一些问题列举:

独立运行问题

  1. 业务代码需要用到 打点、网络等基本能力,背后每个能力都可能牵扯出一堆间接依赖,但这些依赖跟这个Pod本身没任何屁关系,同时还会是不是的出现间接依赖不明确导致的编译报错问题。

  2. 作为已经是独立可运行的Pod了,界面什么的都自己hold了,那么它一定还需要跟其他本身之外的几面进行交互,举个例子,一个Product的Pod,需要跳转到Order中的一个界面,或者Chat中的一个界面,而这个界面在代码层面根本不存在,要如何处置。

  3. 之前也提到了,一个依赖清晰的独立运行Pod如果被不小心path方式开发了一次,那么这个Pod会慢慢变废,下次运行可能就不能运行了,所以还要想办法要怎么不被path依赖。

  4. 还有一个比较头疼的问题是,随着业务迭代,某个冷门的独立运行Pod并没有跟上脚步,其直接依赖的功能库在主工程都更新了,但它却全然不知,难道还要一个一个校对吗?

  5. 解决了基础能力的间接依赖,各种必要的直接依赖的间接依赖也会出现不明确而出现的编译失败问题,需要解决,确保只有代码api需要更新时才会编译不过,才是最爽的开发流程。

思考实施解决之道

实施过程中开发了两套工具来解决。均未开源,外网勿搜。下面介绍详细思路。

脚手架工具 - gearmaker

避免被path模式开发

好不容建了一个独立运行Pod,要是不小心被不明真相的同学用path开发了就糟了。如何避免被path模式开发呢?如果是源码模式下,其实是做不到的。所以我们把每个独立运行的Pod的产物定位二进制库,静态动态都可,podspec层面就不允许指向源码,想要修改源码,只能通过独立运行的工程进行修改。

具体操作这里不展开了,如果podspec指向的是静态库,而没有源码指向则这个Pod理论上不可能不可被path模式开发。

而如果通过Cocoapods 官方建议的 pod lib create 方式创建,则Pod代码会存在于 Development Pods 下,而必须在podspec中指定源码路径,因此我修改了 pod lib create 的脚手架模板,将Pod代码直接放入项目的一个Group中,而Group对应产物是一个framework,podspec直接指向podspec,外加Universal打包脚本就可以啦。

有同学有疑问了,那源码调试怎么办呢?这个不用担心,既然Pod已经可以独立运行,有什么问题需要调试,是都可以在Pod工程中进行数据Mock来还原问题的,所以主工程只是用来集成,不需要考虑调试问题。

即便是特别特殊的情况,只在主工程能还原,那临时加一下podspec指向本地path做一下debug也是可以的。

版本仲裁 & 保鲜

Pod独立运行工程的一大诟病就是时间一长,工程就无法编译通过运行了,并且哪些依赖需要更新,需要详细对比,成本非常高,很多遇到这样情况就会放弃Pod工程。

为此gearmaker中集成了版本仲裁能力,通过hook pod命令,在pod install之前,计算出指定客户端主工程最新的依赖全集,在pod install时,在这个全集中找到仲裁版本来使用。

这样一来,pod install后的Pod工程所有依赖必然与主工程一致,只需要修改因为依赖更新带来的相关api变更即可通过编译正常运行,确保Pod工程不会腐败。

依赖切断 服务提供组件 - ServiceProvider

所有Pod只需要直接依赖ServiceProvider,由ServiceProvider来统一提供服务能力,包括Pod工程本身需要的任何能力。比如,路由、打点、网络、从另一个模块获取数据,获取一个View对象等,均不需要依赖其他库,直接从ServiceProvider中通过内置的protocol来获得,并使用。

提供能力的一方对预先放置在ServiceProvider中的protocol进行功能实现,通过以下方式将自身的能力注册入ServiceProvider,即可为其他提供能力,而不需要依赖。

注册服务

[ServiceProvider registService:[XMUserTrack class] withProtocol:@protocol(UserTrack)];

获取服务

id<UserTrack> ut = [ServiceProvider serviceWithProtocol:@protocol(UserTrack)];

使用体验

对于新的Pod创建,直接使用以下命令创建:

gearmaker <PodName>

根据命令行提示进行创建即可。创建完成的脚手架直接提供了ServiceProvider,开发同学直接从ServiceProvider中获取服务进行Pod开发,开发完成后通过Universal脚本生成framework上传到私有repo中定版本即可直接使用。