[iOS] 组件化方案学习 - CTMediator

1. Target-Action

这种方案是基于 OCruntimecategory 特性动态获取模块,例如通过 NSClassFromString 获取类并创建实例,通过 performSelector + NSInvocation动态调用方法。

这种方案的代表框架:CTMediator,实现原理可以看这篇文章,具体实践可以看这篇文章

本篇文章主要就是通过上面提到的两篇文章学习使用并记录一下CTMediator这个框架,然后分析一下其在组件化实施中的优点和缺点。

2. CTMediator的实现

我们先来看一下该方案实现的架构图,如下图所示:

截屏2021-04-11 下午6.38.48.png

我们以 Module_A为例,打算将 Module_A抽离为单独的组件,在使用CTMediator 此种方案时,需要先给 CTMediator添加一个相应的 CTMediator+A 的分类,这个分类里面包含了Module_A对外提供的接口,以及负责具体实现的 Target,并且使用performTarget:action方法来调用Target 的方法,示例如下:

NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionAViewController = @"getAViewController";

@implementation CTMediator (A)

- (UIViewController *)CTMediator_AViewController
{  
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action: kCTMediatorActionAViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界选择是push还是present
        return viewController;
    } else {
        // 这里处理异常场景,具体如何处理取决于产品
        return [[UIViewController alloc] init];
    }
}

我们可以看到上面的performTarget:action方法是 CTMediator 类中的,这个并不影响我们继续学习,它其实是将 Target 字符串,用 NSClassFromString 反射方法,构建了 Target 对应类的实例对象,然后调用了 action 对应的方法。

我们继续往下看Module_A对应的 Target_A,这个 Target_AModule_A有单向引用,负责对于Module_A暴露接口方法的具体实现,比如上面的那个方法,将会调用到Target_A中的getAViewController方法,如下:

#import "DemoModuleAViewController.h"

@implementation Target_A

- (UIViewController *)Action_getAViewController:(NSDictionary *)params
{
    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
    DemoModuleAViewController *viewController = [[DemoModuleAViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}
@end

从而实现了对Module_A 中方法的调用。

CTMediator 主要是利用了Category 分类提供的特性,去调用组件对应的Catetory中的的方法,进而再去调用Category分类中指定的 Target 的 Action方法,这一步是利用反射 + NSInvocation调用方法,下面源码分析时会有介绍。

3. CTMediator 源码分析

CTMediator代码很简短,也很容易理解,但这并不是我们要学习的重点,我们需要了解的是CTMediator这种实现方案的思想,不同项目中对于组件之间通信的方案是不一样的,这个就需要在实践的时候具体去衡量了。

我们以上面提到的performTarget:action:params:方法为切入点:

- (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];
    
    // 创建 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];
    }

    // 获取 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 safeSetCachedTarget:target key:targetClassString];
    }
    
    // 去调用 target 实例对象的 action 方法
    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;
        }
    }
}

上面有对于方法或者类找不到的容错处理,这里就不做介绍了,该方法主要是通过 NSClassFromString 将传入的target 转成类,并创建一个该类的对象,然后通过 NSSelectorFromString 将传入的 action 转成 方法,然后调用targetaction 方法,调用的方法实现如下:

- (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 *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);
    }
...
}

就是获取 action 对应的方法签名,然后使用NSInvocation调用, 或者使用系统的performSelector:withObject:调用方法。

4. 总结

优点:

  • 利用 category 可以明确声明的接口,进行编译检查
  • 实现方式属于轻量级

缺点:

  • 需要给每一个模块创建对应的 targetmediator 分类,模块化代码时较为繁琐
  • category 中仍然使用了字符串硬编码,内部使用字典传参
  • 无法保证所调用的目标模块一定存在,运行时才能发现错误

你可能感兴趣的:([iOS] 组件化方案学习 - CTMediator)