iOS组件化中路由方案的分析

随着移动互联网的不断发展,用户的需求越来越多,对App的用户体验也变的越来越高。为了更好的应对各种需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等复杂架构。更换适合业务的架构,是为了后期能更好的维护项目。

​但是用户依旧不满意,继续对开发人员提出了更多更高的要求,不仅需要高质量的用户体验,还要求更多的功能体验,如哔哩哔哩客户端从原有的视频观看的基础之上逐步增加了直播、动态、IM、专栏、会员购、音乐等模块,可以说web网站所具有的功能都在移动端进行了实现。这样如果仅仅在 Xcode 目录这个层次进行分层已经是不够的了。不管你的目录是以业务进行划分还是以 M-V-C 三个部分进行划分,当业务量非常大(成百上千)的时候,你会发现,想找到某个具体业务的某部分代码简直是大海捞针。同时,由于所有文件都在一个 Project 里面,如果开发人员不注意的话,很容易出现头文件各种互相 include,产生各种混乱的依赖关系。另外我们想要测试某一个部分的功能时,就会产生很多不必要的额外工作。所以,这时我们想到了将整个APP根据业务的不同拆分成很多组件,每个组件可以单独编译运行进行测试,并且当我们参与项目的人员越来越多时,代码量越来越大时,单工程代码更加难以维护于是,也就有了组件化的概念,实际上组件化也就是模块化一种的表现方式。

​关于组件化的优缺点,以及确定项目使用组件化如何对代码进行拆分不在本文的讨论之中。如果感兴趣可以参考下面几篇文章。

iOS 混编 模块化/组件化 经验指北

蘑菇街 App 的组件化之路

传统的页面之间的跳转以及通信都是直接通过import的方式进行导入操作,这也是刚接触iOS开发时最常用的方式。然而,项目越来越庞大,这种方式会导致代码之间直接的相互依赖、耦合严重,管理起来相当混乱,代码维护成本高。


image.png

所以,如果有一个中间模块(Mediator)负责对各个模块之间的通信进行协调,模块通过Mediator发起通信,然后由Mediator负责将信息传递到相应模块,这样以来就将模块之间的相互依赖进行了解耦合。

image.png

这样做还有一个问题,虽说模块之间不存在了依赖,但是每个模块和中间的通信模块Mediator都相互产生了依赖,所以最理想的方式就是下面这种:每个模块只需要做好自己的事情就好,然后中间通信模块Mediator则在各个组件中进行转发或者跳转。实现这一模式需要中间通信模块Mediator,通过某种方式能够找到每个组件,并且能调用该组件的方法。


image.png

这个问题可以归纳为如何在APP内组件间进行路由设计。我们将业务进行模块化的架构往往是为了:

  1. 代码拆分,将关联性强的基础服务代码或者业务代码抽调在一起,单独封版,独立开发
  2. 防止主工程越来越大,变得臃肿

所以相对应的,模块化就需要以下功能:

  1. 提供多个库之间的服务调用
  2. 保持库与库之间的独立、非强依赖

总的来说,模块化的重点还是如何去除多个模块之间的耦合,让每个模块在不强依赖的情况下可以调用其他模块的服务。现在在开源的方案中有以下三种方案被广泛使用。

1、利用url-scheme注册

2、Protocol-class注册

3、利用runtime实现的target-action方法

并各自有比较成熟的第三方库可供使用。如URL—Scheme库:

  1. JLRoutes

  2. routable-ios

  3. HHRouter

  4. MGJRouter

Target-Action库:

​ 1、CTMediator

接下来对这三种方法的实现进行简单的介绍:

URL—Scheme

在iOS系统中默认是支持URL Scheme的方式,例如可以在浏览器中输入:weixin://

可以打开微信应用。自然在APP内部通过这种方法也能实现组件之间的路由设计。

这种方式实现的原理是:在APP启动的时候,或者向以下实例中的在每个模块自己的load方法里面注册自己的短链、以及对外提供服务(通过block)通过URL-scheme标记好,然后维护在URL-Router里面。

URL-Router中保存了各个组件对应的URL-scheme,只要其他组件调用了 open URL的方法,URL-Router就会去根据URL查找对应的服务并执行。

A_VC

@interface A_VC : UIViewController
-(void)action_A:(NSString*)para1;
@end


====================
#import "A_VC.h"
#import "URL_Roueter.h"
@implementation A_VC
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        [[self new] action_A:para1];
    }];
}

-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"调用组件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
}

-(void)btn_click{
    [[URL_Roueter sharedInstance] openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
}
-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}
@end

B_VC

@interface B_VC : UIViewController
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end
=======================
#import "B_VC.h"
#import "URL_Roueter.h"
@implementation B_VC
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://B_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        NSInteger para2 = [para[@"para2"]integerValue];
        NSInteger para3 = [para[@"para3"]integerValue];
        NSInteger para4 = [para[@"para4"]integerValue];
        [[self new] action_B:para1 para2:para2 para3:para3 para4:para4];
    }];
}
-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn setTitle:@"调用组件A" forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    self.view.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:btn];
}

-(void)btn_click{
    [[URL_Roueter sharedInstance]openURL:@"test://A_Action" withParam:@{@"para1":@"param1"}];
}

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3  {
    NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
}
@end

URL_Router

#import <Foundation/Foundation.h>
typedef void (^componentBlock) (NSDictionary *param);
@interface URL_Roueter : NSObject
+ (instancetype)sharedInstance;
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- (void)openURL:(NSString *)url withParam:(id)param;
@end
=================================
#import "URL_Roueter.h"

@interface URL_Roueter()
@property (nonatomic, strong) NSMutableDictionary *cache;
@end
@implementation URL_Roueter
+ (instancetype)sharedInstance
{
    static URL_Roueter *router;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        router = [[URL_Roueter alloc] init];
    });
    return router;
}

-(NSMutableDictionary *)cache{
    if (!_cache) {
        _cache = [NSMutableDictionary new];
    }
    return _cache;
}
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}
- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}
@end

这种方法会存在一些问题:

1、当组件多起来的时候,需要提供一个关于URL和服务的对应表,并且需要开发人员对这样一份表进行维护。

2、这种方式需要在应用启动时每个组件需要到路由管理中心注册自己的URL及服务,因此内存中需要保存这样一份表,当组件多起来之后会出现一些内存的问题。

3、混淆了本地调用和远程调用。
(a、远程调用和本地调用的处理逻辑是不同的,正确的做法应该是把远程调用通过一个中间层转化为本地调用,如果把两者两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面,但是本地调用可能需要做其他处理。如果不加以区分,那么久无法完成这种业务要求。
b、远程调用只能传能被序列化为json的数据,像 UIImage这样非常规的对象是不行的。所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。出现这种情况的原因就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能(远程调用),所以这个方案是天生有缺陷的)


URL组件化调用方式.png

protocol-class 「协议」 <-> 「类」绑定的方式

将各个模块提供的协议统一放在一个文件中(CommonProtocol.h),在各个模块中依赖这个文件,实现其协议。如:

CommonProtocol.h

#import <Foundation/Foundation.h>

@protocol A_VC_Protocol <NSObject>
-(void)action_A:(NSString*)para1;
@end

@protocol B_VC_Protocol <NSObject>
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end

中间件提供模块的注册和获取模块的功能,如:

ProtocolMediator.h

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- (Class)classForProtocol:(Protocol *)proto;

@end

ProtocolMediator.m

#import "ProtocolMediator.h"

@interface ProtocolMediator()
@property (nonatomic,strong) NSMutableDictionary *protocolCache;

@end
@implementation ProtocolMediator


+ (instancetype)sharedInstance
{
static ProtocolMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    mediator = [[ProtocolMediator alloc] init];
});
return mediator;
}

-(NSMutableDictionary *)protocolCache{
    if (!_protocolCache) {
        _protocolCache = [NSMutableDictionary new];
    }
    return _protocolCache;
}

- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
    [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}


@end

在各个模块中实现其协议

A模块:A_VC.h

#import <UIKit/UIKit.h>
#import "CommonProtocol.h"

@interface A_VC : UIViewController<A_VC_Protocol>
@end

A_VC.m

#import "A_VC.h"
#import "ProtocolMediator.h"


@implementation A_VC

+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];

}
     
     
-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}


-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"调用组件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    
}
@end

B模块同A模块相同,代码片段不贴出。

该方法是对URL路由方式的补充,通过这种方法可以实现组件间非常规数据的传递方式,以及对模块中方法的调用。

protocol-class.png

RunTime(target-action)

相较于url-scheme的方式进行组件间的路由,Runtime的方式借助了OC运行时的特征,实现了组件间服务的自动发现,无需注册即可实现组件间的调用。因此,不管是从维护性、可读性、扩展性来说都是一个比较完美些的解决方案。


image.png
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Mediator : NSObject
+(UIViewController *)AVC_viewcontroller:(NSString *)parent;
+(UIViewController *)BVC_viewcontroller:(NSInteger)type;
@end
============================
#import "Mediator.h"
@implementation Mediator
+ (UIViewController *)AVC_viewcontroller:(NSString *)parent{
    Class cls = NSClassFromString(@"A_VC");
  return  [cls performSelector:NSSelectorFromString(@"a_VC_detailViewController:") withObject:@{@"parent":parent}];
   
}
+(UIViewController *)BVC_viewcontroller:(NSInteger)type{
    Class cls = NSClassFromString(@"B_VC");
    return [cls performSelector:NSSelectorFromString(@"b_VC_detailViewController:") withObject:@{ @"type":@(33)  }];
}
@end

A_VC

#import <UIKit/UIKit.h>
#import "Mediator.h"
@interface A_VC : UIViewController
+(void)a_VC_detailViewController:(NSString *)parent;
@end
==================
+ (void)a_VC_detailViewController:(NSString *)parent{
    NSLog(@"======通过runtime进行调用 ====== ==%@", parent ); 
}
-(void)btn_click{
    [Mediator BVC_viewcontroller:1];
}

B_VC

#import <UIKit/UIKit.h>
#import "Mediator.h"
@interface B_VC : UIViewController 
+(void)b_VC_detailViewController:(NSInteger)type;
@end
============================================
-(void)b_VC_detailViewController:(NSInteger)type  {
  NSLog(@"======通过runtime进行调用%ld====== ==%@",(long)type );
}
-(void)btn_click{
    [Mediator AVC_viewcontroller:@"dsds"];
}

以上使用runtime的方式对组件间进行路由的一个小例子。由于受限于performSelector方法,最多只能传递两个参数。因此可以通过对组件增加一层wrapper,把对外提供的业务包装一次。
Target_B.h

#import <Foundation/Foundation.h>

@interface target_B : NSObject
-(void)B_Action:(NSDictionary*)para;

@end

Target_B.m

#import "target_B.h"
#import "B_VC.h"

@implementation target_B

-(void)B_Action:(NSDictionary*)para{
    NSString *para1 = para[@"para1"];
    NSInteger para2 = [para[@"para2"]integerValue];
    NSInteger para3 = [para[@"para3"]integerValue];
    NSInteger para4 = [para[@"para4"]integerValue];
    B_VC *VC = [B_VC new];
    [VC action_B:para1 para2:para2 para3:para3 para4:para4];
}
@end

组件A调用组件B的步骤变成如下:

A—》Mediator—>wrapper(B)—>B—>具体object

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

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

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

#import "CTMediator.h"

@interface CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;

@end



#import "CTMediator+B_VC_Action.h"

@implementation CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
}
@end

在调用的过程中使用如下:

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

推荐阅读更多精彩内容