上篇我们知道了如何创建组件化项目,这篇我们来聊聊组件化的重点:组件化通信
组件化通信方法
目前所了解的主流方式有一下三种:
- 1.
URL
路由 - 2.
target-action
- 3.
protocol
匹配
协议试编程
在编译层面使用协议定义规范
,实现在不同地方
,从而达到分布管理
和维护组件的目的
。这种方式也遵循了依赖反转
原则,是一种很好的面向对象编程
的实践。
但是方案也很明显:
- 由于
协议式编程缺少统一调度层
,导致难于集中管理
,特别是项目规模变大
、团队变多
的情况下,架构管控就会显得越来越重要
。 -
协议式编程接口
定义模式过于规范
,从而使得架构的灵活性不够高
。当需要引入一个新的设计模式来开发
时,我们就会发现很难融入到当前架构
中,缺乏架构的统一性
。
中间者
它采用中间者统一管理
的方式,来控制App的整个生命周期中组件间的调用关系
。同时iOS对于组件接口
的设计也需要保持一致性
,方便中间者统一调用
。
拆分的组件
都会依赖
于中间者
,但是组间之间
就不存在
相互依赖的关系了
。由于其他组件
都会依赖
于这个中间者
,相互间的通信
都会通过中间者统一调度
,所以组件间的通信
也就更容易管理
了。在中间者
上也能够轻松添加新的设计模式
,从而使得架构更容易扩展
。
好的架构一定是健壮的、灵活的
。中间者架构
的易管控带来的架构更稳固
,易扩展带来的灵活性
。
URL路由
这也是很多iOS项目
使用的通信方案
,它就是基于路由匹配
,或者根据命名约定
,用runtime
方法进行动态调用
,URL路由思路
采用了中间者模式
- 优点:
实现简单
- 缺点:需要
维护字符串表
,或者依赖于命名约定
,无法
在编译时暴露
出所有问题
,需要在运行时
才能发现错误
。
URL路由的优缺点
【优点】
-
极高的动态性
,适合经常展开运营活动的app。例如:电商类 -
方便统一管理
多平台的路由规则 易于适配URL Scheme
【缺点】
-
传参方式有限
,并且无法利用编译期进行参数类型检查
(所有的参数都是通过字符串转换而来) -
只适用于界面模块
,不适用于通用模块
-
参数格式不明确
,是个灵活的dictionary,还需要有个地方查看参数格式 不支持storyboard
-
依赖于字符串硬编码
,难以管理,蘑菇街为此专门做了一个后台管理这部分 无法保证所有使用的模块一定存在
-
解耦能力有限
,URL的"注册","实现","使用"必须使用相同的字符串规则
,一旦任何一方做出修改都会导致其他地方的代码失效,并且重构难度大
URL路由方式主要是以蘑菇街为代表的MGJRouter
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
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:
方法主要是对targetName
和actionName
进行容错处理,也就是对调用方法无响应的处理。 - 这个方法封装了
safePerformAction:target:params
方法,入参targetName就是调用接口的对象
,actionName是调用的方法名
,params是参数
。 - 并且代码中同时还能看出只有
满足Target_ 前缀的类的对象
和Action_的方法才能被CTMediator使用
。这时,我们可以看出中间者架构的优势,也就是利于统一管理,可以轻松管控制定的规则。
下面主要看下有action
的情况
protocol class
protocol匹配的实现思路
是:
- 1.将
protocol
和对应的类
进行字典匹配
- 2.通过用
protocol获取class
,再动态创建实例
protocol
比较典型的三方框架就是阿里的BeeHive。BeeHive
借鉴了Spring Service、Apache DSO
的架构理念,采用AOP+扩展App生命周期API
形式,将业务功能
、基础功能
模块方式以解决大型应用
中的复杂问题,并让模块之间以Service形式调用
,将复杂问题切分
,以AOP方式模块化服务
BeeHive核心思想
- 1.
各个模块间调用
从直接调用对应模块
,变成调用Service的形式
,避免直接依赖
- 2.App生命周期的分发,将
耦合在AppDelegate中逻辑拆分
,每个模块以微应用
的形式独立存在。
【优点】
- 1.
利用接口调用
,实现参数传递时的类型安全
- 2.直接使用
模块的protocol接口
,无需再重复封装
【缺点】
- 1.用
框架来创建所有对象
,创建方式不同,即不支持外部传参 - 2.用
OC
的runtime
创建对象,不支持Swift
- 3.只做了
protocol
和class
的匹配,不支持更复杂的创建方式和依赖注入
- 4.
无法保证所以使用的protocol一定存在对应的模块
,也无法直接判断
某个protocol是否能用于获取模块
除了BeeHive还有Swinject
BeeHive中的Module注册
在BeeHive
主要是通过BHModuleManager
来管理各个模块的。BHModuleManager
中只会管理已经被注册过的模块
BeeHive
提供了三种
不同的调用形式,静态plist
,动态注册
,annotation
。Module
、Service
之间没有关联
,每个业务模块
可以单独实现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
主要分三种
- 1.
系统事件
:主要是指Application生命周期事件
通常做法是AppDelegate
改为继承自BHAppDelegate
- 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
,就能进一步的把implInstance
和serviceStr
对应的加到BHContext
的servicesByName
字典里面缓存
起来。这样就可以随着上下文传递了
- 进入
serviceImplClass
实现,从这里可以看出protocol
和类是通过字典
绑定的,protocol
作为key
,serviceImp(类的名字)作为value
Module & Protocol
这里总结一下:
- 对于
Module
:数组存储
- 对于
Protocol
:通过字典
将protocol
与类
进行绑定
,key
为protocol
,value
为serviceImp
即类名
辅助类
-
BHConfig
类:是一个单例
,其内部有一个NSMutableDictionary
类型的config属性
,该属性维护了一些动态的环境变量,作为BHContext
的补充存在 -
BHContext
类:是一个单例,其内部有两个NSMutableDictionary
的属性,分别是modulesByName
和servicesByName
。这个类主要用来保存上下文信息的。例如:application:didFinishLaunchingWithOptions:
的时候,就可以初始化大量的上下文信息
-
BHTimeProfiler
类:用来进行计算时间性能
方面的Profiler
-
BHWatchDog
类:用来开一个线程,监听主线程是否堵塞
最后
几个月下来熬夜写了不少关于OC底层的文章,这个过程中对OC又有了新的认知,OC的内容研究目前告于段落!后面会分享一些自己在实际开发中的觉得比较好的封装和架构思路。我将继续探究Swift底层,也会继续更新文章,希望大家能够相互交流,一起进步。谢谢!