美文网首页iOS 架构项目优化
iOS中CTMediator原理探究

iOS中CTMediator原理探究

作者: 傲骨天成科技 | 来源:发表于2020-10-27 11:32 被阅读0次

    一、前言

    相信各位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,用_
    

    相关文章

      网友评论

        本文标题:iOS中CTMediator原理探究

        本文链接:https://www.haomeiwen.com/subject/nwfpmktx.html