一、前言
相信各位iOS开发者在做组件化开发中都有考虑过组件之间的通讯问题。由于市面上有主流开源的组件化通讯有蘑菇街的MGJRouter、CTMediator等
其中MGJRouter的问题
1.每一个组件都需要初始化,内存中需要保存一份跳转的url表,这样当项目足够大,组件足够多的时候对内存会是一种负担。
2.参数的格式有限制,不能传递模型等问题
因此,我选了CTMediator进行使用。下来我们来看看CTMediator的主要原理是什么。
二、CTMediator的结构
CTMediator按照功能结构,我们可以将CTMediator分为三个部分。
1.CTMediator类:承担大总管的职务,居中调度。
2.Targer_(ModuleName)类:括号里面的名字可以自定义。承担组件对外暴露接口功能,组件需要提供怎样的服务。
3.CTMediator+(ModuleName)分类:括号里面的名字可以自定义。里面声明了组件可以调用的接口。
三、CTMediator核心功能
CTMediator主要采用target-action的方式实现组件间,自身的功能独立,不需要依赖任何组件模块
CTMediator作为一个通讯中枢,主要通过2中方案进行实现:
1.使用cocoa touch 提供的命令模式,将消息和消息接收者封装成一个对象,进行执行。
首先,利用target-action生成方法签名
然后,创建NSInvocation对象,执行invoke. 并拿到返回的结果。
利用方法签名,NSInvocation实现
// 1.通过target和action生成方法前方法签名
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
// 2.通过方法签名生成NSInvocation(调用的意思)对象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
// 3.设置参数位置在第二个位置
[invocation setArgument:¶ms atIndex:2];
// 4.设置action
[invocation setSelector:action];
// 5.设置target
[invocation setTarget:target];
// 6.开始调用
[invocation invoke];
id result;
// 7.获得方法的返回值
[invocation getReturnValue:&result];
2、或者利用runtime进行反射,将类字符串和方法字符串转换成类和SEL方法选择器
// 1.生成对应类名的字符串
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
// 2.将类字符串转换成类对象
Class targetClass = NSClassFromString(targetClassString);
// 3.初始化该类
id target = [[targetClass alloc] init];
// 1.生成对应类下的方法名字符串
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
// 2.根据方法名字符串生成方法选择器
SEL action = NSSelectorFromString(actionString);
然后通过performSelector方法调用
// 直接调用该类生成对象的对应方法,同时传递参数
[target performSelector:action withObject:params];
实际上CTMediator是先使用方法签名和NSInvocation进行方法的调用的,当几种返回值不满足的条件下才去执行performSelector调用方法的
CTMediator判断返回值类型,如果是void,NSInteger,BOOL,CGFloat,NSUInteger就使用NSInvocation进行特殊处理,不是的话就直接返回performSelector的返回值类型.
原因是[target performSelector:action withObject:params]返回的是id类型,因为在ARC中是不允许隐士的将void,NSInteger,BOOL,CGFloat,NSUInteger这些类型转换为id类型的。
四、对外提供服务的接口
如果你封装的业务组件中的某些类需要对外提供服务,就需要创建自己的接收动作类
比如CPHomeModule要对外提供服务,那么就要创建一个Target_CPHome类,然后在Target_CPHome类中声明对外服务的接口,并进行实现。
注意:Target_CPHome类在当前业务组件之内的。它属于业务组件的一部分。
例如:
import UIKit
@objc class Target_CPHome: NSObject {
/// 获取主页VC
@objc func Action_GetHomeVC(_ par:[AnyHashable:Any]?) -> UIViewController {
let homeVC = CPHomeVC.init()
return homeVC
}
/// 获取主页搜索VC
@objc func Action_GetHomeSearchVC(_ par:[AnyHashable:Any]?) -> UIViewController {
let vc = CPHomeSearchVC.init()
return vc
}
}
五、CTMediator+CPHomeModule组件通讯使用的分类
单独创建一个跳转组件例如CPJumpModule,然后在此组件中创建对应业务组件的通讯类,CTMediator的分类,如CTMediator+CPHomeModule,
使用此种做法巧妙的对项目进行了解耦。利用CTMediator的分类可以直接通过实例化CTMediator进行调用相关的方法,获取我们想要的信息,比如跳转的类对象。
import UIKit
import CTMediator
// 模块名,注意,如果模块名改变了(也就是DisplayName),需要改变对应的名称,
let kCTMediatorTarget_CPHomeModule = "CPHomeModule"
/*
比如:获取app的名称
// app名称
func kCTMediatorTarget_CPItemPoolModule() -> String {
let appName:String = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String
return appName
}
*/
// 对应模块的Target_后面的名称
let kCTMediatorTarget_CPHome = "CPHome"
public extension CTMediator {
/// 返回主页VC
@objc func CTMediator_vcForHomeVC() -> UIViewController? {
// GetHomeVC代表Target_ CPHome类里面Action_后面的名称
if let viewController:UIViewController = self.performTarget(kCTMediatorTarget_CPHome, action: "GetHomeVC", params:[kCTMediatorParamsKeySwiftTargetModuleName:kCTMediatorTarget_CPHomeModule], shouldCacheTarget: false) as? UIViewController {
return viewController
}
return nil
}
/// 返回主页搜索VC
@objc func CTMediator_vcForHomeSearchVC() -> UIViewController? {
if let viewController:UIViewController = self.performTarget(kCTMediatorTarget_CPHome, action: "GetHomeSearchVC", params:[kCTMediatorParamsKeySwiftTargetModuleName:kCTMediatorTarget_CPHomeModule], shouldCacheTarget: false) as? UIViewController {
return viewController
}
return nil
}
}
六、CTMediator还提供了远程app调用入口
也就是其他app通过url白名单的方式启动我们的app进行相关的操作
// 远程App调用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
它的使用方式也就是app间的跳转,例如:A->B A跳转B
1.需要我们给需要被跳转的应用B设置自己的白名单,例如zhengruiceping,
2.iOS13之前在应用B中的AppDelegate中的application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:])方法中进行CTMediator的操作,此方法可以接收到应用A传递过来的URL,通过URL进行判断跳转
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
debugPrint("要打开的url0=\(url)")
if let vc = CTMediator.sharedInstance().performAction(with: url, completion: nil) {
CTMediator.sharedInstance().push(vc as! UIViewController, animated: true)
}
return true
}
3.iOS13或之后在应用B中的SceneDelegate中的func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)方法中进行CTMediator的操作,此方法可以接收到应用A传递过来的URL,通过URL进行判断跳转
@available(iOS 13.0, *)
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for item in URLContexts.enumerated() {
let a:UIOpenURLContext = item.element
debugPrint(a.url)
let url:URL = a.url
debugPrint("要打开的url3=\(url)")
if let vc = CTMediator.sharedInstance().performAction(with: url, completion: nil) {
CTMediator.sharedInstance().push(vc as! UIViewController, animated: true)
}
}
}
4.在应用A中写上要跳转的url,通过openURL进行跳转到B应用
NSURL *url = [[NSURL alloc] initWithString:@"zhengruiceping://CPHome/GetHomeMessageVC?kCTMediatorParamsKeySwiftTargetModuleName=CPHomeModule"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
NSLog(@"成功了么==%d",success);
}];
}
注意:
url的写法:
zhengruiceping代表的是应用B的白名单
CPHome代表域名,也就是应用B中的Target_CPHome类
GetHomeMessageVC代表路径,也就是应用B中Target_CPHome中的Action_ GetHomeMessageVC方法
kCTMediatorParamsKeySwiftTargetModuleName代表模块名称的key值
CPHomeModule代表对应的模块名称
[[UIApplication sharedApplication] canOpenURL:url]中注意,如果调用了此方法,则需要在应用A中注册应用B的白名单,否则此方法返回NO
七、Swift组件与Objective-C组件互相调用的全部8种情况,及其对策
其实如果你的工程是Swift调用者、Swift响应者的情况,那么应用CTMediator就一点问题都没有。
对于Objective-C的开发团队来说,如果开始渐进地将工程Swift化,那么就需要分各种情况去处理block和closure转化的问题。在这里我把可能出现的所有情况及其处理方法总结如下:
1.Swift调用者 + Extension + Swift响应者
1.Extension中给到的params需要带有 kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
2.Extension中不需要针对closure做任何转化
3.响应者target-action跟正常情况一样写
4.响应者action方法首参数不要带Argument Label,用_
2.Swift调用者 + Extension + Objective-C响应者
1.Extension中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
2.Extension中需要将closure转化成block对象之后再放入params传过去
3.响应者的Target-Action跟正常情况一样写
3.Swift调用者 + Category + Swift响应者
1.Category中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
2.Category中不需要针对block做任何转化
3.响应者的Target-Action需要将block转化成closure
4.响应者action方法首参数不要带Argument Label,用_
4. Swift调用者 + Category + Objective-C响应者
1.Category中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
2.Category中不需要针对block做任何转化
3.响应者的Target-Action跟正常情况一样写
5.Objective-C调用者 + Category + Objective-C响应者
1.Category中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
2.Category中不需要针对block做任何转化
3.响应者的Target-Action跟正常情况一样写
6.Objective-C调用者 + Category + Swift响应者
1.Category中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
2.Category中不需要针对block做任何转化
3.响应者的Target-Action需要将block转化成closure
4.响应者action方法首参数不要带Argument Label,用_
7.Objective-C调用者 + Extension + Objective-C响应者
1.Extension中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
2.Extension中需要将closure转化成block对象之后再放入params传过去
3.Extension中的方法需要带前缀@objc
4.响应者的Target-Action跟正常情况一样写
8.Objective-C调用者 + Extension + Swift响应者
1.Extension中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
2.Extension中不需要针对closure做任何转化
3.Extension中的方法需要带前缀@objc
4.响应者target-action跟正常情况一样写
5.响应者action方法首参数不要带Argument Label,用_