iOS 组件化方案

231 阅读7分钟

蘑菇街组件化

  • App启动时实例化各组件,组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册。 第一种通过MGJRouterregisterURLPattern:toHandler:进行注册,将URL和block绑定。当组件执行[MGJRouter openURL:@"mgj://detail?id=404"]时,根据之前registerURLPattern:toHandler:的信息,找到之前通过toHandler:收集的block,然后将URL中带的GET参数,此处是id=404,传入block中执行。

第二种通过ModuleManagerregisterClass:forProtocol:的方法,在应用启动时,各组件都会有一个专门的ModuleEntry被唤起,然后ModuleEntry@protocolClass进行配对。当组件执行ModuleManagerclassForProtocol:方法,业务传入一个@protocolModuleManager,然后ModuleManager通过之前注册过的字典查找到对应的Class返回给业务方,然后业务方再自己执行allocinit方法得到一个符合刚才传入@protocol的对象,然后再执行相应的逻辑。

  • 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。

存在问题:

  • 蘑菇街没有拆分远程调用和本地间调用
  • 蘑菇街以远程调用的方式为本地间调用提供服务
  • 蘑菇街的本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋
  • 蘑菇街必须要在app启动时注册URL响应者 在组件化的过程中,注册URL并不是充分必要条件,组件是不需要向组件管理器注册Url的。注册了Url之后,会造成不必要的内存常驻,如果只是注册Class,内存常驻量就小一点,如果是注册实例,内存常驻量就大了。
  • 在iOS领域里,一定是组件化的中间件为openUrl提供服务,而不是openUrl方式为组件化提供服务。
  • 新增组件化的调用路径时,蘑菇街的操作相对复杂
  • 蘑菇街没有针对target层做封装

为什么存在问题:

  • 因为组件化方案的实施过程中,需要处理的问题的复杂度,以及拆解、调度业务的过程的复杂度比较大,单纯以openURL的方式是无法胜任让一个App去实施组件化架构的。

  • 实际App场景下,如果本地组件间采用GET方式的URL调用,就会产生两个问题:

    • 根本无法表达非常规对象
    • URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折 注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。

CTMediator

image.png

优点:

  • 组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。
  • 方便传递各种类型的参数。
  • 本地组件通讯为远程组件通讯提供服务
  • 利用 category 可以明确声明的接口,进行编译检查
  • 实现方式属于轻量级

缺点:

  • 需要给每一个模块创建对应的 target和 mediator 分类,模块化代码时较为繁琐
  • 在 category 中仍然使用了字符串硬编码,内部使用字典传参
  • 无法保证所调用的目标模块一定存在,运行时才能发现错误

解耦原理

  • 通过中介模式,将CTMediator类作为各个模块的中心,各个模块以CTMediator分类的形式扩展功能,CTMediator分类中提供服务
  • 分类中的函数具体去调用target-action,target和action需要以字符串硬编码的形式,如果模块比较多,提供服务比较多,那么字符串的硬编码需要时间维护。
  • 去model化思想:如果A模块向B模块传递信息,推荐参数使用基本数据类型,而不是以model形式提供,否则A模块依赖同一个model,B模块也依赖同一个model,这样模块间还是存在相互依赖,没有达到真正完全耦合。

实现原理

  • 基于OC的runtimecategory特性动态获取模块

    • 通过NSClassFromString动态获取类,并创建实例
    • 通过NSSelectorFromString动态获取SEL
  • 通过performSelector+NSInvocation动态调用方法

    • performSelector提供动态执行方法的能力
    • NSInvocation提供了消息调用的能力
    • 方法调用的本质是消息发送,即底层调用的是objc_msgSend

源码分析

image.png

以上是组件化方案的一个简化版架构描述,主要是基于Mediator模式Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务

  • 组件仅通过Action暴露可调用接口

  • 复杂参数和非常规参数,以及组件化相关设计思路

  • 组件化方案中的去model设计 组件间调用时,是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立

调用方如何知道接收方需要哪些key的参数?调用方如何知道有哪些target可以被调用?

  • 解决方案就是使用category

为什么是category而不是其他?

  • category本身就是一种组合模式,根据不同的分类提供不同的方法,此时每一个组件就是一个分类,因此把每个组件可以支持的调用用category封装是很合理的。
  • 在category的方法中可以做到参数的验证,在架构中对于保证参数安全是很有必要的。当参数不对时,category就提供了补救的入口。
  • category可以很轻松地做请求转发,如果不采用category,请求转发逻辑就非常难做了。
  • category统一了所有的组件间调用入口,因此无论是在调试还是源码阅读上,都为工程师提供了极大的方便。
  • 由于category统一了所有的调用入口,使得在跨模块调用时,对于param的hardcode在整个App中的作用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,因此是可以接受的。

其他优点

  • 基于安全考虑 我们需要防止黑客通过URL的方式调用本属于native的组件,在架构层面要做的最基础的一点就是区分调用是来自于远程App还是本地组件,我在demo中的安全措施是采用给action添加native前缀去做的,凡是带有native前缀的就都只允许本地组件调用,如果在url阶段发现调用了前缀为native的方法,那就可以采取响应措施了。这也是将远程app调用入口和本地组件调用入口区分开来的重要原因之一。

  • 基于动态调度考虑 今天我可能这个跳转是要展示A页面,但是明天可能同样的跳转就要去展示B页面了。这个跳转有可能是来自于本地组件间跳转也有可能是来自于远程app。

做这个事情的切点在本文架构中,有很多个:

  • 以url parse为切点
  • 以实例化target时为切点
  • 以category调度方法为切点
  • 以target下的action为切点