模块化和组件化在当下的iOS开发中已经深入人心,App的代码不会全部都放在一个主工程里。尤其是开发人员越来越多,业务越来越复杂,代码量越来越多,模块化/组件化开发变得尤为重要。这样做的好处有:
- 解耦:避免代码严重合耦,增加复用和扩展难度;
- 结构清晰,易读,易维护;
- 开发效率高:可独立开发、测试、维护每个模块或组件,开发效率高。
目前组件化开发的方式大约有三种:protocol - class、url - block以及CTMediator target - action方案。博主在项目中使用到的是CTMediator target - action方案,本文将介绍CTMediator的使用,博客大纲如下。
1.CTMediator工作原理
1.1 源码分析
CTMediator的源码就2个文件和2个m文件。核心的代码在CTMediator.m中,而CTMediator+HandyTools扩展类的作用是实现ViewContronller的切换。
查看CTMediator.h文件可以看到,它支持远程App调用和本地组件调用两种方式。
#import <Foundation/Foundation.h>
extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName;
@interface CTMediator : NSObject
+ (instancetype _Nonnull)sharedInstance;
// 远程App调用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地组件调用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;
@end
// 简化调用单例的函数
CTMediator* _Nonnull CT(void);
- 远程调用
通过scheme://[target]/[action]?[params]调用。回调写在AppDelegate中,暴露的scheme写在info.plist中。如果你对微信登录非常熟悉的话,那么理解CTMediator的远程调用就非常容易了。其做法是一样的。我们查看CTMediator.m的代码,看看如何实现远程调用的,代码分析如下:
/*
scheme://[target]/[action]?[params]
url sample:
aaa://targetA/actionB?id=1234
*/
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
if (url == nil||![url isKindOfClass:[NSURL class]]) {
return nil;
}
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithString:url.absoluteString];
// 遍历所有参数
[urlComponents.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.value&&obj.name) {
[params setObject:obj.value forKey:obj.name];
}
}];
// 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
if (completion) {
if (result) {
completion(@{@"result":result});
} else {
completion(nil);
}
}
return result;
}
这个函数里面,主要做了几件事:参数和路由的获取、安全防护、调用performTarget。重点来看performTarget的实现,performTarget也是本地调用的入口。
- 本地调用
本地组件调用入口 :performTarget,源码分析如下:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) { // 判断是否是swift模式
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = [self safeFetchCachedTarget:targetClassString];
if (target == nil) { // 从缓存中拿到我们的目标类对象 如果没有就创建一个
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) { // 缓存target
[self safeSetCachedTarget:target key:targetClassString];
}
if ([target respondsToSelector:action]) { // 执行target
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}
上面还有个关键函数的实现:safeFetchCachedTarget,我们来看看safeFetchCachedTarget里面做了什么处理?
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) { // 方法签名不能为空
return nil;
}
const char* retType = [methodSig methodReturnType]; // 获取方法返回值类型
if (strcmp(retType, @encode(void)) == 0) { // 方法返回值类型为void
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; // 反射调用
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil; // 返回nil
}
if (strcmp(retType, @encode(NSInteger)) == 0) { // 方法返回值类型为NSInteger
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; // 反射调用
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result]; // 设置返回值
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) { // 方法返回值类型为BOOL
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; // 反射调用
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result]; // 设置返回值
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) { // 方法返回值类型为CGFloat
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; // 反射调用
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result]; // 设置返回值
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) { // 方法返回值类型为NSUInteger
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);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
safeFetchCachedTarget里面首先获取方法签名(方法签名不能为空)和返回值参数类型。返回值参数类型分为:void无参数返回、有参数返回(NSInteger、BOOL、CGFloat、NSUInteger)两类。不论哪一类,都是通过runtime 反射机制调用模块。有参数返回的区别在于,需要获取并设置返回值,而void无参数返回则直接返回。
1.2 CTMediator的工作原理总结
- 远程调用
远程调用是通过openURL的方式实现的。首先,在Appdelegate中声明openURL回调,在info.plist里设置scheme。openURL回调时,系统根据info.plist里的scheme配置来找到响应的URL的应用。如果能找到响应的应用,调用CTMediator的OpenURL方法将接收到的信息进行传参。CTMediator解析URL,并进行安全防护处理,再将请求的路由和参数发给相对应的target-action,最后进入了本地调用,由本地调用完成响应。
- 本地调用
调用方通过调用CTMediator 的 performTarget,向CTMediator发起了跨组件调用。CTMediator先根据是否使用缓存target,解析发送过来的target和action,然后经过OC的runtime机制转为target实例和action,利用runtime进行反射调用。还要根据函数返回值类型是否void进行参数回传处理,如果为void,则不需要返回参数;其他还需要回传返回值参数。
2.CTMediator的使用
2.1 引入CTMediator库
pod 'CTMediator'
2.2 创建Category
例如创建一个home framework,首先创建 Home category。
在Category中定义并实现对外暴露的方法
1)头文件
#import "CTMediator.h"
#import <UIKit/UIKit.h>
@interface CTMediator (Home)
-(UIViewController *)CTMediator_HomeController:(NSDictionary *)param;
@end
NS_ASSUME_NONNULL_END
2)源文件
#import "CTMediator+Home.h"
#import <UIKit/UIKit.h>
@implementation CTMediator (Home)
-(UIViewController *)CTMediator_HomeController:(NSDictionary *)param {
[self performTarget:@"HomeController" action:@"CreateHomeController" params:nil shouldCacheTarget:NO];
}
@end
注:HomeController是Target的名字,CreateHomeController是Action的名字。
2.3 创建Tartget action文件
注:文件Target_XX1.h、Target_XX1.m,Target_XX1.m源文件中的方法Action_XX2也是标准的格式,方法要带params,否则会出错。
例如这里HomeController是Target的名字,CreateHomeController是Action的名字。
2.4 将文件CTMediator+Home.h暴露出去
2.5 主工程集成framework
将Home设置为静态库(或动态库,结合项目评估)。
2.6 引用CTMediator暴露的方法
1)引入头文件
#import <Home/CTMediator+Home.h>
2)调用方法
UIViewController *newController = [[CTMediator sharedInstance]
CTMediator_HomeController:nil];