ctmediator_ios ctmediator - CSDN
精华内容
参与话题
  • 最近开始用CTMetidor来做App模块化,顺便研究一下它的实现原理 CTMetidor 源码中经常...在理解CTMediator原理之前我们先弄懂这么几个概念: Method 先来看一下Method相关的定义 typedef struct objc_method *Meth...

    最近开始用CTMetidor来做App模块化,顺便研究一下它的实现原理

    CTMetidor 源码中经常出现如下关键词:NSSelectorFromStringNSClassFromStringSEL 这些是个啥???

    在理解CTMediator原理之前我们先弄懂这么几个概念:

    Method

    先来看一下Method相关的定义

    typedef struct objc_method *Method
    struct objc_method{
        SEL method_name      OBJC2_UNAVAILABLE; // 方法名
        char *method_types   OBJC2_UNAVAILABLE; // 函数的返回值和参数
        IMP method_imp       OBJC2_UNAVAILABLE; // 方法的具体实现
    }
    复制代码

    我们可以看到该结构体中包含一个SELIMP,实际上相当于在SELIMP之间作了一个映射,将SELIMP进行了关联,通过SEL我们便可以找到对应的IMP,从而调用方法的实现代码。

    SEL(selector)

    • 方法编号,对方法名hash化的字符串

    • 无论什么类里,只要方法名相同,SEL就相同。项目里的所有SEL都保存在一个NSSet集合里(NSSet集合里的元素不能重复),所以查找对应方法,只要找到对应的SEL就可以了。

    既然SEL是方法的唯一标识,那不同的类调用名字相同的方法怎么办呢?

    每个方法名有对应的唯一seletor,其SEL相同,但对应的IMP函数指针不同。

    如何获取SEL?

    SEL s1  = @selector(test);
    SEL s2 = NSSelectorFromString(@“test”)
    复制代码

    以上两个方法是等价的

    IMP (implement)

    • 一个函数指针,保存了方法的地址,内部实现:
    typedef id (*IMP)(id, SEL, ...); 
    复制代码
    • 包含id(消息接受者,也就是对象),SEL(方法的名字),参数

    XX调用XXX方法,参数XX也都确定了

    执行对应的方法:

    [object test];
    // @selector(test) 是一个C的字符串
    [object performSelector:@selector(test)]];
    // 转换成如下实现方式
    objc_msgSend(object,@selector(test))
    复制代码

    总结

    • NSClassFromString 通过字符串的名称来获取一个类,可以根据Target来进行获取
    • NSSelectorFromString 通过字符串(已存在的方法名称)获取一个SEL
    展开全文
  • 前述 国内业界大家对组件化的讨论从今年年初开始到年尾,不外乎两个方案:URL/protocol注册调度,runtime调度。 我之前批评过URL注册调度是错误的组件化实施方案,在所有的基于URL注册调度的方案中,存在两...




    前述



    国内业界大家对组件化的讨论从今年年初开始到年尾,不外乎两个方案:URL/protocol注册调度,runtime调度。


    我之前批评过URL注册调度是错误的组件化实施方案,在所有的基于URL注册调度的方案中,存在两个普遍问题:


    1. 命名域渗透
    2. 因注册是不必要的,而带来同样不必要的注册列表维护成本


    其它各家的基于URL注册的不同方案在这两个普遍问题上还有各种各样的其他问题,例如FRDIntent库中的FRDIntent对象其本质是鸡肋对象、原属于响应者的业务被渗透到调用者的业务中、组件化实施方案的过程中会产生对原有代码的侵入式修改等问题。


    另外,我也发现还是有人在都没有理解清楚的前提下就做出了自己的解读,流毒甚广。我之前写过关于CTMediator比较理论的描述,也有Demo,但惟独没有写实践方面的描述。我本来以为Demo就足够了,可现在看来还是要给一篇实践的文章的。


    在更早之前,卓同学的swift老司机群里也有人提出因为自己并没有理解透彻CTMediator方案,所以不敢贸然直接在项目中应用。所以这篇文章的另一个目的也是希望能够让大家明白,基于CTMediator的组件化方案实施其实非常简单,而且也是有章法可循的。这篇文章可能会去讨论一些理论的东西,但主要还会是以实践为主。争取做到能够让大家看完文章之后就可以直接在自己的项目中顺利实施组件化。


    最后,我希望这篇文章能够终结业界持续近一年的关于组件化方案的无谓讨论和错误讨论。




    准备工作



    我在github上开了一个orgnization,里面有一个主工程:MainProject,我们要针对这个工程来做组件化。组件化实施完毕之后的主工程就是ModulizedMainProject了。抽出来的独立Pod、私有Pod源也都会放在这个orgnization中去。


    在一个项目实施组件化方案之前,我们需要做一个准备工作,建立自己的私有Pod源和快手工具脚本的配置:


    1. 先去开一个repo,这个repo就是我们私有Pod源仓库
    2. pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]
    3. 创立一个文件夹,例如Project。把我们的主工程文件夹放到Project下:~/Project/MainProject
    4. 在~/Project下clone快速配置私有源的脚本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git
    5. 将ConfigPrivatePod的template文件夹下Podfile中source 'https://github.com/ModulizationDemo/PrivatePods.git'改成第一步里面你自己的私有Pod源仓库的repo地址
    6. 将ConfigPrivatePod的template文件夹下upload.sh中PrivatePods改成第二步里面你自己的私有Pod源仓库的名字



    最后你的文件目录结构应该是这样:


    Project
    ├── ConfigPrivatePod
    └── MainProject



    到此为止,准备工作就做好了。




    实施组件化方案第一步:创建私有Pod工程和Category工程



    MainProject是一个非常简单的应用,一共就三个页面。首页push了AViewController,AViewController里又push了BViewController。我们可以理解成这个工程由三个业务组成:首页、A业务、B业务。


    我们这一次组件化的实施目标就是把A业务组件化出来,首页和B业务都还放在主工程。


    因为在实际情况中,组件化是需要循序渐进地实施的。尤其是一些已经比较成熟的项目,业务会非常多,一时半会儿是不可能完全组件化的。CTMediator方案在实施过程中,对主工程业务的影响程度极小,而且是能够支持循序渐进地改造方式的。这个我会在文章结尾做总结的时候提到。


    既然要把A业务抽出来作为组件,那么我们需要为此做两个私有Pod:A业务Pod(以后简称A Pod)、方便其他人调用A业务的CTMediator category的Pod(以后简称A_Category Pod)。这里多解释一句:A_Category Pod本质上只是一个方便方法,它对A Pod不存在任何依赖。


    我们先创建A Pod




    1. 新建Xcode工程,命名为A,放到Projects下
    2. 新建Repo,命名也为A,新建好了之后网页不要关掉


    此时你的文件目录结构应该是这样:



    ├── ConfigPrivatePod
    ├── MainProject
    └── A



    然后cd到ConfigPrivatePod下,执行./config.sh脚本来配置A这个私有Pod。脚本会问你要一些信息,Project Name就是A,要跟你的A工程的目录名一致。HTTPS RepoSSH Repo网页上都有,Home Page URL就填你A Repo网页的URL就好了。


    这个脚本是我写来方便配置私有库的脚本,pod lib create也可以用,但是它会直接从github上拉一个完整的模版工程下来,只是国内访问github其实会比较慢,会影响效率。而且这个配置工作其实也不复杂,我就索性自己写了个脚本。


    这个脚本要求私有Pod的文件目录要跟脚本所在目录平级,也会在XCode工程的代码目录下新建一个跟项目同名的目录。放在这个目录下的代码就会随着Pod的发版而发出去,这个目录以外的代码就不会跟随Pod的版本发布而发布,这样子写用于测试的代码就比较方便。


    然后我们在主工程中,把属于A业务的代码拎出来,放到新建好的A工程的A文件夹里去,然后拖放到A工程中。原来主工程里面A业务的代码直接删掉,此时主工程和A工程编译不过都是正常的,我们会在第二步中解决主工程的编译问题,第三步中解决A工程的编译问题。


    此时你的主工程应该就没有A业务的代码了,然后你的A工程应该是这样:



    A
    ├── A
    |   ├── A
    |   │   ├── AViewController.h
    |   │   └── AViewController.m
    |   ├── AppDelegate.h
    |   ├── AppDelegate.m
    |   ├── ViewController.h
    |   ├── ViewController.m
    |   └── main.m
    └── A.xcodeproj




    我们再创建A_Category Pod



    同样的,我们再创建A_Category,因为它也是个私有Pod,所以也照样子跑一下config.sh脚本去配置一下就好了。最后你的目录结构应该是这样的:


    ├── A
    │   ├── A
    │   │   ├── A
    │   │   ├── AppDelegate.h
    │   │   ├── AppDelegate.m
    │   │   ├── Assets.xcassets
    │   │   ├── Info.plist
    │   │   ├── ViewController.h
    │   │   ├── ViewController.m
    │   │   └── main.m
    │   ├── A.podspec
    │   ├── A.xcodeproj
    │   ├── FILE_LICENSE
    │   ├── Podfile
    │   ├── readme.md
    │   └── upload.sh
    ├── A_Category
    │   ├── A_Category
    │   │   ├── A_Category
    │   │   ├── AppDelegate.h
    │   │   ├── AppDelegate.m
    │   │   ├── Info.plist
    │   │   ├── ViewController.h
    │   │   ├── ViewController.m
    │   │   └── main.m
    │   ├── A_Category.podspec
    │   ├── A_Category.xcodeproj
    │   ├── FILE_LICENSE
    │   ├── Podfile
    │   ├── readme.md
    │   └── upload.sh
    ├── ConfigPrivatePod
    │   ├── config.sh
    │   └── templates
    └── MainProject
    ├── FILE_LICENSE
    ├── MainProject
    ├── MainProject.xcodeproj
    ├── MainProject.xcworkspace
    ├── Podfile
    ├── Podfile.lock
    ├── Pods
    └── readme.md


    然后去A_Category下,在Podfile中添加一行pod "CTMediator",在podspec文件的后面添加s.dependency "CTMediator",然后执行pod install --verbose


    接下来打开A_Category.xcworkspace,把脚本生成的名为A_Category的空目录拖放到Xcode对应的位置下,然后在这里新建基于CTMediator的Category:CTMediator+A。最后你的A_Category工程应该是这样的:



    A_Category
    ├── A_Category
    |   ├── A_Category
    |   │   ├── CTMediator+A.h
    |   │   └── CTMediator+A.m
    |   ├── AppDelegate.h
    |   ├── AppDelegate.m
    |   ├── ViewController.h
    |   └── ViewController.m
    └── A_Category.xcodeproj

    到这里为止,A工程和A_Category工程就准备好了。




    实施组件化方案第二步:在主工程中引入A_Category工程,并让主工程编译通过



    去主工程的Podfile下添加pod "A_Category", :path => "../A_Category"来本地引用A_Category。


    然后编译一下,说找不到AViewController的头文件。此时我们把头文件引用改成#import <A_Category/CTMediator+A.h>


    然后继续编译,说找不到AViewController这个类型。看一下这里是使用了AViewController的地方,于是我们在Development Pods下找到CTMediator+A.h,在里面添加一个方法:



    • (UIViewController *)A_aViewController;



    再去CTMediator+A.m中,补上这个方法的实现,把主工程中调用的语句作为注释放进去,将来写Target-Action要用:

    • (UIViewController )A_aViewController
      {
      /

      AViewController *viewController = [[AViewController alloc] init];
      */
      return [self performTarget:@“A” action:@“viewController” params:nil shouldCacheTarget:NO];
      }



    补充说明一下,performTarget:@"A"中给到的@"A"其实是Target对象的名字。一般来说,一个业务Pod只需要有一个Target就够了,但一个Target下可以有很多个Action。Action的名字也是可以随意命名的,只要到时候Target对象中能够给到对应的Action就可以了。


    关于Target-Action我们会在第三步中去实现,现在不实现Target-Action是不影响主工程编译的。


    category里面这么写就已经结束了,后面的实施过程中就不会再改动到它了。


    然后我们把主工程调用AViewController的地方改为基于CTMediator Category的实现:

    UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController];
    [self.navigationController pushViewController:viewController animated:YES];



    再编译一下,编译通过。

    到此为止主工程就改完了,现在跑主工程点击这个按钮跳不到A页面是正常的,因为我们还没有在A工程中实现Target-Action。


    而且此时主工程中关于A业务的改动就全部结束了,后面的组件化实施过程中,就不会再有针对A业务线对主工程的改动了。




    实施组件化方案第三步:添加Target-Action,并让A工程编译通过



    此时我们关掉所有XCode窗口。然后打开两个工程:A_Category工程和A工程。


    我们在A工程中创建一个文件夹:Targets,然后看到A_Category里面有performTarget:@"A",所以我们新建一个对象,叫做Target_A


    然后又看到对应的Action是viewController,于是在Target_A中新建一个方法:Action_viewController。这个Target对象是这样的:

    头文件:
    #import <UIKit/UIKit.h>
    @interface Target_A : NSObject

    • (UIViewController )Action_viewController:(NSDictionary )params;

    @end

    实现文件:
    #import “Target_A.h”
    #import “AViewController.h”

    @implementation Target_A

    • (UIViewController )Action_viewController:(NSDictionary )params
      {
      AViewController *viewController = [[AViewController alloc] init];
      return viewController;
      }

    @end



    这里写实现文件的时候,对照着之前在A_Category里面的注释去写就可以了。


    因为Target对象处于A的命名域中,所以Target对象中可以随意import A业务线中的任何头文件。


    另外补充一点,Target对象的Action设计出来也不是仅仅用于返回ViewController实例的,它可以用来执行各种属于业务线本身的任务。例如上传文件,转码等等各种任务其实都可以作为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是可以写在Action方法里面的。


    换个角度说就是:Action具备调度业务线提供的任何对象和方法来完成自己的任务的能力。它的本质就是对外业务的一层服务化封装。


    现在我们这个Action要完成的任务只是实例化一个ViewController并返回出去而已,根据上面的描述,Action可以完成的任务其实可以更加复杂。


    然后我们再继续编译A工程,发现找不到BViewController。由于我们这次组件化实施的目的仅仅是将A业务线抽出来,BViewController是属于B业务线的,所以我们没必要把B业务也从主工程里面抽出来。但为了能够让A工程编译通过,我们需要提供一个B_Category来使得A工程可以调度到B,同时也能够编译通过。


    B_Category的创建步骤跟A_Category是一样的,不外乎就是这几步:新建Xcode工程、网页新建Repo、跑脚本配置Repo、添加Category代码。


    B_Category添加好后,我们同样在A工程的Podfile中本地指过去,然后跟在主工程的时候一样。


    所以B_Category是这样的:

    头文件: #import
    • (UIViewController )B_viewControllerWithContentText:(NSString )contentText;

    @end

    实现文件:
    #import “CTMediator+B.h”

    @implementation CTMediator (B)

    • (UIViewController )B_viewControllerWithContentText:(NSString )contentText
      {
      /*
      BViewController *viewController = [[BViewController alloc] initWithContentText:@“hello, world!”];
      /
      NSMutableDictionary params = [[NSMutableDictionary alloc] init];
      params[@“contentText”] = contentText;
      return [self performTarget:@“B” action:@“viewController” params:params shouldCacheTarget:NO];
      }

    @end



    此时再编译一下,编译通过了。注意哦,这里A业务线跟B业务线就已经完全解耦了,跟主工程就也已经完全解耦了。




    实施组件化方案最后一步:收尾工作、组件发版



    此时还有一个收尾工作是我们给B业务线创建了Category,但没有创建Target-Action。所以我们要去主工程创建一个B业务线的Target-Action。创建的时候其实完全不需要动到B业务线的代码,只需要新增Target_B对象即可:

    Target_B头文件:
    #import <UIKit/UIKit.h>
    @interface Target_B : NSObject

    • (UIViewController )Action_viewController:(NSDictionary )params;

    @end

    Target_B实现文件:
    #import “Target_B.h”
    #import “BViewController.h”

    @implementation Target_B

    • (UIViewController )Action_viewController:(NSDictionary )params
      {
      NSString contentText = params[@“contentText”];
      BViewController viewController = [[BViewController alloc] initWithContentText:contentText];
      return viewController;
      }

    @end


    这个Target对象在主工程内不存在任何侵入性,将来如果B要独立成一个组件的话,把这个Target对象带上就可以了。


    收尾工作就到此结束,我们创建了三个私有Pod:A、A_Category、B_Category。


    接下来我们要做的事情就是给这三个私有Pod发版,发版之前去podspec里面确认一下版本号和dependency。


    Category的dependency是不需要填写对应的业务线的,它应该是只依赖一个CTMediator就可以了。其它业务线的dependency也是不需要依赖业务线的,只需要依赖业务线的Category。例如A业务线只需要依赖B_Category,而不需要依赖B业务线或主工程。


    发版过程就是几行命令:



    git add .
    git commit -m "版本号"
    git tag 版本号
    git push origin master --tags
    ./upload.sh
    



    命令行cd进入到对应的项目中,然后执行以上命令就可以了。


    要注意的是,这里的版本号要和podspec文件中的s.version给到的版本号一致。upload.sh是配置私有Pod的脚本生成的,如果你这边没有upload.sh这个文件,说明这个私有Pod你还没用脚本配置过。


    最后,所有的Pod发完版之后,我们再把Podfile里原来的本地引用改回正常引用,也就是把:path...那一段从Podfile里面去掉就好了,改动之后记得commit并push。


    组件化实施就这么三步,到此结束。




    总结



    hard code


    这个组件化方案的hard code仅存在于Target对象和Category方法中,影响面极小,并不会泄漏到主工程的业务代码中,也不会泄漏到业务线的业务代码中。


    而且在实际组件化的实施中,也是依据category去做业务线的组件化的。所以先写category里的target名字,action名字,param参数,到后面在业务线组件中创建Target的时候,照着category里面已经写好的内容直接copy到Target对象中就肯定不会出错(仅Target对象,并不会牵扯到业务线本身原有的对象)。


    如果要消除这一层hard code,那么势必就要引入一个第三方pod,然后target对象所在的业务线和category都要依赖这个pod。为了消除这种影响面极小的hard code,而且只要按照章法来就不会出错。为此引入一个新的依赖,其实是不划算的。



    命名域问题


    在这个实践中,响应者的命名域并没有泄漏到除了响应者以外的任何地方,这就带来一个好处,迁移非常方便。


    比如我们的响应者是一个上传组件。这个上传组件如果要替换的话,只需要在它外面包一个Target-Action,就可以直接拿来用了。而且包Target-Action的过程中,不会产生任何侵入性的影响。


    例如原来是你自己基于AFNetworking写的上传组件,现在用了七牛SDK上传,那么整个过程你只需要提供一个Target-Action封装一下七牛的上传操作即可。不需要改动七牛SDK的代码,也不需要改动调用方的代码。倘若是基于URL注册的调度,做这个事情就很蛋疼。



    服务管理问题


    由于Target对象处于响应者的命名域中,Target对象就可以对外提供除了页面实例以外的各种Action。


    而且,由于其本质就是针对响应者对外业务逻辑的Action化封装(其实就是服务化封装),这就能够使得一个响应者对外提供了哪些Action(服务)Action(服务)的实现逻辑是什么得到了非常好的管理,能够大大降低将来工程的维护成本。然后Category解决了服务应该怎么调用的问题。


    但在基于URL注册机制和Protocol共享机制的组件化方案中,由于服务散落在响应者各处,服务管理就显得十分困难。如果还是执念于这样的方案,大家只要拿上面提到的三个问题,对照着URL注册机制和Protocol共享机制的组件化方案比对一下,就能明白了。


    另外,如果这种方案把所有的服务归拢到一个对象中来达到方便管理的目的的话,其本质就已经变成了Target-Action模式,Protocol共享机制其实就已经没有存在意义了。



    高内聚


    基于protocol共享机制的组件化方案导致响应者业务逻辑泄漏到了调用者业务逻辑中,并没有做到高内聚


    如果这部分业务在其他地方也要使用,那么代码就要重新写一遍。虽然它可以提供一个业务高内聚的对象来符合这个protocol,但事实上这就又变成了Target-Action模式,protocol的存在意义就也没有了。



    侵入性问题


    正如你所见,CTMediator组件化方案的实施非常安全。因为它并不存在任何侵入性的代码修改。


    对于响应者来说,什么代码都不用改,只需要包一层Target-Action即可。例如本例中的B业务线作为A业务的响应者时,不需要修改B业务的任何代码。


    对于调用者来说,只需要把调用方式换成CTMediator调用即可,其改动也不涉及原有的业务逻辑,所以是十分安全的。


    另外一个非侵入性的特征体现在,基于CTMediator的组件化方案是可以循序渐进地实施的。这个方案的实施并不要求所有业务线都要被独立出来成为组件,实施过程也并不会修改未组件化的业务的代码。


    在独立A业务线的过程中如果涉及其它业务线(B业务线)的调用,就只需要给到Target对象即可,Target对象本身并不会对未组件化的业务线(B业务线)产生任何的修改。而且将来如果对应业务线需要被独立出去的时候,也仅需要把Target对象一起复制过去就可以了。


    但在基于URL注册和protocol共享的组件化方案中,都必须要在未组件化的业务线中写入注册代码和protocol声明,并分配对应的URL和protocol到具体的业务对象上。这些其实都是不必要的,无端多出了额外维护成本。



    注册问题


    CTMediator没有任何注册逻辑的代码,避免了注册文件的维护和管理。Category给到的方法很明确地告知了调用者应该如何调用。


    例如B_Category给到的- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;方法。这能够让工程师一眼就能够明白使用方式,而不必抓瞎拿着URL再去翻文档。


    这可以很大程度提高工作效率,同时降低维护成本。



    实施组件化方案的时机


    MVP阶段过后,越早实施越好。


    这里说的MVP不是一种设计模式,而是最小价值产品的意思,它是产品演进的第一个阶段。


    一般来说天使轮就是用于MVP验证的,在这个阶段产品闭环尚未确定,因此产品本身的逻辑就会各种变化。但是过了天使轮之后,产品闭环已经确定,此时就应当实施组件化,以应对A轮之后的产品拓张。


    有的人说我现在项目很小,人也很少,所以没必要实施组件化。确实,把一个小项目组件化之后,跟之前相比并没有多大程度的改善,因为本来小项目就不复杂,改成组件化之后,也不会更简单。


    但这其实是一种很短视的认知。


    组件化对于一个小项目而言,真正发挥优势的地方是在未来的半年甚至一年之后。


    因为趁着人少项目小,实施组件化的成本就也很小,三四天就可以实施完毕。于是等将来一年之后业务拓张到更大规模时,就不会束手束脚了。


    但如果等到项目大了,人手多了再去实施组件化,那时候实施组件化的复杂度肯定比现在规模还很小的时候的复杂度要大得多,三四天肯定搞不定,而且实施过程还会非常艰辛。到那时你就后悔为什么当初没有早早实施组件化了。



    Swift工程怎么办?


    其实只要Target对象继承自NSObject就好了,然后带上@objc(className)。action的参数名永远只有一个,且名字需要固定为params,其它照旧。具体swift工程中target的写法参见A_swift

    因为Target对象是游离于业务实现的,所以它去继承NSObject完全没有任何问题。完整的SwiftDemo在这里。








    本文Demo




    展开全文
  • CTMediator解析

    2020-08-26 11:27:57
    模块解耦手段 实现模块之间真正的解耦才算是真正的模块化 自己的理解 1 面向接口调用(遵守协议,实现协议方法,依赖协议), 即新开一个对象ModuleManager,提供一个registerClass:forProtocol的方法,注册protocol与...

    模块解耦手段

    实现模块之间真正的解耦才算是真正的模块化

    自己的理解

    1 面向接口调用(遵守协议,实现协议方法,依赖协议), 即新开一个对象ModuleManager,提供一个registerClass:forProtocol的方法,注册protocol与class进行配对,调用是,通过protocol找到class返回给业务方,这里protocol的两个作用,1是key值,2是起到定义调用接口的作用,可以定义任意类型的参数
    2 面向自定义协议调用,采用现成的协议如url协议,统一实现本地和远程跳转,实现业务解耦,(真正解耦采用注册机制, 单例中有个字典属性,注册url的key,和block的值) (原理是将url与block进行映射, url起到两个作用,一个是作为key值与block进行映射, 二是可以直接接参数像普通url后面跟参数一样,如果传递非常规参数,可以在url后面添加param)
    1, 2同url注册形式一样,都得维持注册表。被调用方与调用方,虽然不相互依赖,但都得依赖这个协议
    3利用运行时的反射机制 OC的反射机制是通过一个字符串找到一个类的类对象
    基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分
    实现原理
    [[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{…}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。其中用CTMediator分类调用CTMediator类的方法时,实现了对修改关闭,对扩展开发的设计原则,也可以直接调用CTMediator

    CTMediator
     /*
     scheme://[target]/[action]?[params]
     url sample:
     aaa://targetA/actionB?id=1234
     */
    // 远程App调用入口
    - (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
    // 本地组件调用入口
    - (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget{
     
     Class targetClass = NSClassFromString(targetClassString);
            target = [[targetClass alloc] init];
    
    采用运行时构建可执行的NSInvocation。在内部都给其添加了前缀
     NSString * targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
     NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    
      if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
            }
    }
    
    - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {
     // 容错处理 ...
        if (strcmp(retType, @encode(CGFloat)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            CGFloat result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
        // ...
      // 重点:
        return [target performSelector:action withObject:params];
    }
    
    
    // CTMediator分类中调用原来类中的方法
    @implementation CTMediator (CTMediatorModuleAActions)
    - (UIViewController *)CTMediator_viewControllerForDetail
    {
        UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                             action:kCTMediatorActionNativeFetchDetailViewController
                                                        params:@{@"key":@"value"}
                                             shouldCacheTarget:NO
                                            ];
        if ([viewController isKindOfClass:[UIViewController class]]) {
            // view controller 交付出去之后,可以由外界选择是push还是present
            return viewController;
        } else {
            // 这里处理异常场景,具体如何处理取决于产品
            return [[UIViewController alloc] init];
        }
    }
    
    // Target_A
    @implementation Target_A
    - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
    {
        // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
        DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
        viewController.valueLabel.text = params[@"key"];
        return viewController;
    }
    
    // ViewController中调用CTMediator
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
        [tableView deselectRowAtIndexPath:indexPath animated:YES];
        if (indexPath.row == 0) {
            UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
            
            // 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
            [self presentViewController:viewController animated:YES completion:nil];
       }
    
    1. 从.h文件里可以看到,外部的调用将远程与本地分开,内部实现时远程利用了本地(通过解析url,将url转换成了本地的调用)。

    2. Mediator分别对每一个模块有个一个分类,提供对外部的调用的列表。这些分类被需要调用的模块所依赖。也就是只需要依赖Mediator就可以了,是单向依赖。

    3. 为了更好的实现组件对外接口的管理。此种方案专门针对每个模块有一个Target_A类似的对外服务接口的实现。

    4. 用户调用都是通过对Mediator的分类,对固定的模块的类的名字的反射,来对Target_A的调用,当然就调用到了A的服务。

    5. 此种方式为了使代码方便管理,会为每个模块提供Target和一个对Mediator的分类。

    6. Mediator与其分类可以是单独一个repo,方便其他组件依赖。也就是其他组件只依赖于这个中间件。解耦与组件化就完成了。

    对自己理解的总结

    组件化就是在与解耦,解耦的方式大致就是上面提到的三种方法(也可能有其他办法,但至少现在我看到的最好实践就这三)。然后是基于各个原理的工程化实践。从工程实践来看casa的Mediator+target-action更胜一筹。思路清晰,调用统一,没有注册机制的维护,模块的服务的实现(Target)在同一个地方,不用耦合到真正的模块里。
    多说一句,滴滴组件化,页面间的跳转采用openURL,页面在+(void)load方法里进行注册,ONERoute内部保存一份URL与Class的对应表。当调用openURL时,会查找到相应的类,然后生成相应的实例对象。

    大神思想和总结

    蘑菇街的原文地址在这里:《蘑菇街 App 的组件化之路》,没有耐心看完原文的朋友,我在这里简要介绍一下蘑菇街的组件化是怎么做的:

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

    这里的两步中,每一步都存在问题。
    第一步的问题在于,在组件化的过程中,注册URL并不是充分必要条件,组件是不需要向组件管理器注册Url的。而且注册了Url之后,会造成不必要的内存常驻,如果只是注册Class,内存常驻量就小一点,如果是注册实例,内存常驻量就大了。至于蘑菇街注册的是Class还是实例,Limboy分享时没有说,文章里我也没看出来,也有可能是我看漏了。不过这还并不能算是致命错误,只能算是小缺陷。
    真正的致命错误在第二步。在iOS领域里,一定是组件化的中间件为openUrl提供服务,而不是openUrl方式为组件化提供服务。

    什么意思呢?
    也就是说,一个App的组件化方案一定不是建立在URL上的,openURL的跨App调用是可以建立在组件化方案上的。当然,如果App还没有组件化,openURL方式也是可以建立的,就是丑陋一点而已。

    为什么这么说?
    因为组件化方案的实施过程中,需要处理的问题的复杂度,以及拆解、调度业务的过程的复杂度比较大,单纯以openURL的方式是无法胜任让一个App去实施组件化架构的。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度。关于非常规对象我会在详细讲解组件化方案时有一个辨析
    实际App场景下,如果本地组件间采用GET方式的URL调用,就会产生两个问题:

    根本无法表达非常规对象

    比如你要调用一个图片编辑模块,不能传递UIImage到对应的模块上去的话,这是一个很悲催的事情。 当然,这可以通过给方法新开一个参数,然后传递过去来解决。比如原来是:

    [a openUrl:"http://casa.com/detail?id=123&type=0"];
    

    同时就也要提供这样的方法:

    [a openUrl:"http://casa.com/detail" params:@{
        @"id":"123",
        @"type":"0",
        @"image":[UIImage imageNamed:@"test"]
    }]
    

    如果不像上面这么做,复杂参数和非常规参数就无法传递。如果这么做了,那么事实上这就是拆分远程调用和本地调用的入口了

    另外,在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。。。在文章下半部分给出的demo代码样例已经说明了业务工程师在本地间调用时,是不需要知道URL的,而且demo代码样例也阐释了如何解决业务工程师遇到传params容易懵逼的问题。

    URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折

    注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。

    由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

    小总结

    蘑菇街采用了openURL的方式来进行App的组件化是一个错误的做法,使用注册的方式发现服务是一个不必要的做法。而且这方案还有其它问题,随着下文对组件化方案介绍的展开,相信各位自然心里有数。

    正确的组件化方案

    先来看一下方案的架构图

    
                 --------------------------------------
                 | [CTMediator sharedInstance]        |
                 |                                    |
                 |                openUrl:       <<<<<<<<<  (AppDelegate)  <<<<  Call From Other App With URL
                 |                                    |
                 |                                    |
                 |                parseUrl            |
                 |                                    |
                 |                   |                |
                 |                   |                |
    .................................|...............................
                 |                   |                |
                 |                   |/               |
                 |                                    |
                 |  performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  Call From Native Module
                 |                                    |
                 |                   |                |
                 |                   |/               |
                 |                                    |
                 |             -------------          |
                 |             |           |          |
                 |             |  runtime  |          |
                 |             |           |          |
                 |             -------------          |
                 |               .       .            |
                 ---------------.---------.------------
                               .           .
                              .             .
                             .               .
                            .                 .
                           .                   .
                          .                     .
                         .                       .
                        .                         .
    -------------------.-----------      ----------.---------------------
    |                 .           |      |          .                   |
    |           Target            |      |           Target             |
    |                             |      |                              |
    |         /   |   \           |      |         /   |   \            |
    |        /    |    \          |      |        /    |    \           |
    |                             |      |                              |
    |   Action Action Action ...  |      |   Action Action Action ...   |
    |Business A                   |      | Business B                   |
    -------------------------------      --------------------------------
    

    这幅图是组件化方案的一个简化版架构描述,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务,

    调用方式

    先说本地应用调用,本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{…}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。

    在远程应用调用中,远程应用通过openURL的方式,由iOS系统根据info.plist里的scheme配置找到可以响应URL的应用(在当前我们讨论的上下文中,这就是你自己的应用),应用通过AppDelegate接收到URL之后,调用CTMediator的openUrl:方法将接收到的URL信息传入。当然,CTMediator也可以用openUrl:options:的方式顺便把随之而来的option也接收,这取决于你本地业务执行逻辑时的充要条件是否包含option数据。传入URL之后,CTMediator通过解析URL,将请求路由到对应的target和action,随后的过程就变成了上面说过的本地应用调用的过程了,最终完成响应。

    针对请求的路由操作很少会采用本地文件记录路由表的方式,服务端经常处理这种业务,在服务端领域基本上都是通过正则表达式来做路由解析。App中做路由解析可以做得简单点,制定URL规范就也能完成,最简单的方式就是scheme://target/action这种,简单做个字符串处理就能把target和action信息从URL中提取出来了。

    组件仅通过Action暴露可调用接口

    所有组件都通过组件自带的Target-Action来响应,也就是说,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

                --------------------------------
                |                              |
                |           Business A         |
                |                              |
                ---  ----------  ----------  ---
                  |  |        |  |        |  |
                  |  |        |  |        |  |
       ...........|  |........|  |........|  |...........
       .          |  |        |  |        |  |          .
       .          |  |        |  |        |  |          .
       .        ---  ---    ---  ---    ---  ---        .
       .        |      |    |      |    |      |        .
       .        |action|    |action|    |action|        .
       .        |      |    |      |    |      |        .
       .        ---|----    -----|--    --|-----        .
       .           |             |        |             .
       .           |             |        |             .
       .       ----|------     --|--------|--           .
       .       |         |     |            |           .
       .       |Target_A1|     |  Target_A2 |           .
       .       |         |     |            |           .
       .       -----------     --------------           .
       .                                                .
       .                                                .
       ..................................................
    

    大家可以看到,虚线圈起来的地方就是用于跨组件调用的target和action,这种方式避免了由BusinessA直接提供组件间调用会增加的复杂度,而且任何组件如果想要对外提供调用服务,直接挂上target和action就可以了,业务本身在大多数场景下去进行组件化改造时,是基本不用动的。

    复杂参数和非常规参数,以及组件化相关设计思路

    这里我们需要针对术语做一个理解上的统一:

    复杂参数是指由普通类型的数据组成的多层级参数。在本文中,我们定义只要是能够被json解析的类型就都是普通类型,包括NSNumber, NSString, NSArray, NSDictionary,以及相关衍生类型,比如来自系统的NSMutableArray或者你自己定义的都算。

    总结一下就是:在本文讨论的场景中,复杂参数的定义是由普通类型组成的具有复杂结构的参数。普通类型的定义就是指能够被json解析的类型。

    非常规参数是指由普通类型以外的类型组成的参数,例如UIImage等这些不能够被json解析的类型。然后这些类型组成的参数在文中就被定义为非常规参数。

    总结一下就是:非常规参数是包含非常规类型的参数。非常规类型的定义就是不能被json解析的类型都叫非常规类型。
    边界情况:
    假设多层级参数中有存在任何一个内容是非常规参数,本文中这种参数就也被认为是非常规参数。
    如果某个类型当前不能够被json解析,但通过某种转化方式能够转化成json,那么这种类型在场景上下文中,我们也称为普通类型。

    然后我来解释一下为什么应该由本地组件间调用来支持远程应用调用:

    在远程App调用时,远程App是不可能通过URL来提供非常规参数的,最多只能以json string的方式经过URLEncode之后再通过GET来提供复杂参数,然后再在本地组件中解析json,最终完成调用。在组件间调用时,通过performTarget:action:params:是能够提供非常规参数的,于是我们可以知道,远程App调用时的上下文环境以及功能是本地组件间调用时上下文环境以及功能的子集。

    因此这个逻辑注定了必须由本地组件间调用来为远程App调用来提供服务,只有符合这个逻辑的设计思路才是正确的组件化方案的设计思路,其他跟这个不一致的思路一定就是错的。因为逻辑上子集为父集提供服务说不通,所以强行这么做的话,用一个成语来总结就叫做倒行逆施。

    另外,远程App调用和本地组件间调用必须要拆分开,远程App调用只能走CTMediator提供的专用远程的方法,本地组件间调用只能走CTMediator提供的专用本地的方法,两者不能通过同一个接口来调用。

    这里有两个原因:
    远程App调用处理入参的过程比本地多了一个URL解析的过程,这是远程App调用特有的过程。这一点我前面说过,这里我就不细说了。

    架构师没有充要条件条件可以认为远程App调用对于无响应请求的处理方式和本地组件间调用无响应请求的处理方式在未来产品的演进过程中是一致的

    在远程App调用中,用户通过url进入app,当app无法为这个url提供服务时,常见的办法是展示一个所谓的404界面,告诉用户"当前没有相对应的内容,不过你可以在app里别的地方再逛逛"。这个场景多见于用户使用的App版本不一致。比如有一个URL只有1.1版本的app能完整响应,1.0版本的app虽然能被唤起,但是无法完成整个响应过程,那么1.0的app就要展示一个404了。

    在组件间调用中,如果遇到了无法响应的请求,就要分两种场景考虑了。

    场景1

    如果这种无法响应的请求发生场景是在开发过程中,比如两个组件同时在开发,组件A调用组件B时,组件B还处于旧版本没有发布新版本,因此响应不了,那么这时候的处理方式可以相对随意,只要能体现B模块是旧版本就行了,最后在RC阶段统测时是一定能够发现的,只要App没发版,怎么处理都来得及。

    场景2

    如果这种无法响应的请求发生场景是在已发布的App中,有可能展示个404就结束了,那这就跟远程App调用时的404处理场景一样。但也有可能需要为此做一些额外的事情,有可能因为做了额外的事情,就不展示404了,展示别的页面了,这一切取决于产品经理。

    那么这种场景是如何发生的呢?

    我举一个例子:当用户在1.0版本时收藏了一个东西,然后用户升级App到1.1版本。1.0版本的收藏项目在本地持久层存入的数据有可能是会跟1.1版本收藏时存入的数据是不一致的。此时用户在1.1版本的app中对1.0版本收藏的东西做了一些操作,触发了本地组件间调用,这个本地间调用又与收藏项目本身的数据相关,那么这时这个调用就是有可能变成无响应调用,此时的处理方式就不见得跟以前一样展示个404页面就结束了,因为用户已经看到了收藏了的东西,结果你还告诉他找不到,用户立刻懵逼。。。这时候的处理方式就会用很多种,至于产品经理会选择哪种,你作为架构师是没有办法预测的。如果产品经理提的需求落实到架构上,对调用入口产生要求然而你的架构又没有拆分调用入口,对于你的选择就只有两个:要么打回产品需求,要么加个班去拆分调用入口。

    当然,架构师可以选择打回产品经理的需求,最终挑选一个自己的架构能够承载的需求。但是,如果这种是因为你早期设计架构时挖的坑而打回的产品需求,你不觉得丢脸么?

    鉴于远程app调用和本地组件间调用下的无响应请求处理方式不同,以及未来不可知的产品演进,拆分远程app调用入口和本地组件间调用入口是功在当代利在千秋的事情。

    组件化方案中的去model设计

    组件间调用时,是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立。

    假设模块A和模块B之间采用model化的方案去调用,那么调用方法时传递的参数就会是一个对象。

    如果对象不是一个面向接口的通用对象,那么mediator的参数处理就会非常复杂,因为要区分不同的对象类型。如果mediator不处理参数,直接将对象以范型的方式转交给模块B,那么模块B必然要包含对象类型的声明。假设对象声明放在模块A,那么B和A之间的组件化只是个形式主义。如果对象类型声明放在mediator,那么对于B而言,就不得不依赖mediator。但是,大家可以从上面的架构图中看到,对于响应请求的模块而言,依赖mediator并不是必要条件,因此这种依赖是完全不需要的,这种依赖的存在对于架构整体而言,是一种污染。

    如果参数是一个面向接口的对象,那么mediator对于这种参数的处理其实就没必要了,更多的是直接转给响应方的模块。而且接口的定义就不可能放在发起方的模块中了,只能放在mediator中。响应方如果要完成响应,就也必须要依赖mediator,然而前面我已经说过,响应方对于mediator的依赖是不必要的,因此参数其实也并不适合以面向接口的对象的方式去传递。
    因此,使用对象化的参数无论是否面向接口,带来的结果就是业务模块形式上是被组件化了,但实质上依然没有被独立。

    在这种跨模块场景中,参数最好还是以去model化的方式去传递,在iOS的开发中,就是以字典的方式去传递。这样就能够做到只有调用方依赖mediator,而响应方不需要依赖mediator。然而在去model化的实践中,由于这种方式自由度太大,我们至少需要保证调用方生成的参数能够被响应方理解,然而在组件化场景中,限制去model化方案的自由度的手段,相比于网络层和持久层更加容易得多。

    因为组件化天然具备了限制手段:参数不对就无法调用!无法调用时直接debug就能很快找到原因。所以接下来要解决的去model化方案的另一个问题就是:如何提高开发效率。

    在去model的组件化方案中,影响效率的点有两个:调用方如何知道接收方需要哪些key的参数?调用方如何知道有哪些target可以被调用?其实后面的那个问题不管是不是去model的方案,都会遇到。为什么放在一起说,因为我接下来要说的解决方案可以把这两个问题一起解决。

    解决方案就是使用category

    mediator这个repo维护了若干个针对mediator的category,每一个对应一个target,每个category里的方法对应了这个target下所有可能的调用场景,这样调用者在包含mediator的时候,自动获得了所有可用的target-action,无论是调用还是参数传递,都非常方便。接下来我要解释一下为什么是category而不是其他:

    category本身就是一种组合模式,根据不同的分类提供不同的方法,此时每一个组件就是一个分类,因此把每个组件可以支持的调用用category封装是很合理的。

    在category的方法中可以做到参数的验证,在架构中对于保证参数安全是很有必要的。当参数不对时,category就提供了补救的入口。

    category可以很轻松地做请求转发,如果不采用category,请求转发逻辑就非常难做了。

    category统一了所有的组件间调用入口,因此无论是在调试还是源码阅读上,都为工程师提供了极大的方便。

    由于category统一了所有的调用入口,使得在跨模块调用时,对于param的hardcode在整个App中的作用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,因此是可以接受的。

    这里是业务方使用category调用时的场景,大家可以看到非常方便,不用去记URL也不用纠结到底应该传哪些参数。

    if (indexPath.row == 0) {
            UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
    
            // 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
            [self presentViewController:viewController animated:YES completion:nil];
        }
    
        if (indexPath.row == 1) {
            UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
            [self.navigationController pushViewController:viewController animated:YES];
        }
    
        if (indexPath.row == 2) {
            // 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
            [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
        }
    
        if (indexPath.row == 3) {
            // 这种场景下,参数有问题,因此需要在流程中做好处理
            [[CTMediator sharedInstance] CTMediator_presentImage:nil];
        }
    
        if (indexPath.row == 4) {
            [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
                // 做你想做的事
                NSLog(@"%@", info);
            }];
        }
    
    基于其他考虑还要再做的一些额外措施
    基于安全考虑

    我们需要防止黑客通过URL的方式调用本属于native的组件,比如支付宝的个人财产页面。如果在调用层级上没有区分好,没有做好安全措施,黑客就有通过safari查看任何人的个人财产的可能。

    安全措施其实有很多,大部分取决于App本身以及产品的要求。在架构层面要做的最基础的一点就是区分调用是来自于远程App还是本地组件,我在demo中的安全措施是采用给action添加native前缀去做的,凡是带有native前缀的就都只允许本地组件调用,如果在url阶段发现调用了前缀为native的方法,那就可以采取响应措施了。这也是将远程app调用入口和本地组件调用入口区分开来的重要原因之一。

    当然,为了确保安全的做法有很多,但只要拆出远程调用和本地调用,各种做法就都有施展的空间了。

    基于动态调度考虑

    动态调度的意思就是,今天我可能这个跳转是要展示A页面,但是明天可能同样的跳转就要去展示B页面了。这个跳转有可能是来自于本地组件间跳转也有可能是来自于远程app。

    做这个事情的切点在本文架构中,有很多个:
    以url parse为切点
    以实例化target时为切点
    以category调度方法为切点
    以target下的action为切点

    如果以url parse为切点的话,那么这个动态调度就只能够对远程App跳转产生影响,失去了动态调度本地跳转的能力,因此是不适合的。

    如果以实例化target时为切点的话,就需要在代码中针对所有target都做一次审查,看是否要被调度,这是没必要的。假设10个调用请求中,只有1个要被动态调度,那么就必须要审查10次,只有那1次审查通过了,才走动态调度,这是一种相对比较粗暴的方法。

    如果以category调度方法为切点的话,那动态调度就只能影响到本地件组件的跳转,因为category是只有本地才用的,所以也不适合。

    以target下的action为切点是最适合的,因为动态调度在一般场景下都是有范围的,大多数是活动页需要动态调度,今天这个活动明天那个活动,或者今天活动正在进行明天活动就结束了,所以产生动态调度的需求。我们在可能产生动态调度的action中审查当前action是否需要被动态调度,在常规调度中就没必要审查了,例如个人主页的跳转,商品详情的跳转等,这样效率就能比较高。

    大家会发现,如果要做类似这种效率更高的动态调度,target-action层被抽象出来就是必不可少的,然而蘑菇街并没有抽象出target-action层,这也是其中的一个问题。

    当然,如果你的产品要求所有页面都是存在动态调度需求的,那就还是以实例化target时为切点去调度了,这样能做到审查每一次调度请求,从而实现动态调度。

    说完了调度切点,接下来要说的就是如何完成审查流程。完整的审查流程有几种,我每个都列举一下:

    App启动时下载调度列表,或者定期下载调度列表。然后审查时检查当前action是否存在要被动态调度跳转的action,如果存在,则跳转到另一个action
    每一次到达新的action时,以action为参数调用API获知是否需要被跳转,如果需要被跳转,则API告知要跳转的action,然后再跳转到API指定的action

    这两种做法其实都可以,如果产品对即时性的要求比较高,那么采用第二种方案,如果产品对即时性要求不那么高,第一种方案就可以了。由于本文的方案是没有URL注册列表的,因此服务器只要给出原始target-action和对应跳转的target-action就可以了,整个流程不是只有注册URL列表才能达成的,而且这种方案比注册URL列表要更易于维护一些。

    另外,说采用url rewrite的手段来进行动态调度,也不是不可以。但是这里我需要辨析的是,URL的必要性仅仅体现在远程App调度中,是没必要蔓延到本地组件间调用的。这样,当我们做远程App的URL路由时(目前的demo没有提供URL路由功能,但是提供了URL路由操作的接入点,可以根据业务需求插入这个功能),要关心的事情就能少很多,可以比较干净。在这种场景下,单纯以URL rewrite的方式其实就与上文提到的以url parse为切点没有区别了。
    iOS应用架构谈 组件化方案

    展开全文
  • CTMediator架构Demo学习

    2019-07-11 05:58:35
    https://blog.csdn.net/DevanChen/article/details/52637365 转载于:https://www.cnblogs.com/-WML-/p/9493087.html

    https://blog.csdn.net/DevanChen/article/details/52637365

    转载于:https://www.cnblogs.com/-WML-/p/9493087.html

    展开全文
  • CTMediator的Swift应用

    2018-09-13 16:40:17
    如果你的工程是采用CTMediator方案做的组件化,看完本文以后,你就可以做到渐进式地迁移到Swift了。 CTMediator支持所有情况的调用,具体可以看文后总结。你的工程可以让Swift组件和Objective-C组件通过CTMediator...
  • CTMediator 学习使用总结

    千次阅读 2019-04-13 11:42:13
    先来看看 NSMethodSignature 和 NSInvocation 假设现在有一个方法 -(NSString *)school:(NSString *)name time:(NSInteger)t{ NSString *result = [NSString stringWithFormat:@"%@%d",name,t];...
  • 在上一篇文章中我们大概知道了 CTMetidor 中的 NSSelectorFromString、NSClassFromString 、SEL 这篇文章主要介绍一下 respondsToSelector、 performSelector 、...CTMediator 很有帮助 在介绍 performSelector...
  • 前两篇文章主要是对这篇文章的内容进行了一个铺垫,这里就一起来看下 CTMediator 的实现原理 ,CTMediator是一个单例,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用 CTMediator提供的...
  • ios业务模块间互相跳转的解耦方案

    万次阅读 2016-03-31 13:42:49
    一个app通常由许多个模块组成,所有模块之间免不了会相互调用,例如一个读书管理软件,可能会有书架、用户信息、图书详情等等模块,从用户信息-我读的书中,可以打开图书详情。而在图书详情-所在书架,又可以打开...
  • 前言:公司一年多的小项目,进行项目拆分,要求是每个业务模块都可以单独打包。在开发过程中,如:酒店模块,只修改酒店单元,测试也只测试酒店部分。模块间相互不干扰,就有了,今天组件化之路。...
  • 最近研究组件化、看了不少的资料,遇到很多坑,...一、同一个工程下组件化实现思路,主要用中间件CTMediator(把它理解成沟通的桥梁,比如很多控制器之间跳转是直接push的,那么用到中间件CTMediator 就是通过CTMe...
  • UIViewController *personalVC = [[CTMediator sharedInstance] Person_viewControllerWithUserDic:[self appDelegate].userInfDic LoginOutAction:^(NSDictionary *userInf){ NSLog(@"---LoginOutAction")...
  • 1,概述  CocoaPods是iOS,Mac下优秀的第三方包管理工具,给我们项目管理带来了极大的方便。个人或公司在开发过程中,会积累很多可以复用的代码包,有些我们不想开源,又想像开源库一样在CocoaPods中管理它们,那么...
  • 简述CTMediator CTMediator按照功能的结构来讲,使用时需要实现CTMediator的个三部分。 1.CTMediator类:承担总枢纽,总调度的责任 2.Target_(ModuleName)类:承担组件对外暴漏接口功能,组件要提供什么服务,...
  • 组件化

    2018-03-21 15:17:24
    1.调用方式本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的...
  • 5. BeeHive和CTMediator 1. 组件化是什么 这里的组件化一般是指业务模块化,简单来说就是将一个复杂的系统根据业务划分成不同的模块,这个没什么好说的,一般在做项目时,就已经做好了业务模块的划分。在讨论组件化...
  • iOS应用组件化/模块化探究

    千次阅读 2019-01-10 17:13:56
    组件化是近几年流行起的概念,它是当代码扩张到一定程度时,所采取的一种代码组织架构策略。淘宝、蘑菇街等大厂也在近几年陆续完成了其代码组件化的过程。 提到组件化,给人的感觉似乎很高大上,很神秘的感觉。...
  • iOS组件化开发

    千次阅读 2017-11-05 17:43:36
    一 . 为什么要组件化? 对于一些小的项目,有一个或者两三个人可以独立完成的项目,没有必要用组件化开发。组件化开发主要是为了解决项目越来越大,开发人员越来越多,项目耦合性高,不利于维护的问题。...
  • BeeHive 提供以key为@protocol(协议),value为class实现协议的键值对,当协议发生改变时候,编译阶段能够及时感知. BeeHive组件化产生背景: 1)、功能代码之间的依赖复杂,可维护性差; 2)、协同开发过程中,并行...
  • casatwy组件化方案

    2019-06-17 19:33:07
    整体架构casatwy组件化方案分为两种调用方式,远程调用和本地调用,对于两个不同的调用方式分别对应两个接口。远程调用通过AppDelegate代理方法传递到当前应用后,调用远程接口并在内部做一些处理,处理完成后会在...
1 2 3 4 5 ... 12
收藏数 230
精华内容 92
关键字:

ctmediator