iOS - composition (2) basée sur l'analyse de la communication et du découplage des composants CTMediator, interprétation du code source

Synthèse de l'expérience écrite au recto (avantages de la composition) :

  1. L'architecture intermédiaire adopte la méthode de gestion unifiée de l'intermédiaire pour contrôler la relation d'appel entre les composants tout au long du cycle de vie de l'application.
  2. Les composantes scindées dépendent toutes de l'intermédiaire, mais il n'y a pas d'interdépendance entre les groupes.
  3. Étant donné que d'autres composants dépendront de cet intermédiaire, la communication entre eux sera uniformément planifiée par l'intermédiaire, de sorte que la communication entre les composants est plus facile à gérer.
  4. De nouveaux modèles de conception peuvent également être facilement ajoutés par l'intermédiaire.
  5. La gestion et le contrôle faciles de l'architecture intermédiaire entraînent une architecture plus stable, et la flexibilité apportée par une extension facile facilite l'extension de l'architecture.

1. Analyse de la communication et du découplage des composants CTMediator

Cet article décrit la mise en œuvre de la solution par composants basée sur les éléments de commande de maintenance dans les projets précédemment écrits et combinés avec le contenu de certains blogs bien connus sur Internet.

1. Comment scinder le module métier

La réalisation de la constitutionnalisation des modules métiers consiste à scinder le métier et à réduire le couplage entre les modules métiers. Par exemple, après avoir signalé la page de détails de la commande de révision [cliquez pour payer], l'étape suivante consiste à accéder à la page de génération de commande. Notre pratique habituelle est d'importer directement le ViewController de la page de génération de commande dans le ViewController de la page de détails de la commande de garantie. , puis instanciez le ViewController et transmettez une valeur directement. Lorsque l'échelle du projet devient plus grande et que la logique métier devient plus complexe, cette méthode d'introduction directe des fichiers de code rendra les dépendances entre les modules de plus en plus fortes, et affectera même tout le corps. Même s'il n'y a que quatre modules dans l'exemple, il existe de nombreuses dépendances entre eux, comme le montre la figure suivante

image.pngLa solution est aussi très simple, prévoir un intermédiaire (Médiateur). Les modules métier ne sont pas directement référencés et la relation de référence est formée indirectement via le médiateur, et le médiateur peut fournir les services que les modules doivent exposer à d'autres modules pour l'appel, et le médiateur ne sera pas introduit s'ils n'ont pas besoin être exposé. Par exemple, le module de compte comporte une page de connexion et une page d'inscription. Dans des scénarios réels, la page de connexion ne peut être appelée que par d'autres modules métier. La page d'inscription n'a qu'à sauter de la page de connexion et n'a pas besoin d'être fournie à d'autres modules métier pour invocation. Comme indiqué ci-dessous:

image.png

2. Résoudre les appels entre modules sous forme de 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方法就可以完成服务的调用和通信

Grâce aux méthodes ci-dessus, Category peut appeler directement le service de Target-Action sans importation et le transmettre, complétant ainsi la libération des dépendances.

2. Analyse du code source

Ouvrez le code source CTMediator, nous avons seulement besoin de savoir ce que signifie chaque fonction dans les fichiers CTMediator.h et CTMediator.m, et ensuite nous savons comment l'utiliser.

#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);
复制代码

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

};
复制代码

Ce qui précède est le processus de recherche de l'ensemble du code source. Connaissant la signification de chaque code, cela peut être pratique lorsqu'il est appliqué à des projets réels.

3. Résumé

CTMediatorGrâce au découplage d'exécution, le projet principal n'a pas besoin d'introduire le fichier d'en-tête de module correspondant, seule l' CTMediatorextension de classe du module correspondant doit être introduite.

Divisez un projet en composants. Les composants sont isolés les uns des autres, entretenus par du personnel dédié, et les composants peuvent être testés indépendamment. Cela résout le problème de l'efficacité réduite causée par le développement de plusieurs personnes.

Je suppose que tu aimes

Origine juejin.im/post/7078948560963633159
conseillé
Classement