iOS组件化方案的思考与尝试

455 阅读9分钟

概述

看了casa和bang对于iOS组件化的思路和方案,这里记录一下自己的一些思考与尝试。

首先在此之前我理解的组件化,是一些基础、独立的基本功能的封装,其天然具有高度聚合,对外部依赖较少的特点,所以能够顺其自然的成为一个基础组件。比如我们常用的一些第三方轮子,AFNetworking、SDWebImage等等,就是挺好的网络和图片组件。

这里casa的方案,是将一个完整的业务模块(比如说该业务的整个MVC代码)作为一个业务组件,独立成一个能够单独编译的pod子工程。其核心点在于,业务组件和业务组件之间,并不直接依赖,每个业务组件都是独立的子工程。这里简单图示一下,详细的原理和实践可以去读一下casa的文章,已经有了解的,可以跳过此部分内容。

例如,消息列表单向依赖于好友聊天的情况

通常我们是直接import好友列表相关的vc,然后做跳转,这种方式就是直接代码依赖了好友聊天vc。

如果想在编译层面,解除这种直接的代码依赖,就是通过runtime进行调用。这种方式虽然能够在编译层面解除依赖,但是调用者写起来比较恶心,重复代码也多,没有编译提示容易出错,反而得不偿失。

所以最终优化后的方案就是case的方案:

  • 组件B将提供给外部的接口,统一写到target对象里面(可以有多个target),调用者通过查看target就能对组件的对外接口心中有数。

  • 组件A通过统一的runtime方法,来调用targetB的接口。这部分接口写到categoryB里面来。

    为何是category呢?主要是runtime调用会有一些重复的代码,差异部分主要在参数。所以category这里就是各个组件runtime调用的差异部分。like this:

    123456789
    - (UIViewController *)A_Category_ViewControllerWithCallback:(void (^)(NSString *))callback {      NSMutableDictionary *params = [[NSMutableDictionary alloc] init];  		params[@"callback"] = callback;  		params[kCTMediatorParamsKeySwiftTargetModuleName] = @"A";  		return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];}

问题来了,如果两个业务组件是双向依赖的呢?

所有组件都提供内部的target,以及外部的category。这样双向依赖的组件,也能给解耦。被依赖的只有含有runtime代码的category。

了解了这种解依赖的方式,就不难理解以整个业务MVC为维度去抽离组件的设计思路了。因为按这种维度来,业务和业务之间的交互,可能就收拢到,从另外一个业务获取一个vc,然后push到界面上来,将交互尽可能的减少。

手Q组件化的预期目标

这里以手Q为例,组件化之后,整个工程结构可能是下图这样的:

解释一下业务组件这里的相关名词:

  • 消息列表组件A:包括了消息列表业务的所有基本代码,其应该是个完整的MVC架构。

  • TargetA:消息列表提供给其他业务的能力,写在targetA里面。比如,targetA可提供一个返回消息列表VC实例的方法。这里注意一个要点,业务组件之间是平行关系,并不直接依赖,所以targetA并不是直接被其他业务组件依赖调用的。

  • categoryA:categoryA是通过运行时的代码来调用targetA。这么做的目的,是避免各业务之间有直接的代码依赖,让基础组件A成为一个独立可编译的个体。

这里的核心要点在于:

  • 编译层面上,依赖关系是至上而下的,平行的组件之间没有相互依赖。(业务组件之间的相互依赖解除,在上面概述已经解释过了)

  • 每个业务组件都是一个pod子工程,其是一个独立的编译个体

为何要从编译层面上解除依赖呢?

答案是想将业务组件做成积木形式,编译上解除依赖就带来了灵活性。

比如A依赖B、B依赖C的场景。产品表现上,A跳转B、B跳转C。

编译上不解除依赖,就需要ABC都参与编译。而解除依赖后,可以只需要AB参与编译,因为作为A业务的开发,其实我基本只需要关注A页面的逻辑,以及跳转到B页面的逻辑正常就行了。

真实情况可能会更加复杂,C有可能依赖更多,最后递归依赖下去,会导致我们开发A界面,需要将整个app编译起来。

组件化的预期收益?

上面的方案很清晰,但由于并没有做过这种业务模式的组件化,我比较大的疑问在于,其带来的收益有多大,毕竟如果在现有的项目,尤其是手Q这种重量级的项目中落实该方案,其成本也是巨大的。

独立组件开发场景

假如我是好友列表模块的开发人员,主要做好友列表的需求开发与维护,我的工作模式可能会变成怎样:

添加一个新的程序入口main A,将好友列表组件的VC作为Root VC,同时依赖好友列表组件以及基础功能组件的工程。这样我们就有一个好友列表组件的单页面APP,可以方便的进行好友列表页面的开发。

对于没有参与编译的业务组件,其代码逻辑是无法真正执行的。比如点击好友头像,要跳转到好友聊天业务,但是由于好友聊天组件未参与编译,所以是是无法真正展示好友聊天界面的。

那是不是点击头像后,就直接crash了呢?

可以在category进行运行时调用时,进行统一的异常处理。比如提示异常界面。

那如果我想要调试跳转到好友聊天页是否正常,怎么办呢?

可以考虑在podfile里面添加对应组件的依赖,让对应组件也参与编译。而那些完全不相关的业务组件,是没有必要参与编译的。

独立业务开发的预期收益是这样的:

  • 编译耗时巨幅减少:原来需要整个工程进行全量编译(以手Q的体量,这个耗时可能是40min左右),现在是独立组件进行编译,耗时可能会减少到1-2min左右。

  • 独立页面的单元测试变得可行:貌似移动端开发,做白盒测试的好像不多,不知道其他项目是否是这样的。这里由于子工程更加聚焦,对应的白盒测试貌似可以很容易的进行起来,这个点只是意淫一下,没有实施过

整个app联调场景:

除了单业务的开发场景,如果我们要进行完整app的联调和串联。能得到什么收益呢?

由于各个业务模块分离到pod子工程,并支持独立编译成静态库。在我们想要编译主工程的时候,可以考虑各业务组件以静态库的方式集成,这样整个主工程的编译耗时,就只是链接各个子工程的编译耗时了。

小结

通过上面的分析,貌似组件化的收益巨大。大部分开发场景,业务组件独立以单页面app的方式进行开发运行,不再需要忍受原始主工程的长时间编译,以及xcode动不动的卡死菊花。

步步推进是否可行

以手Q的体量,想要达到大部分业务完成组件化,可以预期将是一个长期的、漫长的过程。那么能否先拿单个业务进行组件化,实行小步快跑呢?

这里拿亲密关系业务先进行组件化进行分析:

选亲密关系页面的主要原因是因为这是最近几个版本新写的代码,应该耦合比较少,后续产品上也有持续的需求规划,到时候开发新需求时能够切实的体验上实际效果。

对比文章开头那张完整的图,可以看到单个组件化,需要做的事情如下:

  • 未进行组件化的代码,还是继续放到主工程不动

  • 已经组件化的亲密关系组件,抽离代码到pod子工程

  • 亲密关系组件依赖的其他业务组件,需要提供target和category。target在主工程,category在独立工程。

那么其他业务未进行组件化,对我们有什么影响呢?

唯一的影响就是,亲密关系单页面app,不再拥有跳转到其他业务模块的能力。因为其他业务模块必须编译主工程才有其代码实现。

以亲密关系为例尝试分析依赖

确认了单个业务模块组件化的情况,也能带来好的收益。那就可以开始着手拆分了,首先需要分析下亲密关系模块的依赖情况。目前网上找到的工具都不能很好的在手Q上运行起来,或者运行起来但是耗时巨久。

这里自己写了一个小工具RelationDependenceAna,是按照深度优先遍历的方式,去索引文件的import/include,目前对于那些无用的import暂时没有过滤。首次执行的话,会去构建缓存,会比较慢。后续执行就很快了。

看看分析出来的结果:

大概是这个意思,涉及到具体代码,这里没有贴出来。一个ViewController递归下来,把整个主工程的所有文件都import了,接近1W个文件。

从根节点开始人工分析,自认为依赖比较少、代码比较新的亲密关系业务,所依赖的比较大的基础模块也有10多个。所以要完成业务模块组件化的第一步,还是进行基础的组件拆分工作,先解决垂直性的依赖。

垂直性的基础组件依赖,是我们业务组件构建的基本,其一定得从主工程抽离,才能够达到我们业务组件独立运行的目标。

任重而道远,且行且珍惜。