1.前言
上一篇文章中分析了组件化的目标和好处,同时梳理了基于URL+Block
的路由组件化解决方案的原理、实践和优缺点。本文将继续介绍第二种解决方案:基于反射原理的组件化方案。
2.场景
从主页
跳转到详情
,传入详情页所需的头像;
在详情中简单的修改信息之后,返回主页时更新主页的标题。
3.思路
基于反射原理的组件化方案,其首要目标还是实现各组件之间的通信,同时减少组件间的耦合。其实现原理如下:
3.1.反射类名
利用运行时接口将类名反射成类,如目标组件的类名:
1
| Class *targetObj = NSClassFromString(NSString *aClassName)
|
3.2.反射方法名
将方法名反射成SEL,如目标组件提供的接口:
1
| SEL sel = NSSelectorFromString(NSString *aSelectorName)
|
3.3.调用
通过 [targetObj performSelector:sel withObject:params] 实现与目标组件的通信:
1
| - (id)performSelector:(SEL)aSelector withObject:(id)object;
|
3.4.优化
由于 performSelector 传递的参数个数有限,且返回值只能为id类型,所以可使用 NSInvocation 作为备用方案:
1 2 3 4 5 6 7 8
| + (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (nullable, assign) id target; @property SEL selector;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; - (void)invoke; - (void)getReturnValue:(void *)retLoc;
|
反射方案利用了运行时的一些接口,如果你熟悉的话完全可以自己去尝试调用这些接口,实现组件间的通信。这里我想介绍的是一个用于实现组件间通信的三方库CTMediator,它正是基于OC的反射机制,帮我们封装了 Target-action 和 NSInvocation 操作。它提供了两种调用组件的方式:本地调用和远程调用,同时提供了其他副产品。这个库对实践基于反射原理的组件化解耦方案提供了很好的帮助,至少你可以拿来做个参考。
4.1.代码
这个库很精炼,只有两个文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@interface CTMediator : NSObject
+ (instancetype)sharedInstance;
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName; @end
|
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
|
@implementation CTMediator
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; NSString *urlString = [url query]; for (NSString *param in [urlString componentsSeparatedByString:@"&"]) { NSArray *elts = [param componentsSeparatedByString:@"="]; if([elts count] < 2) continue; [params setObject:[elts lastObject] forKey:[elts firstObject]]; }
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""]; if ([actionName hasPrefix:@"native"]) { return @(NO); }
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO]; if (completion) { if (result) { completion(@{@"result":result}); } else { completion(nil); } } return result; }
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName]; NSString *targetClassString = nil; if (swiftModuleName.length > 0) { targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName]; } else { targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; } NSObject *target = self.cachedTarget[targetClassString]; if (target == nil) { Class targetClass = NSClassFromString(targetClassString); target = [[targetClass alloc] init]; } NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; SEL action = NSSelectorFromString(actionString); if (target == nil) {
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; return nil; } if (shouldCacheTarget) { self.cachedTarget[targetClassString] = target; } if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else {
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; [self.cachedTarget removeObjectForKey:targetClassString]; return nil; } } }
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params { NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(NSUInteger)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; NSUInteger result = 0; [invocation getReturnValue:&result]; return @(result); }
return [target performSelector:action withObject:params]; } @end
|
ps:库的作者提供了详细的注释,// ME:
标注的注释是我添加,用来说明其中的业务逻辑。
4.2.本地调用
CTMediator
实现了本地调用功能,即“performTarget:action:params:shouldCacheTarget:”接口。
所谓本地调用,就是我们的应用内部组件之间的服务调用,比如主页组件调用详情组件的跳转服务,组件间主要是通过performSelector
的方式进行通信,对于返回值为非对象类型的情况则使用NSInvocation
进行通信。
[target performSelector:action withObject:params]
需要三个主要参数:
即示例中的target
,在组件化实践中则对应着目标组件服务类的一个实例。它是通过NSClassFromString(@“targetClassString”)
接口将字符串类型的类名反射成真实的类,再调用实例化方法创建实例。
即示例中的action
,对应的是目标组件服务类对外暴露的服务接口。它是通过NSSelectorFromString(actionString)
接口将字符串类型的接口名反射成SEL,供运行时调用。
即示例中的params
,对应的是目标组件服务类接口中的参数。由于performSelector
能传递的参数有限,此库要求外部调用者将多个参数包装在一个字典容器中,再由库内部将此参数传递给SEL。
4.3.远程调用
CTMediator还提供了远程调用接口,即库中提供的“-performActionWithUrl:completion:”接口。
所谓远程调用,就是别的应用向我们的应用发起的服务调用,大致过程如下:
- A应用通过
[[UIApplication sharedApplication] openURL:xxx]
传递一个指定格式的URL到B应用;
- B应用在
-application:openURL:
回调中接收URL,再调用库中的performActionWithUrl
开始处理此URL;
performActionWithUrl
中对URL中的参数进行解析之后,再通过performTarget
调用本地组件提供服务。
远程调用时传递的URL一定要按照约定的格式拼接,开发者应按照自己的规范自定义库文件中远程调用的相关逻辑;
基于URL的远程调用有个缺点,即URL中参数的类型将会受到限制,如传递图片、数据、block等类型时将很不方便。
5.示例
着手实现章节开始时提出的场景需求:从主页
组件跳转到详情
组件;返回时更新主页组件的标题。
5.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
| @interface DetailViewController : UIViewController @property (nonatomic, strong) NSDictionary *params; @end
#import "DetailViewController.h"
@interface DetailViewController () @property (weak, nonatomic) IBOutlet UIImageView *mIconImv; @property (nonatomic, strong) UIImage *image; @property (nonatomic, copy) DetailCallback callBack; @end
@implementation DetailViewController
- (void)viewDidLoad { [super viewDidLoad]; self.mIconImv.image = self.params[k_Key_Image]; self.callBack = self.params[k_Key_Block]; 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{ self.callBack(); [self.navigationController popViewControllerAnimated:YES]; } @end
|
这是详情所在的视图控制器,其中定义了用于接收外部图片的属性image
和处理回调的callBack
闭包。视图加载时显示外部传进来的图片;退出详情页时回调callBack
闭包以通知上层调用者处理自己的业务。
5.2.暴露接口
详情组件对外提供服务的类
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @interface Target_DetailViewController : NSObject - (UIViewController*)Action_showDetail:(NSDictionary*)paramDic; @end
#import "Target_DetailViewController.h"
#import "DetailViewController.h"
@implementation Target_DetailViewController
- (UIViewController *)Action_showDetail:(NSDictionary *)paramDic{
DetailViewController *controller = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"DaDetailViewController"]; controller.params = paramDic; return controller; } @end
|
一些细节:
- 详情组件对外提供的所有服务,都在这里定义和实现;
- 在此服务类的头文件中将服务接口暴露给外界;
- 此类是详情组件的一部分,可以导入和依赖详情原类;
- 注意上面的命名方式,类名前缀为
Target_
,而方法名前缀为Action_
;
关于最后一条,这是因为接下来使用到的通信类三方库CTMediator
中做了特殊处理,所以我们需要按照通信层的要求进行命名,当然,你也可以按照自己的规范在原库中将Target_
和Action_
修改为你自己喜欢的前缀。
5.3.通信类
中间通信层
,这是我们在三方库CTMediator
基础上自定义的一个分类:
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 "CTMediator.h"
@interface CTMediator (Detail)
- (UIViewController*)CTDetail:(NSDictionary*)paramDic; @end
#import "CTMediator+Detail.h"
@implementation CTMediator (Detail)
- (UIViewController *)CTDetail:(NSDictionary *)paramDic{
return [self performTarget:@"DetailViewController" action:@"showDetail" params:paramDic shouldCacheTarget:NO]; } @end
|
-CTDetail:
是详情中间通信类暴露的一个接口,其他组件想调用详情组件的服务时从这里找即可~
单独提供这么一层,而非在调用详情组件服务的地方直接执行perform
方法,主要是因为这里的类名和方法名都是明文,在没有代码补全功能的情况下,让别的组件的开发者每次调用时都自己写一遍极有可能会出错,也很不方便。这一层对这些操作进行了封装,外部调用者就不用再为此挠头。
另外这里的明文类名和方法名并没带前一小节提到的前缀,三方库CTMediator
中会自动追加。
5.4.调用服务
在主页组件中调用
详情组件的服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #import "CTMediator+Detail.h"
@implementation HomeViewController
- (IBAction)onShowDetail:(id)sender { UIButton *btn = sender; DetailCallback block = ^(){ self.title = @"Info Checked"; }; NSDictionary *params = @{k_Key_Image : btn.currentBackgroundImage, k_Key_Block : block}; UIViewController *controller = [[CTMediator sharedInstance] CTDetail:params]; if (controller) { [self.navigationController pushViewController:controller animated:YES]; } } @end
|
在本次调用中,主页组件中并未直接引用详情组件的头文件,只是引用了详情组件的中间通信层CTMediator+Detail.h
。主页组件最终通过详情中间通信层提供的接口实现了对详情组件的通信或者说服务的调用。
6.目录结构
整个工程的目录结构如下:
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
| . ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Defines.h ├── Detail │ ├── Controllers │ │ ├── DetailViewController.h │ │ └── DetailViewController.m │ ├── Mediator │ │ ├── CTMediator+Detail.h │ │ └── CTMediator+Detail.m │ └── Module+API │ ├── Target_DetailViewController.h │ └── Target_DetailViewController.m ├── Home │ ├── HomeViewController.h │ └── HomeViewController.m ├── Icons │ ├── 1@2x.png │ └── 1@3x.png ├── Info.plist └── main.m
|
7.流程总结
- 定义目标组件;
- 定义目标组件的对外服务类和接口;
- 定义目标组件的中间通信层;
- 发起者与目标组件的中间通信层交互,调用目标组件的接口;
- 中间通信层调用CTMediator,通过运行时接口实现通信;
8.方案评价
优点:
- 巧妙的利用了运行时接口,通过反射机制完成组件间的通信和解耦;
- 组件之间不再引用对方头文件,而是通过中间通信层进行通信;
- 调用方组件只单向依赖了像
CTMediator+Detail
分类这样的中间通信层;
- 各大组件之间没有耦合,只以中间层为中心与其他组件进行通信,这种思想值得肯定。
缺点:
- 中间通信层中需要明文书写类名和方法名,这种硬编码没有代码补全的辅助,极可能出错且编译时不易察觉;
- 当调用的服务接口中需要传递实体类作为参数时,还需导入对方组件中实体类的头文件,还是有依赖;
- 受反射机制的限制,本地调用时通信层中接收的参数个数有限,所以只能以字典等容器的方式传值,不够灵活方便;
- 远程调用时,URL中参数的类型也会受到限制,只适合用来进行应用内部页面跳转或别的App调起我们本地的页面;
9.结尾
整体来看,基于反射原理的组件化解决方案还是不够完美,后续会继续介绍面向协议的服务注册组件化方案,待续~
相关参考:
#©反射方案demo
#©URL+Block方案demo
#©服务协议注册方案demo
#©有赞Bifrost