iOS - 组件化(二)基于CTMediator组件通讯与解耦分析 、源码解读

写在最前面的心得总结(组件化优势):

  1. 中间者架构采用中间者统一管理的方式,来控制app的整个生命周期中组件间的调用关系。
  2. 拆分的组件都会依赖于中间者,但是组间之间就不存在相互依赖的关系了。
  3. 由于其他组件都会依赖于这个中间者,相互间的通信都会通过中间者统一调度,所以组件间的通信也就更容易管理了。
  4. 在中间者上也能够轻松添加新的设计模式。
  5. 中间者架构的易管控带来的架构更稳固,易扩展带来的灵活性,从而使得架构更容易扩展。

一 . CTMediator组件通讯与解耦分析

本文以之前写过项目中维修下单的项目,并结合网上一些知名的博客内容来描述组件化方案实施

1.如何进行业务模块的拆分

实现业务模块的组件化,是为了将业务拆分出来,降低业务模块之间的耦合性。比如在报修订单详情页面【点击支付】后下一步就是进入订单生成页面,我们习惯性做法就是直接在保修订单详情页面的ViewController里面直接import订单生成页面的ViewController,然后实例化ViewController传个值后直接push过去就可以了。当项目规模变大和业务逻辑变复杂的时候,这种直接引入代码文件的做法就会使得模块之间的依赖变得越来越强,甚至是牵一发就动全身。即使举例的只有四个模块,相互间的依赖也比较多,如下图所示

image.png 解决的办法也很简单,提供一个中间人(Mediator)。业务模块之间不直接进行引用,通过Mediator间接形成引用关系,而且在Mediator可以将模块需要暴露出来的业务提供出来给其它模块调用,不需要暴露出来的就不引入Mediator。比如账户模块有登陆页面和注册页面,实际场景中可能只会把登陆页面给其他业务模块调用,注册页面只需要从登陆页面跳转过去就可以了,并不需要提供给其它业务模块调用。如下图所示:

image.png

2.以服务的方式解决模块间的调用

通过中间人的方式拆分业务模块后也只是逻辑上清晰了一点,实际上还是在引入业务的代码文件,业务与业务之间的调用依旧很不清晰明了。比如在订单页面弹出一个登陆页面,订单模块的开发人员需要先找到登陆页面的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方法就可以完成服务的调用和通信

通过以上方法,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:&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];

};
复制代码

以上就是我研究整个源代码的过程,知道了每句代码所代表的意思,在实际项目中应用的时候,就可得心应手

三.总结

CTMediator通过runtime的方式解耦,主项目中不需要引入对应的模块头文件,只需要引入对应模块的CTMediator类扩展。

将一个项目拆分成一个个组件,组件之间相互隔离,专人维护,组件可以单独提测。这就很好的解决了多人开发带来的效率降低问题。

猜你喜欢

转载自juejin.im/post/7078948560963633159
今日推荐