iOS项目组件化解耦

最近给公司的一个iOS项目进行组件化解耦。本身项目早期开发就不是很规范,而且刚刚开始熟悉这个项目对业务方面也不是很熟悉所以并没有对所有的模块进行组件化。而且组件化解耦后还存在一些问题在文章中都会写出来。
原理和蘑菇街 App 的组件化之路类似,但是也有一些不同并没有加入「组件A」要调用「组件B」的某个方法这种业务场景。所有组件化的模块都是「组件A」要调用「组件B」的这种情况。「组件A」与「组件B」之间是透明的

为何要对项目组件化

  • 对每个模块间相互调用解耦
  • 统一wap与本地调用

首先说一下组件化带来的最大好处就是给每个模块间解耦。之前模块间调用不得不相互引用,这就导致了各个模块间相互依赖。想象一种场景:A,B,C,D是四个VC,四个VC之间是这样的关系,A与B相互跳转、B与C相互跳转、D与A,B相互跳转。它们之间的关系如下图:

而组件化在项目中引入了Mediator对象与Action对象,引入这两个对象后,A,B,C,D之间的关系如下图:

这样结构就很清楚了而且不管多少个组件结构都是这个样子。下面会说明Mediator对象与Action对象的实现。


对于一个App来说必不可免的会有wap页面调用Native的功能。通过URL注册实现组件化方案可以实现同一个URL即可以在本地调用组件也可以在wap页面调用组件。这样原本要在两处同时处理的逻辑统一放在了Mediator对象中处理。
当然这样做有一个缺点就是如果wap页面调用与本地调用同一个模块需要做不同的处理时无法区分是在哪里调用的,因为都是通过一样的URL来调用的。目前在我优化的项目中并没有这样的需求。所以当前的方案是可行的。如果真的要区分的话可以通过传递不同的参数来确定是从哪里发起的调用。


组件化方案的实现

通过程序启动时注册URL来实现组件化,URL注册用通过JLRoutes实现的。
具体使用如下:

1
2
3
4
5
6
7
8
9
10

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

JLRoutes *routes = [JLRoutes routesForScheme:scheme_url];

[routes addRoute: LSYKey1 handler:^BOOL(NSDictionary<NSString *,NSString *> * _Nonnull parameters) {
//要处理的逻辑
return YES; //判断返回值来决定是否执行该处代码
}];
}

在上面的代码中scheme_url是app自定义的协议,LSYKey1URL的路径,parametersURL的参数。通过程序启动初始化JLRoutes对象,并注册不同的URL,当尝试打开scheme_url协议的URL时就会执行对应注册路径下block内的代码。

1
2
3
4
5
6
7
-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation   //当程序尝试打开URL或者处理完从其它应用返回时会调用该方法 (不同版本的系统需要实现的方法不同)
{
if ([url.scheme isEqualToString:scheme_url]) {
return [JLRoutes routeURL:url];
}
return YES;
}

如果尝试打开scheme_url://LSYKey1?oid=123&amount=456这样的url时就会执行上面代码中注册的block。通过parameters取得该url的参数。

1
2
3
4
5
6
7
8
JLRoutes *routes = [JLRoutes routesForScheme:scheme_url];

[routes addRoute: LSYKey1 handler:^BOOL(NSDictionary<NSString *,NSString *> * _Nonnull parameters) {
NSInteger oid = [parameters[@"oid"] integerValue]; //获取url中oid参数
NSInteger amount = [parameters[@"amount"] integerValue]; //获取url中amount参数

return YES;
}];

上面说明如何通过JLRoutes注册URL的,具体组件化解耦是Mediator对象与Action对象配合JLRoutes来实现的。
首先我们需要有一个文件来统一管理本地模块的Key值,这里在ComponentKey.h文件中进行管理

ComponentKey.h

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

static NSString * const LSYKey1 = @"LSYKey1";

static NSString * const LSYKey2 = @"LSYKey2";

static NSString * const LSYKey3 = @"LSYKey3";

...

Mediator对象实现的方法如下:

Mediator.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import <Foundation/Foundation.h>

@interface Mediator : NSObject

/**
JLRountes注册的url
*/
+(void)componentRegister;

/**
本地通过key打开模块

@param key 模块key
@param dic 传递的参数
*/
+(void)openComponentForKey:(NSString *)key parameter:(NSDictionary *)dic;

/**
该方法通过运行时的机制让Mediator对象与Action对象解耦并将消息转发给Action对象执行

@param targetName Action对象名
@param actionName 方法名
@param params 传递的参数
*/
+(void)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
@end

Mediator.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@implementation Mediator


+(void)componentRegister
{
JLRoutes *routes = [JLRoutes routesForScheme:scheme_url];

[routes addRoute: LSYKey1 handler:^BOOL(NSDictionary<NSString *,NSString *> * _Nonnull parameters) {
[self performTarget:@"Action" action:@"performActionA" params:parameters];
return YES;
}];

[routes addRoute: LSYKey2 handler:^BOOL(NSDictionary<NSString *,NSString *> * _Nonnull parameters) {
[self performTarget:@"Action" action:@"performActionB" params:parameters];
return YES;
}];


}
+(void)openComponentForKey:(NSString *)key parameter:(NSDictionary *)dic
{
//将本地调用传入的Key与参数转拼接成url最后通过JLRoutes处理
if (!key.length) {
return;
}
NSMutableString *urlString = [NSMutableString stringWithString:[NSString stringWithFormat:@"%@://%@",scheme_url,key]];
[urlString appendString:@"?"];
[dic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[urlString appendString:[NSString stringWithFormat:@"%@=%@&",key,obj]];
}];
[urlString deleteCharactersInRange:NSMakeRange(urlString.length-1, 1)];
NSURL *url = [NSURL URLWithString:[urlString copy]];
[JLRoutes routeURL:url];
}

+(void)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params
{
NSString *targetClassString = [NSString stringWithFormat:@"%@", targetName];
NSString *actionString = [NSString stringWithFormat:@"%@:", actionName];

Class targetClass = NSClassFromString(targetClassString);
id target = [[targetClass alloc] init];
SEL action = NSSelectorFromString(actionString);

if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 这里是处理无响应请求的地方

}
}
}
@end


Action对象是处理从Mediator对象转发过来的消息。上面分别执行了performActionAperformActionB的方法。我们可以根据功能给这些方法分类,如果performActionAperformActionB的方法都是执行跳转操作,那么我们添加一个Action(Jump)的category专门来处理跳转操作。将这两个方法写入category中。

1
2
3
4
5
6
7
8
9
10
11
12
#import "Action+Jump.h"

@implementation Action (Jump)
-(void)performActionA:(NSDictionary *)params
{
//跳转A界面
}
-(void)performActionB:(NSDictionary *)params
{
//跳转B界面
}
@end

此时我们想要在本地调用LSYKey1模块并要传入oid与amount这两个参数只要执行下面代码即可:

1
[Mediator openComponentForKey:LSYKey1 parameter:@{@"oid":@123,@"amount":@"456"}];

远程调用直接返回:

1
[JLRoutes routeURL:url];

因为我们在程序启动已经注册了所有的url

1
[Mediator componentRegister];

存在的问题

上面虽然实现了组件化但是还是存在一些问题需要后续改进

  • 本地调用与远程调用混在一起无法区别开
  • 无法实现「组件A」要调用「组件B」的某个方法这种业务场景
  • 组件过多有可能影响程序启动速度