Summary written at the top (componentization advantage) :
- Intermediary architecture uses intermediary unified management to control the call relationship between components in the whole life cycle of APP.
- The split components depend on the intermediary, but there is no interdependence between the groups.
- Communication between components is easier to manage because all other components depend on this intermediary, and communication between components is uniformly scheduled through the intermediary.
- It is also easy to add new design patterns on the middle.
- Easy management of an intermediate architecture leads to a more stable architecture, and easy scalability leads to flexibility, which makes the architecture easier to scale.
I. CTMediator component communication and decoupling analysis
This paper describes the componentized program implementation by referring to the project of maintenance order written before and combining with the content of some well-known blogs on the Internet
1. How to split service modules
Componentization of service modules is to separate services and reduce the coupling between service modules. For example, after the repair order details page [click to pay], the next step is to enter the order generation page. Our habitual practice is to directly import the ViewController of the order generation page from the ViewController of the warranty order details page. And then instantiate the ViewController and pass a value and just push it. As the project grows larger and the business logic becomes more complex, this direct introduction of code files can lead to stronger dependencies between modules, or even more complex dependencies. Even though there are only four modules in the example, there are many dependencies among them, as shown in the figure below
The solution, too, was simple: provide a Mediator. Business modules do not directly reference each other, but indirectly form a reference relationship through Mediator. In addition, mediators can provide the business that the module needs to expose for other modules to call, and if they do not need to be exposed, mediators will not be introduced. For example, the account module has a login page and a registration page. In actual scenarios, the login page may only be invoked by other service modules. The registration page only needs to be skipped from the login page and does not need to be invoked by other service modules. As shown below:
2. Solve the invocation between modules as a service
The man-in-the-middle split of business modules only makes logic a little clearer. In fact, it is still introducing business code files, and the calls between business and business are still not clear. For example, when a login page pops up on the order page, the developer of the order module needs to find the UIViewController file for the login page, import it, instantiate the object, and present or push the page. A more complex business might also need to verbally or documentically tell the caller how to use the class file, how to pass parameters, and so on. The developer thinks THAT all I need is a UIViewController instantiation object, and doesn’t care what code file it is or how it’s implemented internally.
We can solve this problem through the way of service. Simply speaking, I give you what you need. Through target-Action, the business provider provides all the services in the form of object methods, and calls and communicates between modules through the parameters and return values of the methods. As shown below:
3. Resolve dependencies and decentralization among modules
After completing the first two steps, two questions remain:
- Mediator is a centralized service. Introducing Mediator will also introduce target-action of all business modules, making irrelevant services redundant. At the same time, after the service provided by all business modules is modified, it needs to be modified in Mediator. This makes Mediators increasingly difficult to maintain.
- The dependency between business modules is not reduced. Although only Mediator is imported when business calls, Mediator indirectly introduces target-Action, which in turn indirectly introduces business code files.
The solution to the first problem was to decentralize Mediator through the idea of composition, using Objective-C categories. Create a Mediator classification (Category) for each business module and introduce Target service into the Category, which is equivalent to encapsulating Target service with another layer of methods. Other business callers only need to introduce corresponding classification (Category). This avoids the unnecessary introduction of extraneous business services. At the same time, after the service provided by the business module was modified, the corresponding business provider only needed to modify its own Category, and mediators did not need to be maintained, thus achieving real decentralization. As shown below:
As you can see from the figure above, calls between modules are no longer introduced directly, but by Category. Based on the above, you can see that the dependency is not reduced. The Category refers to target-action and indirectly refers to the source file.
The second problem is the dependency between Category and target-action, and the solution is simple and crude. Because the implementation of the service Category in the business module is actually the Action method in the Target class that is called directly, the Runtime technology can directly cut off the dependency between the two.
4. Solutions
- The Class object is retrieved from the NSClassFromString method and the Target Class name. The Class object is then retrieved from the Target instance by alloc and init methods.
- The SEL object is obtained using the NSSelectorFromString method and the Action method name.
- The Target instance object calls – (id)performSelector:(SEL)aSelector withObject:(id)object to complete the invocation and communication of the service
By using the above method, a Category can call the target-action service without import and pass it out, thus completing the de-dependency.
Two. Source code analysis
If you open the ctMediator. h and ctMediator. m files, you will know how to use them.
#import <Foundation/Foundation.h> extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName; @interface CTMediator: NSObject Returns the CTMediator object + (InstanceType _Nonnull)sharedInstance; /* Remote App call entry: It's mostly used for remote App calls like passing A URL from App A to App B, / / - (id _Nullable)performActionWithUrl:(NSURL * _Nullable) URL completion:(void(^ _Nullable)(NSDictionary * _Nullable info))completion; /* Local component call entry: PerformTarget uses the RunTime to handle target and action@param targetName class objects. The OC class object is the @param actionName method name prefixed with Target_ ShouldCacheTarget specifies whether to cache the target passed in. */ - (id _Nullable)performTarget:(NSString *) _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget; / * @ param releaseCachedTargetWithTargetName the incoming target is removed from the buffer * / - (void) releaseCachedTargetWithFullTargetName: (nsstrings * _Nullable)fullTargetName; CTMediator* _Nonnull CT(void);Copy the code
CTMediator. M file
#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];
};
Copy the code
The above is my study of the whole source code process, know the meaning of each sentence code, in the actual project application, it can be handy
3. To summarize
CTMediator is decoupled-by the way of Runtime. The main project does not need to introduce the corresponding module header file, but only the CTMediator class extension of the corresponding module.
A project is divided into components, which are isolated from each other and maintained by special personnel. Components can be tested separately. This is a good way to solve the problem of efficiency loss caused by multiplayer development.