上篇我们知道了如何创建组件化项目,这篇我们来聊聊组件化的重点:组件化通信


组件化通信方法

目前所了解的主流方式有一下三种:

  • 1.URL路由
  • 2.target-action
  • 3.protocol匹配

协议试编程

编译层面使用协议定义规范实现在不同地方,从而达到分布管理维护组件的目的。这种方式也遵循了依赖反转原则,是一种很好的面向对象编程的实践。

但是方案也很明显:

  • 由于协议式编程缺少统一调度层,导致难于集中管理,特别是项目规模变大团队变多的情况下,架构管控就会显得越来越重要
  • 协议式编程接口定义模式过于规范,从而使得架构的灵活性不够高。当需要引入一个新的设计模式来开发时,我们就会发现很难融入到当前架构中,缺乏架构的统一性

中间者

它采用中间者统一管理的方式,来控制App的整个生命周期中组件间的调用关系。同时iOS对于组件接口的设计也需要保持一致性,方便中间者统一调用

拆分的组件都会依赖中间者,但是组间之间不存在相互依赖的关系了。由于其他组件都会依赖于这个中间者相互间的通信都会通过中间者统一调度,所以组件间的通信也就更容易管理了。在中间者上也能够轻松添加新的设计模式,从而使得架构更容易扩展

好的架构一定是健壮的、灵活的中间者架构易管控带来的架构更稳固易扩展带来的灵活性

URL路由

这也是很多iOS项目使用的通信方案,它就是基于路由匹配,或者根据命名约定,用runtime方法进行动态调用URL路由思路采用了中间者模式

  • 优点:实现简单
  • 缺点:需要维护字符串表,或者依赖于命名约定无法编译时暴露出所有问题,需要在运行时才能发现错误

URL路由的优缺点

【优点】

  • 极高的动态性,适合经常展开运营活动的app。例如:电商类
  • 方便统一管理多平台的路由规则
  • 易于适配URL Scheme

【缺点】

  • 传参方式有限,并且无法利用编译期进行参数类型检查(所有的参数都是通过字符串转换而来)
  • 只适用于界面模块不适用于通用模块
  • 参数格式不明确,是个灵活的dictionary,还需要有个地方查看参数格式
  • 不支持storyboard
  • 依赖于字符串硬编码,难以管理,蘑菇街为此专门做了一个后台管理这部分
  • 无法保证所有使用的模块一定存在
  • 解耦能力有限,URL的"注册","实现","使用"必须使用相同的字符串规则,一旦任何一方做出修改都会导致其他地方的代码失效,并且重构难度大

URL路由方式主要是以蘑菇街为代表的MGJRouter

我尝试加入各种iOS开发交流群,群里的气氛大致就是:学什么iOS,iOS完了,OC完了,群里大致三种人:谁有企业开发证书,马甲包了解一下,至今,大部分iOS开发群还都是仅供吹水用,偶尔能碰见几个好心人解决一下问题,作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

MGJRouter

其实现思路是:

  • App启动时实例化各组件模块,然后这些组件向MGJRouter注册URL,有时候不需要实例化使用Class注册
  • 组件A需要调用组件B时向ModuleManager传递URL参数跟随URL以GET方式传递类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。

MGJRouter采用URL路由,但是他的解耦能力还是有限

除了上面的MGJRouter,还有以下三方框架

target-action

这个方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例通过performSelector+NSInvocation动态调用方法

这种方式主要是以casatwy的CTMediator为代表

CTMediator

CTMediator其实现思路:

  • 1.利用分类路由添加新的接口,在接口通过字符串获取对应的类
  • 2.通过runtime创建实例,动态调用实例的方法
具体使用用法:
模块间的引用关系如下:

【优点】

  • 利用分类可以声明接口,进行编译检查
  • 实现方式轻量级

【缺点】:

  • 需要在mediator和target重新添加每一个接口模块化代码较为繁琐
  • category中仍然要引入字符串硬编码,内部使用字典传参,一定程度上也存在和URL路由相同的问题
  • 无法保证使用的模块一定存在target在修改后,使用者只能在运行时才能发现错误
  • 创建过多的target类,导致target类泛滥

CTMediator源码分析

CTMediator使用URL路由处理

这个方法主要是针对远程APP的互相调起,通过openURL实现APP之间的跳转,通过URL进行数据传递

CTMediator使用的是运行时解耦,解耦核心方法如下所示:

  • performTarget:action:params:shouldCacheTarget:方法主要是对 targetNameactionName进行容错处理,也就是对调用方法无响应的处理。
  • 这个方法封装了safePerformAction:target:params 方法,入参targetName就是调用接口的对象actionName是调用的方法名params是参数
  • 并且代码中同时还能看出只有满足Target_ 前缀的类的对象Action_的方法才能被CTMediator使用。这时,我们可以看出中间者架构的优势,也就是利于统一管理,可以轻松管控制定的规则。

下面主要看下有action的情况

protocol class

protocol匹配的实现思路是:

  • 1.将protocol和对应的进行字典匹配
  • 2.通过用protocol获取class,再动态创建实例

protocol比较典型的三方框架就是阿里的BeeHiveBeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能基础功能模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务

BeeHive核心思想

  • 1.各个模块间调用从直接调用对应模块,变成调用Service的形式避免直接依赖
  • 2.App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在。

【优点】

  • 1.利用接口调用,实现参数传递时的类型安全
  • 2.直接使用模块的protocol接口无需再重复封装

【缺点】

  • 1.用框架来创建所有对象,创建方式不同,即不支持外部传参
  • 2.用OCruntime创建对象,不支持Swift
  • 3.只做了protocolclass的匹配,不支持更复杂的创建方式和依赖注入
  • 4.无法保证所以使用的protocol一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块

除了BeeHive还有Swinject

BeeHive中的Module注册

BeeHive主要是通过BHModuleManager来管理各个模块的。BHModuleManager中只会管理已经被注册过的模块

BeeHive提供了三种不同的调用形式,静态plist动态注册annotationModuleService之间没有关联每个业务模块可以单独实现Module或者Service的功能

Annotation方式注册

这种方式主要是通过BeeHiveMod宏进行Annotation标记

这里针对__attribute需要说明一下几点

  • 第一个参数used:用来修饰函数被used修饰以后,即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器下会去掉没有被引用的段
  • 通过使用__attribute__((section("name")))指明哪个段数据则用__attribute__((used))来标记防止链接器优化删除未被使用的段,然后将模块注入__DATA中

此时Module已经被存储Mach-O文件的特殊段中,那么如何取呢?

  • 进入BHReadConfiguration方法,主要是通过Mach-O找到存储数据段取出放入数组

读取本地Pilst文件

  • 首先,需要设置好路径
  • 创建plist文件,plist文件的格式也是数组中包含多个字典。字典里面有两个key,一个是"moduleLevel",另一个是"moduleClass"。注意的数组的名字叫"moduleClasses"
  • 进入loadLocalModules方法,主要是从plist里面取出数组,然后把数组加入到BHModuleInfos数组里

load方法注册

该方法注册Module就是在load方法里面注册Module的类

  • 进入registerDynamicModule实现

其底层还是同第一种方式一样,最终会走到addModuleFromObject:shouldTriggerInitEvent:方法中

  • load方法,还可以使用BH_EXPORT_MODULE宏代替

BH_EXPORT_MODULE宏里面可以传入一个参数,代表是否异步加载Module模块,如果是YES就是异步加载,如果是NO就是同步加载。

BeeHive模块事件

BeeHive会给每个模块提供生命周期事件,用于与BeeHive宿主环境进行必要信息交互感知模块生命周期的变化

BeeHive各个模块会收到一些事件。在BHModuleManager中,所有的事件定义成BHModuleEventType枚举。如下所示,其中有2个事件很特殊,一个是BHMInitEvent,一个是BHMTearDownEvent

image

主要分三种

  • 1.系统事件:主要是指Application生命周期事件

通常做法是AppDelegate改为继承自BHAppDelegate

image
  • 2.应用事件:官方给出的流程图,其中modSetup、modInit等,可以用于编码实现各插件模块设置与初始化
  • 3.自定义事件

以上所有的事件都可以通过调用BHModuleManager的triggerEvent:来处理。

从上面的代码中可以发现,除去BHMInitEvent初始化事件BHMTearDownEvent拆除Module事件这两个特殊事件以外,其它所有的事件都是调用的handleModuleEvent:方法,其内部实现主要是遍历BHModules实例数组,调用performSelector:withObject:方法实现对应的方法调用

注意:这里所有的Module必须是遵循BHModuleProtocol的,否则无法接收到这些事件的消息

BeeHive模块调用

在BeeHive中是通过BHServiceManager来管理各个Protocol的。BHServiceManager中只会管理已经被注册过的Protocol。

注册Protocol的方式一共有三种,和和上面讲的注册Module是一样一一对应

Annotation方式注册

读取本地plist文件

  • 首先同Module一样,需要先设置好路径
  • 设置plist文件
  • 同样也是在setContext时注册services

protocol注册

主要是调用BeeHive里面的createService:完成protocol的注册

createService会先检查Protocol协议是否是注册过的。然后接着取出字典里面对应的Class,如果实现了shareInstance方法,那么就创建一个单例对象,如果没有,那么就创建一个实例对象。如果还实现了singleton,就能进一步的把implInstanceserviceStr对应的加到BHContextservicesByName字典里面缓存起来。这样就可以随着上下文传递了

  • 进入serviceImplClass实现,从这里可以看出protocol和类是通过字典绑定的,protocol作为keyserviceImp(类的名字)作为value

Module & Protocol

这里总结一下:

  • 对于Module数组存储
  • 对于Protocol:通过字典protocol进行绑定keyprotocolvalueserviceImp即类名

辅助类

  • BHConfig类:是一个单例,其内部有一个NSMutableDictionary类型的config属性,该属性维护了一些动态的环境变量,作为BHContext的补充存在
  • BHContext类:是一个单例,其内部有两个NSMutableDictionary的属性,分别是modulesByNameservicesByName。这个类主要用来保存上下文信息的。例如:application:didFinishLaunchingWithOptions:的时候,就可以初始化大量的上下文信息
  • BHTimeProfiler类:用来进行计算时间性能方面的Profiler
  • BHWatchDog类:用来开一个线程,监听主线程是否堵塞

最后

几个月下来熬夜写了不少关于OC底层的文章,这个过程中对OC又有了新的认知,OC的内容研究目前告于段落!后面会分享一些自己在实际开发中的觉得比较好的封装和架构思路。我将继续探究Swift底层,也会继续更新文章,希望大家能够相互交流,一起进步。谢谢!