iOS - componentization (2) based on CTMediator component communication and decoupling analysis, source code interpretation

Summary of the experience written at the front (componentization advantages):

  1. The middleman architecture adopts the unified management method of the middleman to control the calling relationship between components in the whole life cycle of the app.
  2. The split components all depend on the intermediary, but there is no interdependence between the groups.
  3. Since other components will depend on this intermediary, the communication between them will be uniformly scheduled through the intermediary, so the communication between components is easier to manage.
  4. New design patterns can also be easily added on the intermediary.
  5. The easy management and control of the middleman architecture brings about a more stable architecture, and the flexibility brought about by easy expansion makes the architecture easier to expand.

1. Communication and decoupling analysis of CTMediator components

This article describes the implementation of the componentized solution based on the maintenance order items in the previously written projects, and combined with the content of some well-known blogs on the Internet

1. How to split the business module

The realization of the componentization of business modules is to split the business and reduce the coupling between business modules. For example, after reporting the revision 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 in the ViewController of the warranty order details page, and then instantiate the ViewController and pass a value directly. Just push it over. When the scale of the project becomes larger and the business logic becomes more complex, this method of directly introducing code files will make the dependencies between modules stronger and stronger, and even affect the whole body. Even if there are only four modules in the example, there are many dependencies between each other, as shown in the following figure

image.pngThe solution is also very simple, provide a middleman (Mediator). The business modules are not directly referenced, and the reference relationship is formed indirectly through the Mediator, and the Mediator can provide the services that the modules need to expose to other modules for calling, and the Mediator will not be introduced if they do not need to be exposed. For example, the account module has a login page and a registration page. In actual scenarios, the login page may only be called to other business modules. The registration page only needs to jump from the login page, and does not need to be provided to other business modules for invocation. As shown below:

image.png

2. Solve the calls between modules in the form of services

通过中间人的方式拆分业务模块后也只是逻辑上清晰了一点,实际上还是在引入业务的代码文件,业务与业务之间的调用依旧很不清晰明了。比如在订单页面弹出一个登陆页面,订单模块的开发人员需要先找到登陆页面的UIViewController文件,然后import进来,接着实例化对象,最后再present或push这个页面。复杂一点的业务可能还需要以口头或者文档的形式告知调用方如何去使用类文件、如何去传递参数等等。而开发人员想着我只需要一个UIViewController实例化对象就可以了,也不关心它是哪个代码文件、它内部是怎么实现的。

我们可以通过服务的方式去解决这个问题,简单的说就是你需要什么,我给你什么。通过Target-Action,业务提供方将所有的服务以对象方法的形式提供,通过方法的参数和返回值进行模块间的调用和通信。如下图所示:

image.png

3.解决模块之间的依赖以及去中心化

完成前两步之后,还存在两个问题:

  1. Mediator是个中心化的服务,引入Mediator也会将所有业务模块的Target-Action引入进来,不相关的服务反而会变得多余。同时所有业务模块对外提供的服务修改后,都需要去Mediator中做出修改。这样会导致Mediator越来越难以维护。
  2. 业务模块之间的依赖并没有减少,虽然业务调用时只import了Mediator,但Mediator会间接引入Target-Action,Target-Action又会间接引入业务代码文件。

第一个问题的解决办法,通过组合的思想,使用Objective-C的分类(Category)将Mediator去中心化。针对每个业务模块创建一个Mediator的分类(Category),并将Target服务引入到分类(Category)中,相当于将Target服务再做一层方法封装,其他业务调用方只需引入相应的分类(Category)即可,这样就可以避免无关业务服务的多余引入。同时业务模块对外提供的服务修改后,相应的业务提供方只需修改自己的分类(Category)即可,Mediator也无需维护,达到真正的去中心化。如下图所示:

image.png 通过上图可以看到模块与模块之间的调用已经没有直接引入了,都是通过Category引入。在上图的基础上还是可以看到依赖并没有减少,Category会引用Target-Action,并间接引用源代码文件。

第二个问题的其实就是Category与Target-Action之间的依赖问题,解决办法也很简单粗暴。因为业务模块中对外提供服务的Category中的方法实现其实就是直接调用的Target类里面的Action方法,所以通过runtime的技术就可以直接切段两者之间的依赖。

4.解决方案

  1. 通过NSClassFromString方法和Target类名获取到Class对象,然后Class对象通过alloc方法和init方法就可以获取到Target实例对象。
  2. 通过NSSelectorFromString方法和Action方法名获取到SEL对象。
  3. Target实例对象调用- (id)performSelector:(SEL)aSelector withObject:(id)object方法就可以完成服务的调用和通信

Through the above methods, Category can directly call the service of Target-Action without import, and pass it out, thus completing the release of dependencies.

2. Source code analysis

Open the CTMediator source code, we only need to know what each function in the CTMediator.h and CTMediator.m files means, and then we know how to use it.

#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 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:&params 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:&params 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:&params 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:&params 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:&params 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];

};
复制代码

The above is the process of my researching the entire source code. Knowing the meaning of each code, it can be handy when applied in actual projects.

3. Summary

CTMediatorThrough runtime decoupling, the main project does not need to introduce the corresponding module header file, only the CTMediatorclass extension of the corresponding module needs to be introduced.

Divide a project into components. Components are isolated from each other, maintained by dedicated personnel, and components can be independently tested. This solves the problem of reduced efficiency caused by multi-person development.

Guess you like

Origin juejin.im/post/7078948560963633159