组件化-面向协议方案

1.前言

URL反射方案各有自己的局限性:

  • URL路由方案支持的参数类型不够灵活;
  • 反射方案的中间通信层使用明文类名和接口名,硬编码极容易出错;
  • 两种方案中组件之间传值时需要引用对方的实体文件,这样就依然存在耦合问题;

本篇将要探讨的协议+服务注册+URL+反射方案,其中的协议就很好的解决了这些问题~

2.思路

组件化-面向协议

2.1.本地调用

组件将自己对外提供的服务以协议的形式进行声明,对其他所有组件可见;

组件内提供一个对外服务类,遵守此协议并实现协议方法,处理组件对外服务的具体内容;

独立出一个单例的中间层,以协议的类名为键,以协议的实现类为值,将此键值对注册到中间层

A组件调用B组件的服务时,A方从中间层以B组件对外暴露的服务协议为key,将实现了此协议的B服务类取出,实例化后调用对应的协议方法,运行时自动查找并调用B服务类中的具体实现,即可完成服务的调用。

2.2.远程调用

按约定格式传入URL,本应用的回调函数中,解析出URL中的协议名、协议方法和参数的字符串;

通过前篇中讲到的反射原理,从上一步的解析结果中得到真正的协议类名和协议方法SEL;

根据协议类名从服务注册中心查询其实现类,并创建其实例;

通过实例调用协议方法,由运行时调用具体的实现,从而完成本次服务调用;

3.示例

需求:从主页组件跳转到详情组件;返回时更新主页组件的标题。

3.1.目标组件

详情组件视图控制器:

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
// 头文件
#import "DetailModuleProtocol.h"
@interface DetailViewController : UIViewController
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, copy) void (^detailCallback) (id<DetailModelProtocol>); //回调中回传详情实体
@end

// 实现文件
#import "DetailModel.h"
@interface DetailViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *mIconImv;
@end

@implementation DetailViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.mIconImv.image = self.image;

UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:@"返回" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(onBackWithBlock) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:btn];
}

- (void)onBackWithBlock{
// block中回传详情实体
DetailModel *model = [[DetailModel alloc] init];
model.name = @"David";
model.version = @"1.0";
self.detailCallback(model); //回调信息

[self.navigationController popViewControllerAnimated:YES];
}
@end

详情实体文件:

1
2
3
4
5
6
7
8
#import "DetailModuleProtocol.h"

// 实现DetailModelProtocol协议
@interface DetailModel : NSObject<DetailModelProtocol>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *version;
@end
3.2.服务协议

详情组件对外提供服务的协议:

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
// DetailModuleProtocol.h

#ifndef DetailModuleProtocol_h
#define DetailModuleProtocol_h

#import <UIKit/UIKit.h>

#pragma mark - Model Protocols

/// 对应的是详情组件中的DetailModel实体,只不过是将实体的字段以协议方法的形式进行声明
@protocol DetailModelProtocol <NSObject>
- (NSString*)name;
- (NSString*)version;
@end

/**
*详情组件对外提供的所有服务,都在这里以协议方法的形式对外曝光;
*此协议对所有其他组件可见,所以其他组件导入此头文件即可知道详情组件有哪些服务可用;
*面向协议编程;
*/
@protocol DetailModuleProtocol <NSObject>

@required

/**
进入详情页

@param pic 图片
@param callback 从详情页返回时的回调
@return 返回详情页,以便主页调用push进入此页面
*/
- (UIViewController*)detailControllerWithPic:(UIImage*)pic callback:(void(^)(id<DetailModelProtocol>))callback;

@end

#endif /* DetailModuleProtocol_h */

其他组件调用详情组件的服务时,只需查看此协议并调用其协议方法即可。为了便于查询,可将此协议导入到公共的协议头文件中:

1
2
3
4
5
6
7
8
9
10
// CommonProtocol.h

#ifndef CommonProtocol_h
#define CommonProtocol_h

// 导入每个组件中声明对外服务接口的协议头文件

#import "DetailModuleProtocol.h"

#endif /* CommonProtocol_h */

这样,别的组件调用详情组件的服务时,只需要导入此公共协议头文件即可。

3.3.服务类

详情组件中实现了服务协议的类,其中提供了协议方法的具体实现,即组件对外服务的具体内容:

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
#import "DetailProtocolImplementor.h"
#import "DetailViewController.h"
#import "ModuleProtocolMediator.h"
#import "DetailModuleProtocol.h"

// 封装了详情组件对外提供服务的具体实现
@interface DetailProtocolImplementor ()<DetailModuleProtocol>
@end

@implementation DetailProtocolImplementor

// 实现协议 提供具体服务
- (UIViewController *)detailControllerWithPic:(UIImage *)pic
callback:(void (^)(id<DetailModelProtocol>))callback
{
// 具体实现
DetailViewController *vc = [[UIStoryboard storyboardWithName:@"Main" bundle:nil]
instantiateViewControllerWithIdentifier:@"DetailViewController"];
vc.image = pic;
vc.detailCallback = callback;

return vc;
}

+ (void)load{
// 注册协议 将协议与实现了此协议的业务类进行绑定
[ModuleProtocolMediator registerModuleProtocolImplementor:[self class]
forProtocol:@protocol(DetailModuleProtocol)];
}
@end

注意这里的+load方法,它调用了中间层注册协议,注册操作的具体实现且往下看~

3.4.注册协议

这是本方案的核心类,属于中间层,负责本地服务注册、查询、远程调用:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// 头文件
@interface ModuleProtocolMediator : NSObject

// 单例类
+ (instancetype)shareInstance;

// 将实现了协议的服务类注册到全局字典中
+ (void)registerModuleProtocolImplementor:(Class)implementor
forProtocol:(Protocol*)protocol;

// 根据协议查询实现了此协议的服务类,并返回它的一个实例
+ (id)protocolImplementorWithProtocol:(Protocol*)protocol;

/**
远程调用接口
@param url 按照约定好的格式传值
@param completion 回调
@return 返回值 是否响应此URL
*/
+ (BOOL)performActionWithUrl:(NSURL *)url
completion:(void(^)(id info))completion;

@end

// 实现文件
#import "ModuleProtocolMediator.h"
#import <UIKit/UIKit.h>

struct CallResult {
BOOL raiseError; // 是否产生异常错误(如target或SEL为nil)
id result; // perform 或 invocation 的返回值
};

@interface ModuleProtocolMediator ()
@property (nonatomic, strong) NSMutableDictionary *mProtocolsDic; // 保存协议与实现类的全局容器
@end

@implementation ModuleProtocolMediator

+ (instancetype)shareInstance{

static ModuleProtocolMediator *mProtocolManager;

static dispatch_once_t token;
dispatch_once(&token, ^{
mProtocolManager = [[ModuleProtocolMediator alloc] init];
});

return mProtocolManager;
}

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

// 注册 将协议与实现类进行绑定 以便后续查找此类
+ (void)registerModuleProtocolImplementor:(Class)implementor
forProtocol:(Protocol*)protocol{

NSString *key = NSStringFromProtocol(protocol);
[ModuleProtocolMediator shareInstance].mProtocolsDic[key] = implementor;
}

// 查找实现了此协议的类 生成其实例
+ (id)protocolImplementorWithProtocol:(Protocol*)protocol{

NSString *key = NSStringFromProtocol(protocol);
Class className = [ModuleProtocolMediator shareInstance].mProtocolsDic[key];
id obj = [[className alloc] init];

return obj;
}

/* url示例:@"DaModuleProtocolScheme://DetailModuleProtocol:detailControllerWithPic:callback:"
* scheme://[protocolName]:[protocolMethod]?key1=value1&key2=value2
* [scheme] [host] [path] [query]
*/
+ (BOOL)performActionWithUrl:(NSURL *)url
completion:(void (^)(id info))completion
{
NSMutableArray *params = [[NSMutableArray alloc] init];
NSString *urlString = [url query];
for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
NSArray *elts = [param componentsSeparatedByString:@"="];
if([elts count] < 2) continue;
[params addObject:[elts lastObject]];
}
// 反射协议对象
Protocol *protocol = NSProtocolFromString(url.host);
// 调用本类查询协议的实现类
NSObject *target = [self protocolImplementorWithProtocol:protocol];
// 取出想调用的协议方法(URL Decode)
NSString *protocolMethod = [[url.path stringByRemovingPercentEncoding]
stringByReplacingOccurrencesOfString:@"/" withString:@""];
// 反射SEL,即协议中定义的方法
SEL selector = NSSelectorFromString(protocolMethod);

// 调用 perform 方法或者 NSInvocation 实现通信
struct CallResult retStruct = [self safePerformAction:selector target:target params:params];

if (retStruct.raiseError) {
return NO;
}else{
id value = retStruct.result;
if (completion) {
completion(value);
}
return YES;
}
}


+ (struct CallResult)safePerformAction:(SEL)action
target:(NSObject *)target
params:(NSArray *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
struct CallResult retStruct = {YES,nil};
return retStruct;
}
const char* retType = [methodSig methodReturnType];

if (strcmp(retType, @encode(void)) == 0 ||
strcmp(retType, @encode(BOOL)) == 0 ||
strcmp(retType, @encode(CGFloat)) == 0 ||
strcmp(retType, @encode(NSInteger)) == 0 ||
strcmp(retType, @encode(NSUInteger)) == 0)
{
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:action];
[invocation setTarget:target];

NSInteger count = params.count;
for (int i = 0; i < count; i++) {
NSObject *value = params[i];
[invocation setArgument:&value atIndex:i + 2]; // 参数从第二位开始算起
}
[invocation invoke];

// 无返回值时,结构体中result字段为nil
if (strcmp(retType, @encode(void)) == 0) {
struct CallResult retStruct = {NO,nil};
return retStruct;
}
else { // 返回值为数值类型时,包装成对象
NSInteger result = 0;
[invocation getReturnValue:&result];
struct CallResult retStruct = {NO,@(result)};
return retStruct;
}
}


#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 返回值为对象类型,直接调用即可
id result = [target performSelector:action withObject:params];
struct CallResult retStruct = {NO,result};
return retStruct;
#pragma clang diagnostic pop
}
@end

此工具类也提供了本地调用远程调用两种调用组件的方式。

  • 本地调用

本地调用组件时,ModuleProtocolMediator作为中间层通过下面的接口将协议与实现了此协议的服务类进行绑定,并保存到内部的字典中:

1
2
+ (void)registerModuleProtocolImplementor:(Class)implementor
forProtocol:(Protocol*)protocol;

在A组件调用B组件的服务时,只需要通过下面的接口,根据B组件提供的协议取出协议实现类,实例化之后调用对应的协议方法即可。

1
+ (id)protocolImplementorWithProtocol:(Protocol*)protocol;
  • 远程调用

此处的远程调用与反射方案中远程调用的方式相同,A应用按照约定的格式组合一个URL并传递给B应用,随后B应用的application:openURL:options:回调中调用以下方法对URL进行解析:

1
2
+ (BOOL)performActionWithUrl:(NSURL *)url
completion:(void(^)(id info))completion;

在此方法的实现中解析出协议名和协议方法SEL;以解析出来的协议名作为key,从本工具类中取出实现了此协议的组件服务类;随后创建其实例并通过 perform 或 NSInvocation 调用反射出来的协议方法,完成本次远程调用。可参考我在上面代码中的注释。

3.5.调用服务
  • 本地调用

主页组件中调用详情组件,跳转到返回的详情组件实例:

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
#import "HomeViewController.h"
#import "ModuleProtocolMediator.h" // 导入中间层
#import "CommonProtocol.h" // 导入公共协议头文件

@implementation HomeViewController

- (IBAction)onShowDetail:(id)sender {

UIButton *btn = sender;

/* 调用详情组件 因为导入了 ModuleProtocolMediator 和 DetailModuleProtocol,
*所以当前类中直到详情组件提供了哪些服务接口,调用这些接口即可~
*当前类只跟ModuleProtocolMediator交互,不与DetailViewController耦合~
*/
id<DetailModuleProtocol> implementor = [ModuleProtocolMediator protocolImplementorWithProtocol:
@protocol(DetailModuleProtocol)];

// 调用协议接口 传递参数 获取返回值
UIViewController *vc = [implementor detailControllerWithPic:btn.currentBackgroundImage
callback:^(id<DetailModelProtocol> model) {
self.title = model.name; // 从详情中返回时更新标题(注意,这里并未导入详情实体,而是实体的协议类)
}];
[self.navigationController pushViewController:vc animated:YES];
}
@end

主页组件调用了详情组件但并未引用详情组件的头文件;详情组件的回调中返回了详情实体,但也只是用了协议的形式,并未真的导入DetailModel.h实体。所以从整体上来说,Home组件只导入了中间层和公共协议头文件。

  • 远程调用

A应用中按照约定的格式组装URL并跳转:(注意SEL部分要进行编码)

1
2
3
4
5
6
7
8
9
10
11
12
NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:@":"] invertedSet];
NSString *scheme = @"DaModuleProtocolScheme";
NSString *protocol = @"DetailModuleProtocol";
NSString *SELStr = [@"detailControllerWithPic:callback:"
stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];

NSString *params = [@"k1=1&k2=2" stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
NSString *urlStr = [NSString stringWithFormat:@"%@://%@:/%@?%@",scheme,protocol,SELStr,params];
// URL示例:@"DaModuleProtocolScheme://DetailModuleProtocol:/detailControllerWithPic:callback:?k1=1&k2=2";

NSURL *url = [NSURL URLWithString:urlStr];
[[UIApplication sharedApplication] openURL:url];

当前应用的AppDelegate回调中调用注册中心提供的远程调用接口:

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [ModuleProtocolMediator performActionWithUrl:url completion:^(id info) {
NSLog(@"++++%@",info);
}];
}

performActionWithUrl接口的具体实现可参考3.4.注册协议章节中的ModuleProtocolMediator类。此方法借鉴了前两篇文章中分析过的URL反射原理,反射出协议类名和方法名,再通过注册中心查找并创建实现了此协议的类的实例,最后利用运行时接口调用协议方法,我已在上面的代码中已经做了详细注释。

3.6.方案总结
  1. 定义协议:包括实体协议、组件对外提供服务的接口协议;
  2. 实现协议:提供协议实现类,处理来自组件外的服务调用,在load中注册本协议名+实现类
  3. 路由服务:提供全局服务类,处理协议名+实现类的映射存取,定义远程调用反射实现方案;
  4. 本地调用:以对方协议名为键,从服务类中取出协议实现者对象,调用组件的协议方法;
  5. 远程调用:从URL中反射协议实现者Target、SEL,通过invocationperform调用服务;

优点:

  • 体现了面向协议的编程思想,组件间通过协议进行通信;
  • 调用协议方法时有代码提示辅助,避免了明文硬编码的不便;
  • 协议方法中可以定义任意个数和任意类型的参数;
  • 组件间需要用到对方的实体时,只需替换为实体所继承的协议即可;
  • 各组件单向依赖于服务中心,实现了以服务类为中心的解耦;

缺点:

  • 组件A向B传递N个参数,就需要在B组件的服务协议方法中定义N个参数,不如传实体方便,但A一旦引入B的实体文件就产生了依赖;
  • 过多的在组件对外服务类的+load方法中注册服务协议,会影响应用的启动速度。

4.目录结构

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
.
├── DaModule+Protocol
│   ├── AppDelegate.h
│   ├── AppDelegate.m
│   ├── Assets.xcassets
│   │   ├── AppIcon.appiconset
│   │   │   └── Contents.json
│   │   └── Contents.json
│   ├── Base.lproj
│   │   ├── LaunchScreen.storyboard
│   │   └── Main.storyboard
│   ├── Detail
│   │   ├── Controllers
│   │   │   ├── DetailViewController.h
│   │   │   └── DetailViewController.m
│   │   ├── Model
│   │   │   ├── DetailModel.h
│   │   │   └── DetailModel.m
│   │   ├── Protocol+IMPer
│   │   │   ├── DetailProtocolImplementor.h
│   │   │   └── DetailProtocolImplementor.m
│   │   └── Protocols
│   │   └── DetailModuleProtocol.h
│   ├── Home
│   │   ├── HomeViewController.h
│   │   └── HomeViewController.m
│   ├── Icons
│   │   ├── 1@2x.png
│   │   └── 1@3x.png
│   ├── Info.plist
│   ├── ProtocolMediator
│   │   ├── CommonProtocol.h
│   │   ├── ModuleProtocolMediator.h
│   │   └── ModuleProtocolMediator.m
│   └── main.m
└── DaModule+Protocol.xcodeproj

5.结尾

基于面向协议编程的协议+服务注册+URL+反射方案很好的解决了单纯URL反射方案的缺点;它也有缺点,但显然优点大于缺点,所以还是很值得推荐的。目前有赞开源的Bifrost库除了URL路由方案外,也提供了基于协议的组件化方案,后续有时间会单开一篇梳理一下这个库~


相关参考:

#©服务协议注册方案demo

#©反射方案demo

#©URL+Block方案demo

#©有赞Bifrost


组件化-面向协议方案
https://davidlii.cn/2020/12/21/modules-protocol.html
作者
Davidli
发布于
2020年12月21日
许可协议