美文网首页冷兔宝宝iOS Developer
基于CocoaPods的iOS的项目进化

基于CocoaPods的iOS的项目进化

作者: 进击的海飞 | 来源:发表于2017-07-09 10:11 被阅读0次

    一、知得失

    每一个便捷工具或技术出现在台前,台后都躺着一个懒人

    在CocoaPods出现之前,iOS项目依赖的第三方库都是直接拖进项目中的,库多了之后,有代码洁癖(或说架构洁癖)的猿就要开始头皮发麻了,那么做的好一点就是建立个Workspace,业务代码在业务Project,第三方库或自己的私有库放到库Project,然后实现联编。这样管理起来虽然繁琐,但是起码项目架构规整了很多,而且越来越像后来CocoaPods的做法。

    熟悉node的都知道npm,CocoaPods与其很类似,实现了iOS项目的包管理,虽然受限于苹果搞得iOS项目的条条框框,起码实现了以下几点功能:

    • 统一的中心库概念,使我们可以便捷地添加、更新优质的第三方库
    • 相对完善的版本控制
    • 库Project和业务Project分离
    • 提供创建私有Pod仓库的能力

    于是CocoaPods开始遍地开花,大家用的都很Happy。

    但是“可以创建私有Pod仓库”这一点,随着时间的推移,会像打开了潘多拉的魔盒一样,问题开始喷涌而出。

    当私有库越来越多,“依赖泥潭”出现了!相对大一些的公司一般在发展过程会都会独立出一个项目组或一部分人去开发和维护这些私有库,虽然做这些工作的都是大神,但是人的思维是不一样的,在私有库进化过程中,要权衡库的耦合度和复用性的微妙关系,比如库A有一个字符串MD5的方法,库B需要对字符串进行MD5,那么库B是自己实现一个呢,还是引用库A的MD5方法,如果说库B依赖库A,很清晰啊,那么如果库A又要依赖库B中的某个方法呢,或者库更多一些的情况是库A依赖库B,库B依赖库C,库A依赖库C... 这样不断进化,最终“全家桶”出现了:当业务代码需要依赖一个库时,由于复杂的依赖关系,几乎所有的库拉下来了!

    你可以说这个完全可以通过精细的规划和设计就能避免,但是要考虑到开发这些库往往是不同的人甚至不同的项目组,而且框架组虽然会尽量考虑到库使用者的需求,但是他们考虑更多的是跟进整个公司的技术迭代,何况现在讲求架构分层,流行将业务代码封装为私有库。

    你也可以说拉下来就拉下来吧,编译通过运行没事就OK。但是编译通过是可以确定知晓的,但是运行没事怎么保证呢?做开发最起码要讲求发生了什么都要知情!

    一个最典型的案例,一个iOS项目中,依赖一个用于统计用户行为的库,但是这个库是隐性依赖(我们把没有直接写在Podfile中,而是通过其他库依赖进项目的库叫隐性依赖库)进项目的,也就是说每次执行pod install都会将最新版本拉下来;这样用了近两年,一直没事,但是某天框架组更新了这个库,将信息上传的服务器变得可选,然后提供了一个默认的服务器A,但是旧版本一直是上传到服务器B;这个变动变得很隐晦,因为我们发生的这些变动根本“不清楚”。于是发版后,线上故障出现了,App就像丢到大海的漂流瓶一样信息全无,基于服务器B而产生的用户行为统计报表突然都变得一片空白,而直到这时,可能心里还在说,这块代码我们没有动过啊,这个版本我们只是把某个Label字体加大了点...

    当然可以找出N种方案来避免这个问题,比如查看Podfile.lock,但我想强调的是“知情”。

    再来看另一个问题,人总是趋于安逸,我们的项目在使用框架组提供的私有库的某个版本,很稳定很Nice,于是我们锁死了这个版本,用了长时间。但是世界总是在变化的,尤其技术的更迭更是迅雷不及掩耳,当某天我们需要引进某个现成的功能,比如直播,突然发现我们项目中使用的库版本太陈旧了,尤其这个功能依赖的某些已存在我们Podfile中的库,怎么办呢?升级吧,结果悲催了,由于全家桶效应,我们Podfile中大部分已存在的库都要升,而升级有又要引入新的库,新的库又要引入新的库...,于是我们一遍又一遍的执行pod install或pod update(出于上文提到的问题,我们都是锁死某个确定的版本,而不是使用版本条件),一遍又一遍的编译...

    软件工程强调项目要为人优化,而不是为机器优化,因为人的时间比机器时间贵

    骄傲的程序猿的时间都是很贵的,为什么不能把这些事情交给机器去做呢?步入正题,如何在存在上述问题的情况下,保证基于CocoaPods的iOS项目架构平滑进化。

    二、成方圆

    《人月神话》中称软件项目像是掉进焦油坑的野兽,体型越大越难以挣扎,那么我们来拆解这个巨兽,让它变小,分而治之。

    我们需要一个清晰的分层架构,用于分解项目,在这个架构上我们先制定总规则:

    • 下层模块/库不能对上层进行依赖
    • 基础框架层的模块/库之间允许相互依赖,但是不建议不提倡,业务层和桥接层禁止同层级依赖
    • 所有层所包含的模块模块/库使用CocoaPods进行管理,以达到强制解耦的目的(拆分模块或库时会强制开发者考虑库依赖的问题)
    整体架构

    下面开始从下往上进行规则设定。

    2.1 基础框架层

    基础框架层特点主要有:

    • 公用性强
    • 包括框架组提供的平台性公共库、自封装的公用库
    • 不包含业务库

    该层又分为公共框架层和非公共框架层,解释下:这里“公共”是指多项目的公用性,而非单项目的公用性。

    1、公共框架层

    公共框架层的模块/库除具有基础框架层的特性外,最主要的特性就是多iOS项目公用,比如我们项目组有多个iOS项目,都需要用到Core库,那么Core便位于该层。

    最重要的一点,该层可独立编译。独立编译也意味着该层甚至可以编译为一个“大包”到处使用。基于这个特性,公共框架层会有版本的概念,每当公共框架层的库有变动时,会上升一个版本,而每个使用该层的项目都可选择版本,但是版本升级往往意味着技术迭代的发生,那么为了不让项目又进入“安逸”状态,要求每个项目都要跟进公共框架层的版本升级,并适当调整自己的的非公共框架层和桥接层。

    公共框架层升级诱因有两个:

    • 来自上层的功能需求
    • 公共框架层各库的技术更新和功能迭代

    公共框架层升级会导致的操作:非公共框架层和桥接层对公共框架层的变动进行适配

    2、非公共框架层

    非公共框架层的模块/库除具有基础框架层的特性外,最主要的特性就是不是多iOS项目公用,比如库TGBase,ProjectA依赖,但是ProjectB不依赖,则TGBase位于ProjectA中的该层。

    2.2 桥接层

    桥接层的主要作用是为上层业务层和下层基础框架层做桥接,主要考虑底层业务层的开放接口变动频繁以及不适配上层业务,需满足以下规则:

    • 使框架层的变动对业务层透明
    • 提供友好的接口给上层业务层,满足开闭原则
    • 可对基础框架层的变动进行快速兼容
    • 易于维护和扩展
    • 可下沉到基础框架层

    2.3 业务层

    业务层的模块/库主要包含业务库,业务层不允许层内模块/库的相互依赖,当某个模块/库确实要依赖另一个模块/库的功能,如果该功能不包含业务逻辑,则拆出该功能并封装成库,下沉到下层,若包含业务逻辑,那么尝试剥离业务逻辑后封库下沉,如果无法剥离业务逻辑,合并这两个业务模块。

    2.4 可编译性和可移植性

    除了公共框架层可独立编译外,其他各层由于需要向下依赖,所以不可独立编译,但是可以生成依赖图的最小生成树,这颗最小生成树可以进行独立编译,这一点的意义在于,当我们要单拎出某个业务库时,不必把下层所有库连根拔起。

    使用最小生成树法移植业务层库虽然是最精简的,但是对于项目组来说,确定一个公共库簇的意义更大一些,而公共库簇其实就是公共框架层,它包含了项目组项目甚至平台项目所需的最基本的库,所以可以把整个公共框架层可看做依赖图/树中的一个节点。

    那么最终可编译的最小生成树就可以合并所有位于公共框架层的节点,形成一个节点,减少分析和移植的复杂度。

    2.5 总结

    分层以及规则制定,主旨是保证复杂的依赖多的模块/库上升,而功能相对单一(或功能统一、明确)同时被依赖多的模块/库下沉,减少依赖环,尽量减少拔起萝卜带出泥的“全家桶惨剧”发生,增加可移植性。

    三、利其器

    CocoaPods确实是一把锋利的好刀,但不是瑞士军刀。

    针对上文所提的问题及需求,我们来看看我们在iOS项目进化时需要哪些功能:

    • 需要知道Podfile真实的依赖库以及依赖关系
    • 需要知道代码级的依赖关系
    • 当要添加或升级一个库时,需要知道隐性依赖和需要升级的已存在库
    • 当去除一个库时,需要知道其他库有哪些可以一并去除
    • 需要知道某个库某个版本的依赖
    • 需要知道多个项目的共同依赖库
    • 直观地实现分层
    • 对第二章制定的一些规则进行强制执行

    总的来说,就是我们希望CocoaPods能够自动进行pod的集成,好吧,这些功能CocoaPods不提供或不方便地提供。我们可以人工地一遍又一遍的pod install和编译来获取这些信息,但是人生苦短,不能这么干。

    那么可以把CocoaPods打造成一把瑞士军刀,我们可以开发CocoaPods的插件(ruby实现),也可以依托CocoaPods,开发适合我们需求的工具来实现上述功能,用什么语言开发无所谓,达成目标即可,最好是脚本语言,不过不建议用宇宙最强大的PHP,如果不嫌“沉甸甸”的话,Java也是一个选择。

    这里我选择开发依托CocoaPods的工具,Go语言实现,至于为什么用Go,安利一下:

    • 全量静态多平台编译,无需安装Go环境便可直接执行,无需繁琐配置和拉依赖库,使用成本低
    • 脚本语言般的开发速度,强类型,现代语言特性
    • 强大的协程(比线程更轻量),意味着更小的开销实现大的并发

    由于CocoaPods是LTS(Long Term Support),那么依托CocoaPods的工具也需要LTS。暂时命名工具名称为Pandora

    工具的意义不仅仅是提供便捷的功能,更重要的是实现强约束,毕竟人的思维具有不一致性和不稳定性,那么规则就不能依靠开发人员的自觉性去遵守,而应通过机器来强制执行。

    三、谋而动

    下面开始面对现实,我们有两个项目,ProjectA和ProjectB,两个项目当前都在使用稳定但陈旧的库,而我们要实现的最终目标有五个:

    • 项目中所有库都升级到最新稳定版本。
    • 实现分层。
    • 实现强约束和自动集成。
    • 库版本持续跟进
    • 项目可编译通过并运行OK。

    虽然很多工作可以交由工具去做,但是这仍然是个繁琐而庞大的进程,需要整个团队配合完成,所以要对这项工作进行任务拆解,分为三期,每期再分几个步骤来完成。

    在开始之前,先介绍下我们项目中存在的几种类型的库:

    • 第三方公有库:托管在CocoaPods中的公有库,如AFNetworking
    • 平台性私有库:公司框架组开发的适用于整个技术平台的库
    • 项目组私有库:项目自己的私有库,用于满足项目组私有业务或功能需求,或弥补平台性私有库的功能空缺

    3.1 一期工作

    一期工作的核心是分别升级ProjectA和ProjectB项目陈旧的库到最新稳定版本,并编译通过和可运行。

    1、建立空项目

    分别建立对应于ProjectA和ProjectB的空项目,目的是防止上层业务逻辑因为升级了底层库而不适配,导致编译不通过,所以要排除这部分的干扰。

    空项目只包含对应旧项目的Podfile。

    1、确定最新稳定版本

    当依赖一个别人开发的库时,也就意味着引进Bug风险,所以引入的这个库我们希望是稳定无Bug的,如何确定一个稳定版本有两个手段:

    • 分析法:人工阅读分析最新版本的库源码,分析Bug风险并得出结论。
    • 参照法:找到一个项目,该项目中使用的库是最新的或相对比较新,如果该项目线上稳定,那么可以以这个项目为参照。

    我们使用成本比较低的方案,就是参照法,参照的项目是ProjectMain。升级或添加的库均为平台性私有库和第三方库。

    那么接下来的工作便可交由Pandora工具完成,基本逻辑如下图。

    分析依赖

    最终会生成一个包含最新/相对比较新且稳定版本库的Podfile,该Podfile执行pod install后所安装的库与Podfile指定的库一致(没有隐性依赖),但是可能编译不通过。

    2.适配

    适配的工作重点是保证新生成的Podfile在执行pod install后可在空项目中编译通过。

    这一过程需要修改、拆分项目组私有库,以兼容平台性私有库和第三方公有库的变动。根据旧的项目组私有库的功能,可逐渐拆解为基础框架层库、桥接层库和业务层库,这样形成初级的分层架构。

    这部分工作暂时无法由工具来完成,需要团队小伙伴们分工配合来完成。

    3.2 二期工作

    二期工作主要是完善架构分层和保证运行无Bug。

    1.拆分业务模块

    将ProjectA和ProjectB旧项目中未拆分为库的业务模块从业务Project中拆出作为Pod库,移入Pod Project,然后适配,这个过程需要不断完善桥接层库,最终形成稳定的业务层和桥接层。

    2.分割基础框架层

    使用Pandora分析最终ProjectA和ProjectB的Podfile,将基础框架层在分为公共框架层和非公共框架层。

    最终,每个项目中非Pod Project变得代码极精简,变为一个壳,而大部分业务逻辑均拆分到Pod Project中,在第二章分层架构的基础上形成如下图所示的架构。

    架构

    3.约束和规约

    约束:检查每个层级的库是否满足第二章所制定的规则,不满足的需要进行修改、拆分和升层降层。这个工作可以由工具分析出这些不满足规则的库,然后团队小伙伴进行操作。

    规约:目的是精简库,需要使用工具对整个项目进行代码级的依赖分析,目标:

    • 每个项目组私有库的podspec都正确指定其依赖
    • 去除没有真正被依赖的库

    4.测试

    测试的最终结果是程序运行无Bug,这个过程需要持续修改、优化桥接层库。

    测试分两条并行线:

    • 自测:需要模块/库的负责人对自己负责的模块进行详细测试,必要时需要阅读和分析代码
    • QA回归:QA同学需要介入并对整个App进行全量回归,尽可能发现隐藏的Bug。

    3.3 三期工作

    三期工作主要是维护前两期工作的成果,保证项目组所有项目持续迭代。

    ProjectA和ProjectB将由Pandora工具托管,Pandora构建在CocoaPods和Git之上,提供命令行和可视化界面以使人工干预可以介入,包括:

    • 维护公共框架层整层大版本,提供详细友好的公共框架层升级建议。
    • 对平台性私有库的变动敏感,持续跟进最新稳定版本。
    • 实现项目的代码画像,保证分层架构规则的应用。
    • 实现分层可见。
    • 实现可人工干预的自动集成。
    • 生成更详细的自动集成结果的数据透视表,实现事事知情,以利于帮助开发人员修改、扩展桥接层库。

    相关文章

      网友评论

        本文标题:基于CocoaPods的iOS的项目进化

        本文链接:https://www.haomeiwen.com/subject/mvjshxtx.html