组件化系列(一) 组件化原理及方案

2018/8/25 posted in  iOS性能优化

本系列文章是对大神的博客文章的研读与总结。我们公司从18年初开始组件化的开发的构建。为了理解组件开发的原理,找了相关资料做了对比分析。以下是我参考的文章
模块化与解耦
Casa的iOS应用架构谈 组件化方案
蘑菇街的开源
Casa的组件化方案开源

一:模块化与解耦

模块化与解耦

1.为什么模块化

因为在实际的开发中,项目业务较多,一个APP会有多个小组进行开发,比如我们公司的有数学组,语文组,英语组,商业组,等,出现的问题大多数情况下一个开发人员只关心我这个组的代码。这样在编译时实际上是编译整个项目,编译效率低。每个小组在同一个工程里增,删,改文件,xcode的工程文件会经常发生冲突(我们用git进行版本控制),合并代码时很痛苦。在整个项目查找自己忘记类名了的功能时,犹如大海捞针。有些基础模块核心模块需要专人维护,对基础的开发人员不开发,需要隔离基础库,也需要进行解耦。

这些理由已经足以说明进行模块化组件化的迫切了。

2. 模块设计原则

  1. 越底层的模块,应该越稳定,越抽象,越具有高度复用度。
  2. 不要让稳定的模块依赖不稳定的模块,减少依赖
  3. 提升模块的复用度,自完备性有时候要优于代码复用
  4. 每个模块只做好一件事情,不要让Common出现
  5. 按照你架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象,业务模块之间也尽量不要耦合

3. 模块解耦手段

要实现模块之间真正的解耦才算真正的模块化。
解耦目标:

在基于模块设计原则上让模块之间没有循环依赖,让业务模块之间解除依赖

公共模块可以通过架构设计来避免耦合业务。但业务模块之间还是会有耦合的。
业务模块之间的比如页面跳转,数据传递,怎么实现解耦不同业务模块之间的代码调用呢?

1. 面向接口调用(很妙)

直接引用有依赖的情况

//A 模块
- (void)getSomeDataFromB {
    B.getSomeData();
}
//B 模块
- (void)getSomeData {
    return self.data;
}
//接口
@protocol BService<NSObject>
- (void)getSomeData;
@end

//A 模块,只依赖接口(针对协议编程)
- (void)getSomeDataFromB {
    id b = findService(@protocol(BService));
    b.getSomeData;
}
//B模块,实现BService接口
@interface B:NSObject<BService>
- (void)getSomeData {
    return self.data;
}

这样就可以实现了既满足了模块之间的调用,也实现了解耦
优点

接口类似代码,可以非常灵活的定义函数和回调

缺点

1. 接口定义文件需要放在一个模块里以供依赖,但是这个模块不贡献代码。还好。
2. 使用麻烦,每个调用都需要定义一个service的接口,幷实现。

2. 面向自定义协议的调用

面向接口的调用的缺点导致幷不能满足所有的需求,也解耦的不彻底(对接口的依赖)。
终极手段就是通过定义一套自定义协议来实现模块间的通信,可以采用现成的协议如URL协议,简单,易于上手,这也是很多人采用url作为协议原因。可统一实现本地于远程的页面跳转,并且实现业务间的解耦。
要实现真正的解耦,采用注册机制。

3.利用运行时的反射机制

Object-C的反射机制是通过一个字符串找到一个类的类对象,即NSClassFromString();
要实现真正的解耦,可以采用通过反射机制获取类,在创建对象,实现跳转或者通信。这样就不用依赖”要跳转的类“了。

总结
以上的解耦方式是从模块化与解耦得到的启发。这些解耦方式无论是在平时开发中,还是要搭建组件化的框架都可以使用。具体的组件化框架我们将在下面对前人的组件化探索做下分析

二:模块拆分

  1. 基础库组件
    第三方库如AFNetWorking,SDWebImage等,还有一些工具也要从主库重剥离出来,形成自己的私有基础仓库。

  2. 服务组件

  3. 业务组件

https://blog.csdn.net/xinzhou201/article/details/51000807

  1. 拆分中遇到的问题 主工程与壳工程的pods版本的管理问题

三:蘑菇街组件化

蘑菇街的方案一

原理采用的是面向自定义协议的方式实现解耦。自定义协议采用的是现成的url协议,url协议成熟,方便,利用URL可以在这里做key值,方便的参数解析,与应用之间的调用相吻合。
嫌在这里看代码代码麻烦的话,可以下载我自己写的原理 Demo组件化原理分析

//Mediator3.h文件
typedef void(^componetBlock)(id param);
typedef id(^objectComponetBlock)(id param);

/*
 原理就是将url与Block进行映射
 url起到两个作用:一是作为key值与block进行映射,二是可以直接接参数就想普通url后面跟参数一样。
 
 特别说明:
 url传递参数受到一定的限制。比如对本地来说,需要传非常规的参数时,就办不到。
 这里我自己的Demo直接将参数放在了函数后面省去对url进行解析的操作。实际的开发中将这个参数从url里解析出来。如果要传递非常规的参数。也可以直接在后面添加有一个param。
 */

@interface Mediator3 : NSObject

+ (instancetype)shareInstance;

//指定相应的url的执行操作
//例如,只是简单的打开一个页面
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block;
//打开某个页面
- (void)openURL:(NSString *)url withParam:(id)param;


//指定相应rul的执行操作,并给一个返回值
//例如打开一个页面,或者在一个组件里面取值后返回来
- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block;
//取得某个组件操作后的值
- (id)objectForURL:(NSString *)url withParam:(id)param;
//Mediator3.m文件
@interface Mediator3()
@property (nonatomic, strong)NSMutableDictionary *cache;
@end

@implementation Mediator3

- (instancetype)init {
    if(self = [super init]){
        _cache = [[NSMutableDictionary alloc]init];
    }
    return self;
}

+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static Mediator3 *mediator = nil;
    dispatch_once(&onceToken, ^{
        
        mediator = [[self alloc]init];
    });
    return mediator;
}

- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componetBlock block = [self.cache objectForKey:url];
    if(block){
        block(param);
    }
}

- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (id)objectForURL:(NSString *)url withParam:(id)param{
    objectComponetBlock block = [self.cache objectForKey:url];
    if(block){
        return block(param);
    }
    return nil;
}

@end
//BookDetailViewController.m
@implementation BookDetailViewController

+ (void)load {
    //实现跳转功能
    [[Mediator3 shareInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(id param) {
        NSDictionary *paramDict = (NSDictionary *)param;
        BookDetailViewController *bookDetailVC = [[BookDetailViewController alloc]initWithBookId:paramDict[@"bookId"]];
        UINavigationController *nav = (UINavigationController *) [UIApplication sharedApplication].keyWindow.rootViewController;
        [nav pushViewController:bookDetailVC animated:YES];
    }];
    
    
    //只是取值的操作
    [[Mediator3 shareInstance]registerURLPattern:@"weread://bookCount" toObjectHandler:^id(id param) {
        //执行一定的操作后
        return @"5";
    }];
    
//ReadingViewController.m
- (void)viewDidLoad {
 [super viewDidLoad];
 //注册url与block的映射,实现调转的操作
    [[Mediator3 shareInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId":@"2"}];
    
    //注册url与block映射,实现只是取值的操作
    NSString *bookCount =  [[Mediator3 shareInstance] objectForURL:@"weread://bookCount" withParam:@""];
    NSLog(@"这是从某个组件取回来的值%@",bookCount);
    }

蘑菇街的方案二

实现原理:
面向接口调用,即新开了一个对象叫做ModuleManager,提供了一个registerClass:forProtocol:的方法,注册后,@protocol和Class进行配对。因此ModuleManager中就有了一个字典来记录这个配对。
当有涉及非常规参数的调用时,业务方就不会去使用[MGJRouter openURL:@"mgj://detail?id=404"]的方案了,转而采用ModuleManager的classForProtocol:方法。业务传入一个@protocol给ModuleManager,然后ModuleManager通过之前注册过的字典查找到对应的Class返回给业务方,然后业务方再自己执行alloc和init方法得到一个符合刚才传入@protocol的对象,然后再执行相应的逻辑。

这里的protocol统样起到两个作用,一是key值,另一个是起到定义调用接口的作用,可以定义任意类型的参数。

缺点
1. 被调用方与调用方,虽然不相互依赖,但都得依赖这个协议。这实际上是一种不彻底的解耦。

  1. 同url注册形式一样,都得维持注册表。

四:casa组件化

基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务

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

Xnip2018-09-04_16-42-52

下面是工程实践
下面是casa开源的实现的头文件。.m文件请自行下载

//casa开源的CTMeditor.h
@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;

@end

//CTMeditor.m
略
采用运行时构建可执行的NSInvocation。在内部都给其添加了前缀
 NSString * targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
 NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
//Target_B.h
@interface Target_B : NSObject

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)dict;

@end

//Target_B.m
#import "Target_B.h"
#import "DemoModuleBDetailViewController.h"
@implementation Target_B

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)param {
    DemoModuleBDetailViewController *bDetailVC = [[DemoModuleBDetailViewController alloc]init];
    return bDetailVC;
}
@end
//ViewController.m
//这是调用方
#import "CTMediator+CTMediatorModuleAActions.h"
#import "CTMediator+CTMediatorModuleBActions.h"
- (void)viewDidLoad {
//直接调用
  if (indexPath.row == 6) {
        [[CTMediator sharedInstance] performTarget:@"InvalidTarget" action:@"InvalidAction" params:nil shouldCacheTarget:NO];
    }
    //通过分类调用
    if(indexPath.row == 7){
        UIViewController *vc =  [[CTMediator sharedInstance]CTMediator_viewControllerForModuleBDetail:@{}];
        [self.navigationController pushViewController:vc animated:YES];
    }
}
  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时,会查找到相应的类,然后生成相应的实例对象。
最后强烈建议大家认真读Casa的iOS应用架构谈 组件化方案