随着移动互联网的不断发展,用户的需求越来越多,对App的用户体验也变的越来越高。为了更好的应对各种需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等复杂架构。更换适合业务的架构,是为了后期能更好的维护项目。
但是用户依旧不满意,继续对开发人员提出了更多更高的要求,不仅需要高质量的用户体验,还要求更多的功能体验,如哔哩哔哩客户端从原有的视频观看的基础之上逐步增加了直播、动态、IM、专栏、会员购、音乐等模块,可以说web网站所具有的功能都在移动端进行了实现。这样如果仅仅在 Xcode 目录这个层次进行分层已经是不够的了。不管你的目录是以业务进行划分还是以 M-V-C 三个部分进行划分,当业务量非常大(成百上千)的时候,你会发现,想找到某个具体业务的某部分代码简直是大海捞针。同时,由于所有文件都在一个 Project 里面,如果开发人员不注意的话,很容易出现头文件各种互相 include,产生各种混乱的依赖关系。另外我们想要测试某一个部分的功能时,就会产生很多不必要的额外工作。所以,这时我们想到了将整个APP根据业务的不同拆分成很多组件,每个组件可以单独编译运行进行测试,并且当我们参与项目的人员越来越多时,代码量越来越大时,单工程代码更加难以维护于是,也就有了组件化的概念,实际上组件化也就是模块化一种的表现方式。
关于组件化的优缺点,以及确定项目使用组件化如何对代码进行拆分不在本文的讨论之中。如果感兴趣可以参考下面几篇文章。
传统的页面之间的跳转以及通信都是直接通过import的方式进行导入操作,这也是刚接触iOS开发时最常用的方式。然而,项目越来越庞大,这种方式会导致代码之间直接的相互依赖、耦合严重,管理起来相当混乱,代码维护成本高。
所以,如果有一个中间模块(Mediator)负责对各个模块之间的通信进行协调,模块通过Mediator发起通信,然后由Mediator负责将信息传递到相应模块,这样以来就将模块之间的相互依赖进行了解耦合。
这样做还有一个问题,虽说模块之间不存在了依赖,但是每个模块和中间的通信模块Mediator都相互产生了依赖,所以最理想的方式就是下面这种:每个模块只需要做好自己的事情就好,然后中间通信模块Mediator则在各个组件中进行转发或者跳转。实现这一模式需要中间通信模块Mediator,通过某种方式能够找到每个组件,并且能调用该组件的方法。
这个问题可以归纳为如何在APP内组件间进行路由设计。我们将业务进行模块化的架构往往是为了:
- 代码拆分,将关联性强的基础服务代码或者业务代码抽调在一起,单独封版,独立开发
- 防止主工程越来越大,变得臃肿
所以相对应的,模块化就需要以下功能:
- 提供多个库之间的服务调用
- 保持库与库之间的独立、非强依赖
总的来说,模块化的重点还是如何去除多个模块之间的耦合,让每个模块在不强依赖的情况下可以调用其他模块的服务。现在在开源的方案中有以下三种方案被广泛使用。
1、利用url-scheme注册
2、Protocol-class注册
3、利用runtime实现的target-action方法
并各自有比较成熟的第三方库可供使用。如URL—Scheme库:
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这样非常规的对象是不行的。所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。出现这种情况的原因就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能(远程调用),所以这个方案是天生有缺陷的)
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路由方式的补充,通过这种方法可以实现组件间非常规数据的传递方式,以及对模块中方法的调用。
RunTime(target-action)
相较于url-scheme的方式进行组件间的路由,Runtime的方式借助了OC运行时的特征,实现了组件间服务的自动发现,无需注册即可实现组件间的调用。因此,不管是从维护性、可读性、扩展性来说都是一个比较完美些的解决方案。
#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];