CTMediator在iOS模块化/组件化开发中的应用

        模块化和组件化在当下的iOS开发中已经深入人心,App的代码不会全部都放在一个主工程里。尤其是开发人员越来越多,业务越来越复杂,代码量越来越多,模块化/组件化开发变得尤为重要。这样做的好处有:

  • 解耦:避免代码严重合耦,增加复用和扩展难度;
  • 结构清晰,易读,易维护;
  • 开发效率高:可独立开发、测试、维护每个模块或组件,开发效率高。

       目前组件化开发的方式大约有三种:protocol - class、url - block以及CTMediator target - action方案。博主在项目中使用到的是CTMediator target - action方案,本文将介绍CTMediator的使用,博客大纲如下。

CTMediator在iOS模块化/组件化开发中的应用
图1 大纲内容

 

 

1.CTMediator工作原理

1.1 源码分析

CTMediator在iOS模块化/组件化开发中的应用
图1.1 CTMediator源码结构截图

 

       CTMediator的源码就2个文件和2个m文件。核心的代码在CTMediator.m中,而CTMediator+HandyTools扩展类的作用是实现ViewContronller的切换。

       查看CTMediator.h文件可以看到,它支持远程App调用和本地组件调用两种方式。

#import <Foundation/Foundation.h>

extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName;

@interface CTMediator : NSObject

+ (instancetype _Nonnull)sharedInstance;

// 远程App调用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地组件调用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;

@end
  
// 简化调用单例的函数
CTMediator* _Nonnull CT(void);
  • 远程调用

       通过scheme://[target]/[action]?[params]调用。回调写在AppDelegate中,暴露的scheme写在info.plist中。如果你对微信登录非常熟悉的话,那么理解CTMediator的远程调用就非常容易了。其做法是一样的。我们查看CTMediator.m的代码,看看如何实现远程调用的,代码分析如下:

/*
 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];
    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;
}

       这个函数里面,主要做了几件事:参数和路由的获取、安全防护、调用performTarget。重点来看performTarget的实现,performTarget也是本地调用的入口。

  • 本地调用

       本地组件调用入口 :performTarget,源码分析如下:

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) { // 判断是否是swift模式
        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 action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) { // 缓存target
        [self safeSetCachedTarget:target key:targetClassString];
    }

    if ([target respondsToSelector:action]) { // 执行target
        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;
        }
    }
}

      上面还有个关键函数的实现:safeFetchCachedTarget,我们来看看safeFetchCachedTarget里面做了什么处理?


- (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) { // 方法返回值类型为void
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; // 反射调用
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil; // 返回nil
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) { // 方法返回值类型为NSInteger
        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) {  // 方法返回值类型为BOOL
        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) {  // 方法返回值类型为CGFloat
        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) { // 方法返回值类型为NSUInteger
        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
}

       safeFetchCachedTarget里面首先获取方法签名(方法签名不能为空)和返回值参数类型。返回值参数类型分为:void无参数返回、有参数返回(NSInteger、BOOL、CGFloat、NSUInteger)两类。不论哪一类,都是通过runtime 反射机制调用模块。有参数返回的区别在于,需要获取并设置返回值,而void无参数返回则直接返回。

1.2 CTMediator的工作原理总结

  • 远程调用

        远程调用是通过openURL的方式实现的。首先,在Appdelegate中声明openURL回调,在info.plist里设置scheme。openURL回调时,系统根据info.plist里的scheme配置来找到响应的URL的应用。如果能找到响应的应用,调用CTMediator的OpenURL方法将接收到的信息进行传参。CTMediator解析URL,并进行安全防护处理,再将请求的路由和参数发给相对应的target-action,最后进入了本地调用,由本地调用完成响应。

  • 本地调用

        调用方通过调用CTMediator 的 performTarget,向CTMediator发起了跨组件调用。CTMediator先根据是否使用缓存target,解析发送过来的target和action,然后经过OC的runtime机制转为target实例和action,利用runtime进行反射调用。还要根据函数返回值类型是否void进行参数回传处理,如果为void,则不需要返回参数;其他还需要回传返回值参数。

2.CTMediator的使用

2.1  引入CTMediator库

        pod  'CTMediator'

2.2 创建Category

       例如创建一个home framework,首先创建 Home category。

CTMediator在iOS模块化/组件化开发中的应用
图2.1 创建CTMediator category

 

在Category中定义并实现对外暴露的方法

1)头文件

#import "CTMediator.h"
#import <UIKit/UIKit.h>

@interface CTMediator (Home)
-(UIViewController *)CTMediator_HomeController:(NSDictionary *)param;
@end

NS_ASSUME_NONNULL_END

2)源文件

#import "CTMediator+Home.h"
#import <UIKit/UIKit.h>

@implementation CTMediator (Home)
-(UIViewController *)CTMediator_HomeController:(NSDictionary *)param {
    [self performTarget:@"HomeController" action:@"CreateHomeController" params:nil shouldCacheTarget:NO];
}
@end

注:HomeController是Target的名字,CreateHomeController是Action的名字。

2.3  创建Tartget action文件

CTMediator在iOS模块化/组件化开发中的应用
图2.2 创建CTMediator action

 

注:文件Target_XX1.h、Target_XX1.m,Target_XX1.m源文件中的方法Action_XX2也是标准的格式,方法要带params,否则会出错。

例如这里HomeController是Target的名字,CreateHomeController是Action的名字。

2.4 将文件CTMediator+Home.h暴露出去

CTMediator在iOS模块化/组件化开发中的应用
图2.3 暴露头文件

 

2.5 主工程集成framework

       将Home设置为静态库(或动态库,结合项目评估)。

CTMediator在iOS模块化/组件化开发中的应用
图2.4 集成到主工程

 

2.6 引用CTMediator暴露的方法

 1)引入头文件

#import <Home/CTMediator+Home.h>

2)调用方法

UIViewController *newController = [[CTMediator sharedInstance]
                                       CTMediator_HomeController:nil];

 3. 参考链接

  casatwy/CTMediator

上一篇:[POJ3630]Phone List (Tire)


下一篇:C语言之指针变量