iOS中CTMediator原理探究

一、前言

相信各位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:&params 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,用_
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350