写在最前面的心得总结(组件化优势):
- 中心者架构选用中心者共同管理的方法,来控制app的整个生命周期中组件间的调用联络。
- 拆分的组件都会依托于中心者,但是组间之间就不存在互相依托的联络了。
- 因为其他组件都会依托于这个中心者,互相间的通讯都会通过中心者共同调度,所以组件间的通讯也就更简略管理了。
- 在中心者上也可以轻松添加新的规划方式。
- 中心者架构的易管控带来的架构更安定,易扩展带来的灵活性,然后使得架构更简略扩展。
一 . CTMediator组件通讯与解耦剖析
本文以之前写过项目中修理下单的项目,并结合网上一些闻名的博客内容来描绘组件化计划实施
1.怎样进行业务模块的拆分
结束业务模块的组件化,是为了将业务拆分出来,下降业务模块之间的耦合性。比如在报修订单概况页面【点击付出】后下一步就是进入订单生成页面,我们习惯性做法就是直接在保修订单概况页面的ViewController里边直接import订单生成页面的ViewController,然后实例化ViewController传个值后直接push曩昔就可以了。当项目规模变大和业务逻辑变凌乱的时分,这种直接引入代码文件的做法就会使得模块之间的依托变得越来越强,甚至是牵一发就动全身。即便举例的只有四个模块,互相间的依托也比较多,如下图所示
处理的方法也很简略,供应一个中心人(Mediator)。业务模块之间不直接进行引用,通过Mediator直接构成引用联络,并且在Mediator可以将模块需求显显露来的业务供应出来给其它模块调用,不需求显显露来的就不引入Mediator。比如账户模块有登陆页面和注册页面,实践场景中或许只会把登陆页面给其他业务模块调用,注册页面只需求从登陆页面跳转曩昔就可以了,并不需求供应给其它业务模块调用。如下图所示:
2.以服务的方法处理模块间的调用
通过中心人的方法拆分业务模块后也只是逻辑上清楚了一点,实践上仍是在引入业务的代码文件,业务与业务之间的调用仍旧很不清楚清楚。比如在订单页面弹出一个登陆页面,订单模块的开发人员需求先找到登陆页面的UIViewController文件,然后import进来,接着实例化方针,最终再present或push这个页面。凌乱一点的业务或许还需求以口头或者文档的方式奉告调用方怎样去运用类文件、怎样去传递参数等等。而开发人员想着我只需求一个UIViewController实例化方针就可以了,也不关心它是哪个代码文件、它内部是怎样结束的。
我们可以通过服务的方法去处理这个问题,简略的说就是你需求什么,我给你什么。通过Target-Action,业务供应方将全部的服务以方针方法的方式供应,通过方法的参数和回来值进行模块间的调用和通讯。如下图所示:
3.处理模块之间的依托以及去中心化
结束前两步之后,还存在两个问题:
- Mediator是个中心化的服务,引入Mediator也会将全部业务模块的Target-Action引入进来,不相关的服务反而会变得剩余。一同全部业务模块对外供应的服务修正后,都需求去Mediator中做出修正。这样会导致Mediator越来越难以保护。
- 业务模块之间的依托并没有减少,尽管业务调用时只import了Mediator,但Mediator会直接引入Target-Action,Target-Action又会直接引入业务代码文件。
第一个问题的处理方法,通过组合的思维,运用Objective-C的分类(Category)将Mediator去中心化。针对每个业务模块创建一个Mediator的分类(Category),并将Target服务引入到分类(Category)中,相当于将Target服务再做一层方法封装,其他业务调用方只需引入相应的分类(Category)即可,这样就可以防止无关业务服务的剩余引入。一同业务模块对外供应的服务修正后,相应的业务供应方只需修正自己的分类(Category)即可,Mediator也无需保护,抵达真正的去中心化。如下图所示:
通过上图可以看到模块与模块之间的调用现已没有直接引入了,都是通过Category引入。在上图的基础上仍是可以看到依托并没有减少,Category会引用Target-Action,并直接引用源代码文件。
第二个问题的其实就是Category与Target-Action之间的依托问题,处理方法也很简略粗犷。因为业务模块中对外供应服务的Category中的方法结束其实就是直接调用的Target类里边的Action方法,所以通过runtime的技能就可以直接切段两者之间的依托。
4.处理计划
- 通过NSClassFromString方法和Target类名获取到Class方针,然后Class方针通过alloc方法和init方法就可以获取到Target实例方针。
- 通过NSSelectorFromString方法和Action方法名获取到SEL方针。
- Target实例方针调用- (id)performSelector:(SEL)aSelector withObject:(id)object方法就可以结束服务的调用和通讯
通过以上方法,Category可以不必import就直接调用Target-Action的服务,并传递出去,这样就结束了革除依托。
二.源码解析
翻开CTMediator源码,我们只需知道CTMediator.h 和 CTMediator.m 文件每个函数代表的是什么意思,就知道怎样运用了。
#import <Foundation/Foundation.h>
extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName;
@interface CTMediator : NSObject
//单利回来CTMediator 方针
+ (instancetype _Nonnull)sharedInstance;
/*
远程App调用进口:
主要用于远程App调用
比如从A运用传递一个URL到B运用,在B运用openURL方法中处理URL
*/
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^ _Nullable)(NSDictionary * _Nullable info))completion;
/*
本地组件调用进口:
performTarget 运用RunTime处理target和action
@param targetName 类方针 OC中类方针是要Target_为前缀的
@param actionName 方法称谓 最终实践调用的是以Action_为前缀的
@param shouldCacheTarget 是否对传入的target进行缓存
*/
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
/*
@param releaseCachedTargetWithTargetName 把传入的target从缓冲中删除
*/
- (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;
@end
// 简化调用单例的函数
CTMediator* _Nonnull CT(void);
CTMediator.m文件
#import "CTMediator.h"
#import <objc/runtime.h>
#import <CoreGraphics/CoreGraphics.h>
NSString * const kCTMediatorParamsKeySwiftTargetModuleName = @"kCTMediatorParamsKeySwiftTargetModuleName";
@interface CTMediator ()
@property (nonatomic, strong) NSMutableDictionary *cachedTarget;
@end
@implementation CTMediator
#pragma mark - public methods
+ (instancetype)sharedInstance
{
static CTMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mediator = [[CTMediator alloc] init];
[mediator cachedTarget]; // 一同把cachedTarget初始化,防止多线程重复初始化
});
return mediator;
}
/*
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];
/*
科普一下NSURL(这儿我只是做了一下验证,忽略即可)
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/search?id=1"];
NSLog(@"scheme:%@", [url scheme]); //协议 http
NSLog(@"host:%@", [url host]); //域名 www.baidu.com
NSLog(@"absoluteString:%@", [url absoluteString]); //完好的url字符串 http://www.baidu.com:8080/search?id=1 (刚才在真机上跑了一下,并没有打印出来端口 8080 啊)
NSLog(@"relativePath: %@", [url relativePath]); //相对途径 searc
NSLog(@"port :%@", [url port]); // 端口 8080
NSLog(@"path: %@", [url path]); // 途径 search
NSLog(@"pathComponents:%@", [url pathComponents]); // search
NSLog(@"Query:%@", [url query]); //参数 id=1
*/
/*
NSURLComponents *urlComponents 便利高效的提取URL 中的各个参数
*/
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;
}
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
//供swift项目运用
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
//======================================================================================================================================
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
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
/*
IOS SEL(@selector)原理
其间@selector()是取类方法的编号,取出的结果是SEL类型。
SEL:类成员方法的指针,与C的函数指针不一样,函数指针直接保存了方法的地址,而SEL只是方法的编号。
NSSelectorFromString:动态加载实例方法
*/
SEL action = NSSelectorFromString(actionString);
//先从缓冲中取,取不到去创建,但是也或许创建失利的情况(targetName值不正确)
if (target == nil) {
// 这儿是处理无照应央求的当地之一,这个demo做得比较简略,假设没有可以照应的target,就直接return了。实践开发进程中是可以事先给一个固定的target专门用于在这个时分顶上,然后处理这种央求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
//是否缓存该方针
if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}
//该方针是否能调起该方法
if ([target respondsToSelector:action]) {
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;
}
}
}
- (void)releaseCachedTargetWithFullTargetName:(NSString *)fullTargetName
{
//fullTargetName在oc环境下,就是Target_XXXX。要带上Target_前缀。在swift环境下,就是XXXModule.Target_YYY。不光要带上Target_前缀,还要带上模块名。
if (fullTargetName == nil) {
return;
}
@synchronized (self) {
[self.cachedTarget removeObjectForKey:fullTargetName];
}
}
#pragma mark - private methods
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
SEL action = NSSelectorFromString(@"Action_response:");
NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"originParams"] = originParams;
params[@"targetString"] = targetString;
params[@"selectorString"] = selectorString;
[self safePerformAction:action target:target params:params];
}
- (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) {
/*
科普:
在 iOS 中不通过类可以直接调用某个方针的消息方法有两种:
1.performSelector:withObject:比较简略,能结束简略的调用
2.NSInvocation:对于 > 2 个的参数或者有回来值的处理
*/
// 通过NSMethodSignature方针创建NSInvocation方针,NSMethodSignature为方法签名类
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
// 设置消息参数
[invocation setArgument:¶ms atIndex:2];
// 设置要调用的消息
[invocation setSelector:action];
// 设置消息调用者
[invocation setTarget:target];
// 发送消息,即履行方法
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
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) {
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) {
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) {
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
}
#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
if (_cachedTarget == nil) {
_cachedTarget = [[NSMutableDictionary alloc] init];
}
return _cachedTarget;
}
- (NSObject *)safeFetchCachedTarget:(NSString *)key {
@synchronized (self) {
return self.cachedTarget[key];
}
}
- (void)safeSetCachedTarget:(NSObject *)target key:(NSString *)key {
@synchronized (self) {
self.cachedTarget[key] = target;
}
}
@end
CTMediator* _Nonnull CT(void){
return [CTMediator sharedInstance];
};
以上就是我研究整个源代码的进程,知道了每句代码所代表的意思,在实践项目中运用的时分,就可称心如意
三.总结
CTMediator
通过runtime的方法解耦,主项目中不需求引入对应的模块头文件,只需求引入对应模块的CTMediator
类扩展。
将一个项目拆分红一个个组件,组件之间互相隔绝,专人保护,组件可以单独提测。这就很好的处理了多人开发带来的功率下降问题。