MGJRouter CTMediator BeeHive 组件化源码分析

2,601 阅读5分钟

前言

我当时最早是看的是蘑菇街是开源的通过URL来做组件化的。 MGJRouter学习它里面的思路 所有的模块都应该暴露一个文件publicHeader的pch,供其它业务组件来调用的。如果主页需要调用一个URL就通过MGJRouter去publicHeader中找。

所有组件化通信的原理都是暴露服务

  • 通过URL形式 :暴露一个Header(暴露当前的组件具有的服务)
  • CTMediator: 通过暴露Target(文件),其实Category就是我们需要暴露出来的服务,这样方便调用。
  • BeeHive是通过runtime和Mach-O() 来注入的,也是通过暴露服务,先把服务加载到数组或者内存中然后供其它模块调用。

2、MGJRouter

2.1 注册URL

解析URL 通过字典的形式存放Handler Block 存放到全局的数组中 根据当前的规则去遍历数组找到当前的Block

3、CTMediator

暴露Target(文件),通过Category暴露服务,这样方便调用。 CTMediator里面就是核心调用逻辑,最终都是通过CTMediator这个类去调用

// 本地组件调用入口
//targetName -- 类名
//action  -- SEL
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;

通过NSInvocation来调用方法,这样的好处就是不需要知道当前具体class 也不需要知道当前的方法编号,可以通过当前targetNameactionName字符串来获得classSEL(方法编号), 来设置NSInvocation的参数调用invoke来执行方法。

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
  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.cachedTarget[targetClassString];
  if (target == nil) {
      //获得class
      Class targetClass = NSClassFromString(targetClassString);
      //
      target = [[targetClass alloc] init];
  }

  // generate action
  NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
  SEL action = NSSelectorFromString(actionString);
  
  if (target == nil) {
      // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
      [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
      return nil;
  }
  
  if (shouldCacheTarget) {
      self.cachedTarget[targetClassString] = target;
  }

  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];
          [self.cachedTarget removeObjectForKey:targetClassString];
          return nil;
      }
  }
}

通过NSInvocation执行对应的方法

//通过NSInvocation最终调用方法
- (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) {
        //NSInvocation 进行方法调用
        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
}

使用CTMediator

  • 1、每一个模块需要一个对应的Target文件,来暴露当前模块的服务。
  • 2、需要维护一个CTMediator分类的Pod库,来为当前模块提供服务。供其它模块调用。
  • 3、其它模块调用只需依赖暴露出来的Pod库就可以了。

CTMediator+CTMediatorModuleAActions是为了解决硬编码的问题。

调用顺序

CTMediator+CTMediatorModuleAActions ===> CTMediator ===> Target_A ===> DemoModuleADetailViewController

4、BeeHive

  • BHAnnotation 注解的方式调用(通过macho 注入)。

  • BHAppDelegate 重写了AppDelegate逻辑

  • BHConfig 上下文配置的环境

  • BHModuleManager 模块的管理(首页、个人中心等模块)

  • BHServiceManager 服务的管理 (每个模块暴露出来的方法)

  • BHContext 分发它里面的环境 BHContext里面维护了一个servicesByName字典以Key-Value的形式存储着,Protocol 和服务(IMP的具体实现)。

  • 代理解耦(接管APP Delegate) 分发给不同的模块。

  • 模块间的解耦(解除耦合)

4.1 服务注册

4.1.1 MachO 依赖注入注册服务

把当前的BeehiveServices作为section字段 ,BeehiveServices 作为section name注册到Macho__DATA数据段。BeehiveServices字段存放着对应HomeProtocolBHViewController

__DATA由不同的section组成, 不同section又有不同section name __attribute GNU编译的语法

//used 告诉编译器需要保留的
//函数:TEXT
//数据:DATA(section)
//Macho 文件格式
//
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))


#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

//注册到数据段(Main 函数执行之前执行了)
//@BeeHiveService(HomeServiceProtocol, BHViewController)

//HomeProtocol
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

//__DATA  BeehiveServices -- Macho 加载的时候添加进去的
/**
 char *kHomeProtocol_service __attribute((used, section("__DATA,"#BeehiveServices" "))) =
 {\"""HomeProtocol"""\,\""""BHViewController"""\}
 */

5、 模块划分

模块:(首页模块, 个人中心模块) 服务: (每个模块暴露出来的方法)

  • 模块:由不同的组件来构成
  • 组件:LGUtilsLGNetwork

APP职责的划分(最多三层)

  • 底层:基础层(网络请求、数据缓存)
  • 通用业务层:拿到数据 ===> 清洗数据 ===> 交付数据, 视频播放(单独的Pod库)
  • 具体业务层:首页模块 关注模块

依赖下沉

  • 依赖关系应该单一
  • 上层只能依赖下层
  • 下层不应该知道上层的业务逻辑,如果有这个功能需要单独抽离出来下沉到下层模块

参考