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{ 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"
@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
|
#ifndef DetailModuleProtocol_h #define DetailModuleProtocol_h
#import <UIKit/UIKit.h>
#pragma mark - Model Protocols
@protocol DetailModelProtocol <NSObject> - (NSString*)name; - (NSString*)version; @end
@protocol DetailModuleProtocol <NSObject>
@required
- (UIViewController*)detailControllerWithPic:(UIImage*)pic callback:(void(^)(id<DetailModelProtocol>))callback;
@end
#endif
|
其他组件调用详情组件的服务时,只需查看此协议并调用其协议方法即可。为了便于查询,可将此协议导入到公共的协议头文件中:
1 2 3 4 5 6 7 8 9 10
|
#ifndef CommonProtocol_h #define CommonProtocol_h
#import "DetailModuleProtocol.h"
#endif
|
这样,别的组件调用详情组件的服务时,只需要导入此公共协议头文件即可。
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;
+ (BOOL)performActionWithUrl:(NSURL *)url completion:(void(^)(id info))completion;
@end
#import "ModuleProtocolMediator.h" #import <UIKit/UIKit.h>
struct CallResult { BOOL raiseError; id result; };
@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; }
+ (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]; NSString *protocolMethod = [[url.path stringByRemovingPercentEncoding] stringByReplacingOccurrencesOfString:@"/" withString:@""]; SEL selector = NSSelectorFromString(protocolMethod); 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]; 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;
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];
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.方案总结
- 定义协议:包括实体协议、组件对外提供服务的接口协议;
- 实现协议:提供协议实现类,处理来自组件外的服务调用,在
load
中注册本协议名
+实现类
;
- 路由服务:提供全局服务类,处理
协议名
+实现类
的映射存取,定义远程调用反射实现方案;
- 本地调用:以对方协议名为键,从服务类中取出协议实现者对象,调用组件的协议方法;
- 远程调用:从URL中反射协议实现者Target、SEL,通过
invocation
或perform
调用服务;
优点:
- 体现了面向协议的编程思想,组件间通过协议进行通信;
- 调用协议方法时有代码提示辅助,避免了明文硬编码的不便;
- 协议方法中可以定义任意个数和任意类型的参数;
- 组件间需要用到对方的实体时,只需替换为实体所继承的协议即可;
- 各组件单向依赖于服务中心,实现了以服务类为中心的解耦;
缺点:
- 组件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