iOS 路由 组件化 Target-Action 方案(推荐)

Target-Action方案的优点。

  • 充分的利用Runtime的特性,实现了组件间服务的自动发现,无需注册即可实现组件间的调用。
  • Target-Action方案,只存在组件依赖Mediator中介的这一层依赖关系。
  • 在每个组件中创建Mediator的Category分类,针对维护Mediator的Category分类即可。
  • 每个组件的Category对应一个Target类,Categroy中的Action方法对应Target类中的Action场景。
  • Target-Action方案统一了所有组件间调用入口。都是调用“performTarget: action: params: shouldCacheTarget:”方法。第三个参数是一个字典,这个字典里面可以传很多参数,只要Key-Value写好就可以了。
  • Target-Action方案处理错误的方式也统一在一个地方了,Target没有,或者是Target无法响应的Action方法,都可以在Mediator这里进行统一出错处理。
  • Target-Action方案也能有一定的安全保证,它对URL中的Native前缀进行安全验证。
  • 因此,Target-Action方案不管从维护性、可读性、扩展性来说,都是一个比较完美的方案。

Target-Action方案的缺点。

  • Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这就造成了一部分的硬编码。

Target-Action路由方案的技术实现。

1、通过Target-Action的方式,创建一个Target类,Target类里面定义一些Action方法,这些方法的结果是返回一个Controller或其它Object对象。

2、在每个组件中,给Mediator中介类创建一个对应这个组件的分类,来保证Mediator中介类的代码纯净性。

3、在每个组件Mediator的分类中,定义组件外部可调用的接口方法。接口方法内部,通过统一调用Mediator中介类的“performTarget: action: params: shouldCacheTarget:”方法,实现组件间解耦。(即调用者即使不导入其它组件的头文件,也能调用其它组件。

4、在Mediator中介类的“performTarget: action: params: shouldCacheTarget:”方法中,主要通过Runtime中的NSClassFromString获取Target类,通过NSSelectorFromString获取Target类中的Action方法名。

5、路由方案最终实现是通过获取到的Target类和Target类中的Action方法名,去匹配已经创建好的Target类,由Target类中的Action方法,真正的实现创建需要的控制器或对象。将这些对象作为方法的返回值,传递给调用者。(即在Target类中的Action方法中,才能真正调用当前组件控制器的功能。)

相关链接

组件化架构漫谈
https://www.jianshu.com/p/67a6004f6930

iOS应用架构谈 组件化方案
https://casatwy.com/iOS-Modulization.html

iOS中的三种路由方案实践
https://www.jianshu.com/p/72d705ecc177

iOS 组件化 —— 路由设计思路分析
https://www.jianshu.com/p/76da56b3bd55

Target-Action组件化方案(来自casatwy)

整体架构

  • casatwy组件化方案可以处理两种方式的调用,远程调用和本地调用,对于两个不同的调用方式分别对应两个接口。
  • 远程调用通过AppDelegate代理方法传递到当前应用后,调用远程接口并在内部做一些处理,处理完成后会在远程接口内部调用本地接口,以实现本地调用为远程调用服务。
  • 本地调用由performTarget:action:params:方法负责,但调用方一般不直接调用performTarget:方法。CTMediator会对外提供明确参数和方法名的方法,在方法内部调用performTarget:方法和参数的转换。
casatwy提出的组件化架构

架构设计思路

casatwy是通过CTMediator类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的Target、Action。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。

但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。

casatwy组件化实现细节

对于服务方的组件来说,每个组件都提供一个或多个Target类,在Target类中声明Action方法。Target类是当前组件对外提供的一个“服务类”,Target将当前组件中所有的服务都定义在里面,CTMediator通过runtime主动发现服务。

在Target中的所有Action方法,都只有一个字典参数,所以可以传递的参数很灵活,这也是casatwy提出的去Model化的概念。在Action的方法实现中,对传进来的字典参数进行解析,再调用组件内部的类和方法。

架构分析

casatwy为我们提供了一个Demo,通过这个Demo可以很好的理解casatwy的设计思路,下面按照我的理解讲解一下这个Demo。

Demo链接

https://github.com/casatwy/CTMediator

文件目录

文件目录

打开Demo后可以看到文件目录非常清楚,在上图中用蓝框框出来的就是中间件部分,红框框出来的就是业务组件部分。我对每个文件夹做了一个简单的注释,包含了其在架构中的职责。

远程调用和本地调用

在CTMediator中定义远程调用和本地调用的两个方法,其他业务相关的调用由Category完成。

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

在CTMediator中定义的ModuleA的Category,为其他组件提供了一个获取控制器并跳转的功能,下面是代码实现。由于casatwy的方案中使用performTarget的方式进行调用,所以涉及到很多硬编码字符串的问题,casatwy采取定义常量字符串来解决这个问题,这样管理也更方便。

#import "CTMediator+CTMediatorModuleAActions.h"

NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";

@implementation CTMediator (CTMediatorModuleAActions)

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

下面是ModuleA组件中提供的服务,被定义在Target_A类中,这些服务可以被CTMediator通过runtime的方式调用,这个过程就叫做发现服务。

在Target_A中对传递的参数做了处理,以及内部的业务逻辑实现。方法是发生在ModuleA内部的,这样就可以保证组件内部的业务不受外部影响,对内部业务没有侵入性。

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
    // 对传过来的字典参数进行解析,并调用ModuleA内部的代码
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}

命名规范

在大型项目中代码量比较大,需要避免命名冲突的问题。

对于这个问题casatwy采取的是加前缀的方式,从casatwy的Demo中也可以看出,其组件ModuleA的Target命名为Target_A,可以区分各个组件的Target。

被调用的Action命名为Action_nativeFetchDetailViewController:,可以区分组件内的方法与对外提供的方法。

casatwy将类和方法的命名,都统一按照其功能做区分当做前缀,这样很好的将组件相关和组件内部代码进行了划分。


CTMediator 代码注释

注释是自己对代码的理解,可能会有不对的地方,仅供参考。

调用路径

  • 主框架调用模块的分类。
  • 模块的分类调用统一的中间件CTMediator
  • 中间件CTMediator通过runtime主动发现服务。
  • 重点注释:在这里就实现了主框架对各个模块之间的解耦。(主框架不需要导入模块的头文件,也能实现对模块的调用)
  • 中间件CTMediator调用模块Target中的Action方法。实现引入模块的功能。
  • 模块将创建的控制器等需要实现的业务逻辑返回给主框架。

==主框架==

ViewController

//
//  ViewController.h
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController


@end












//
//  ViewController.m
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "ViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>
#import "CTMediator+CTMediatorModuleAActions.h"
#import "TableViewController.h"

// 全局常量
NSString * const kCellIdentifier = @"kCellIdentifier";

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataSource;

@end

@implementation ViewController

#pragma mark - life cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.tableView];
}

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.tableView fill];
}

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
    cell.textLabel.text = self.dataSource[indexPath.row];
    return cell;
}

#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 取消选定由索引路径标识的给定行,并使用使取消选定具有动画效果的选项。
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    if (indexPath.row == 0) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
        
        // 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
        [self presentViewController:viewController animated:YES completion:nil];
    }
    
    if (indexPath.row == 1) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
        [self.navigationController pushViewController:viewController animated:YES];
    }
    
    if (indexPath.row == 2) {
        // 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
        [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
    }
    
    if (indexPath.row == 3) {
        // 这种场景下,参数有问题,因此需要在流程中做好处理
        [[CTMediator sharedInstance] CTMediator_presentImage:nil];
    }
    
    if (indexPath.row == 4) {
        [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
            // 做你想做的事
        }];
    }
    
    if (indexPath.row == 5) {
        TableViewController *tableViewController = [[TableViewController alloc] init];
        [self presentViewController:tableViewController animated:YES completion:nil];
    }
    
    if (indexPath.row == 6) {
        [[CTMediator sharedInstance] performTarget:@"InvalidTarget" action:@"InvalidAction" params:nil shouldCacheTarget:NO];
    }
}

#pragma mark - getters and setters
- (UITableView *)tableView
{
    if (_tableView == nil) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        
        // 注册用于创建新表单元格的类。
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellIdentifier];
    }
    return _tableView;
}

- (NSArray *)dataSource
{
    if (_dataSource == nil) {
        _dataSource = @[@"present detail view controller",
                        @"push detail view controller",
                        @"present image",
                        @"present image when error",
                        @"show alert",
                        @"table view cell",
                        @"No Target-Action response"
                        ];
    }
    return _dataSource;
}
@end

中间件Mediator

CTMediator+CTMediatorModuleAActions.h

//
//  CTMediator+CTMediatorModuleAActions.h
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

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

@interface CTMediator (CTMediatorModuleAActions)

/// 中介 视图详情控制器
- (UIViewController *)CTMediator_viewControllerForDetail;

/**
 中介 显示警报信息

 @param message 警报信息
 @param cancelAction 取消动作回调
 @param confirmAction 确定动作回调
 */
- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction;

/// 中介 模态弹出图片
- (void)CTMediator_presentImage:(UIImage *)image;

/// 中介 返回cell
- (UITableViewCell *)CTMediator_tableViewCellWithIdentifier:(NSString *)identifier tableView:(UITableView *)tableView;

/// 中介 配置cell
- (void)CTMediator_configTableViewCell:(UITableViewCell *)cell withTitle:(NSString *)title atIndexPath:(NSIndexPath *)indexPath;

/// 中介 清除cell点击
- (void)CTMediator_cleanTableViewCellTarget;

@end


















//
//  CTMediator+CTMediatorModuleAActions.m
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "CTMediator+CTMediatorModuleAActions.h"

/// 中介目标 A
NSString * const kCTMediatorTargetA = @"A";
/// 中介动作 本地获取详细视图控制器
NSString * const kCTMediatorActionNativeFetchDetailViewController = @"nativeFetchDetailViewController";
/// 中介动作 本地模态弹出图片
NSString * const kCTMediatorActionNativePresentImage = @"nativePresentImage";
/// 中介动作 本地没有图片
NSString * const kCTMediatorActionNativeNoImage = @"nativeNoImage";
/// 中介动作 展示警报
NSString * const kCTMediatorActionShowAlert = @"showAlert";
/// 中介动作 返回cell
NSString * const kCTMediatorActionCell = @"cell";
/// 中介动作 配置cell
NSString * const kCTMediatorActionConfigCell = @"configCell";

@implementation CTMediator (CTMediatorModuleAActions)

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

/// 中介 模态弹出图片
- (void)CTMediator_presentImage:(UIImage *)image
{
    if (image) {
        [self performTarget:kCTMediatorTargetA
                     action:kCTMediatorActionNativePresentImage
                     params:@{@"image":image}
          shouldCacheTarget:NO];
    } else {
        // 这里处理image为nil的场景,如何处理取决于产品
        [self performTarget:kCTMediatorTargetA
                     action:kCTMediatorActionNativeNoImage
                     params:@{@"image":[UIImage imageNamed:@"noImage"]}
          shouldCacheTarget:NO];
    }
}


/**
 中介 显示警报信息
 
 @param message 警报信息
 @param cancelAction 取消动作回调
 @param confirmAction 确定动作回调
 */
- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction
{
    // 可变参数字典,将要发送出去的参数
    NSMutableDictionary *paramsToSend = [[NSMutableDictionary alloc] init];
    
    if (message) {
        paramsToSend[@"message"] = message;
    }
    if (cancelAction) {
        paramsToSend[@"cancelAction"] = cancelAction;
    }
    if (confirmAction) {
        paramsToSend[@"confirmAction"] = confirmAction;
    }
    
    [self performTarget:kCTMediatorTargetA
                 action:kCTMediatorActionShowAlert
                 params:paramsToSend
      shouldCacheTarget:NO];
}

/// 中介 返回cell
- (UITableViewCell *)CTMediator_tableViewCellWithIdentifier:(NSString *)identifier tableView:(UITableView *)tableView
{
    return [self performTarget:kCTMediatorTargetA
                        action:kCTMediatorActionCell
                        params:@{
                                 @"identifier":identifier,
                                 @"tableView":tableView
                                 }
             shouldCacheTarget:YES];
}

/// 中介 配置cell
- (void)CTMediator_configTableViewCell:(UITableViewCell *)cell withTitle:(NSString *)title atIndexPath:(NSIndexPath *)indexPath
{
    // perform Target 执行目标
    [self performTarget:kCTMediatorTargetA
                 action:kCTMediatorActionConfigCell
                 params:@{
                          @"cell":cell,
                          @"title":title,
                          @"indexPath":indexPath
                          }
      shouldCacheTarget:YES];
}

/// 中介 清除cell点击
- (void)CTMediator_cleanTableViewCellTarget
{
    [self releaseCachedTargetWithTargetName:kCTMediatorTargetA];
}

@end


CTMediator

//
//  CTMediator.h
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import <UIKit/UIKit.h>

/// 全局常量 中介参数关键字 Swift目标模块名称
extern NSString * const kCTMediatorParamsKeySwiftTargetModuleName;

@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 远程App调用入口
/**
 远程App调用入口
 performActionWithUrl 通过远程URL执行动作

 @param url                 远程URL
 @param completion          动作完成后的回调 字典block参数
 @return                    返回结果
 */
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;


// 本地组件调用入口
/**
 本地组件调用入口
 performTarget 执行目标

 @param targetName          目标名称
 @param actionName          动作名称
 @param params              参数字典
 @param shouldCacheTarget   是否缓存目标
 @return                    返回结果
 */
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;



/**
 释放缓存的目标 通过目标名称

 @param targetName          目标名称
 */
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;

@end




















//
//  CTMediator.m
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "CTMediator.h"
#import <objc/runtime.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];
    });
    return mediator;
}

/*
 远程App调用入口
 核心方法
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    // 查询字符串,符合RFC 1808。
    NSString *urlString = [url query];
    // componentsSeparatedByString 返回一个包含已被给定分隔符分隔的来自接收器的子字符串的数组。
    // 遍历 被 & 分隔的 urlString数组
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        // 取出被等号分隔的数组
        NSArray *elts = [param componentsSeparatedByString:@"="];
        // continue  忽略了当次循环continue语句后的代码
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
    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 执行目标
 
 @param targetName          目标名称
 @param actionName          动作名称
 @param params              参数字典
 @param shouldCacheTarget   是否缓存目标
 @return                    返回结果
 */
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    // 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.cachedTarget[targetClassString];
    if (target == nil) {
        // 按名称获取类。(将目标类的字符串 转化为 目标类对象)
        Class targetClass = NSClassFromString(targetClassString);
        // 创建目标类的实例.    alloc:分配内存。 init:初始化。
        target = [[targetClass alloc] init];
    }

    //==============================================================
    // generate action  生成动作
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    // NSSelectorFromString 返回带有给定名称的选择器。
    // 通过动作字符串 创建动作方法
    SEL action = NSSelectorFromString(actionString);
    
    //=============================================================
    // 处理 target == nil 的情况.
    // 但是上面已经通过 targetClassString = Target_%@ 创建了target对象.
    // 所以 这里可能是多余的.
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    //================================================================
    // 判断是否缓存目标
    if (shouldCacheTarget) {
        // 将目标对象 放入缓存目标的可变字典
        self.cachedTarget[targetClassString] = target;
    }

    //================================================================
    // 处理 target 没有 action 的情况
    // respondsToSelector       返回一个布尔值,该值指示接收方是否实现或继承可以响应指定消息的方法。
    if ([target respondsToSelector:action]) {
        // 核心方法 安全的执行action
        return [self safePerformAction:action target:target params:params];
    } else {
        // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            // 核心方法 安全的执行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;
        }
    }
}

/**
 释放缓存的目标 通过目标名称
 
 @param targetName          目标名称
 */
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    [self.cachedTarget removeObjectForKey:targetClassString];
}

#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];
}



/**
 确保安全执行动作的方法 通过OC运行时实现的核心方法

 @param action 执行动作的方法
 @param target 目标对象(执行动作的主体调用者)
 @param params 参数字典
 @return 返回模块通过Action_方法创建出来的对象
 */
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    // methodSignatureForSelector 返回一个NSMethodSignature对象,该对象包含由给定选择器标识的方法的描述。
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    // methodReturnType 在Objective-C类型编码中 对方法的返回类型进行编码的C字符串。
    const char* retType = [methodSig methodReturnType];

    
    
    
    //=============================================================
    /** 注释
     
     1、iOS 的基础数据类型及其包装类型:
     1.1、iOS 的基础数据类型
     int、float、double、long、char、NSInteger、NSUInteger、CGFloat、BOOL等
     
     基本数据类型——char类型(在计算机内部以int类型存储)
     
     在 64 位平台和类似于 64 位平台的各种平台上,
     NSInteger-> long,
     NSUInteger-> unsigned long,
     CGFloat-> double.
     
     1.2、iOS 的基础数据类型与包装类型的转换
     由于只有对象类型才能放入数组、字典中,所以需要将基本数据类型转换成包装类型,OC 中提供的包装类是 NSNumber,NSValue。其中NSNumber 继承于 NSValue。NSNumber 主要针对于基本数据类型的包装,NSValue 主要针对结构体进行包装。
     
     https://www.jianshu.com/p/ff2274430b1c
     */
    
    
    /** 注释
     strcmp函数
     是string compare(字符串比较)的缩写,用于比较两个字符串并根据比较结果返回整数。
     基本形式为strcmp(str1,str2),
     若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。
     
     @encode(Type)
     @encode()
     作用:用来判断类型,常和strcmp(ObjCType, @encode(Type))合用。
     @encode(Type) 可以返回该类型的 C 字符串(char *)的表示。
     
     */
    
    
    // 下面这些if语句的作用,应该是排除返回值是基本数据类型的情况.
    // @encode() 作用:用来判断类型,常和strcmp(ObjCType, @encode(Type))合用。
    // 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(void)
    if (strcmp(retType, @encode(void)) == 0) {
        // 返回一个能够使用给定方法签名构造消息的NSInvocation对象。Invocation 调用
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        // 设置接收者的参数。
        /** 注释
         atIndex 指数
         指定参数索引的整数。
         索引0和1分别表示隐藏参数self和_cmd;您应该使用target和selector属性直接设置这些值。对于通常在消息中传递的参数,使用索引2或更大。
         */
        [invocation setArgument:&params atIndex:2];
        // 接收器的选择器,如果没有设置则为0。
        [invocation setSelector:action];
        // 接收者的目标,如果接收者没有目标,则为nil。
        [invocation setTarget:target];
        // 将接收方的消息(带有参数)发送到其目标,并设置返回值。
        [invocation invoke];
        return nil;
    }

    // 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(NSInteger)
    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;
        // 获取接收者的返回值。
        // 将模块中 方法的返回值得实际值 赋值给result
        [invocation getReturnValue:&result];
        // 返回oc对象(将返回值的实际值 包装成OC对象)
        return @(result);
    }

    // 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(BOOL)
    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);
    }

    // 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(CGFloat)
    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);
    }

    // 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(NSUInteger)
    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);
    }

    
/** 注释
 详情链接
 https://www.cnblogs.com/lurenq/p/7709731.html
 首先#pragma在本质上是声明,常用的功能就是注释,尤其是给Code分段注释;
 而且它还有另一个强大的功能是处理编译器警告,但却没有上一个功能用的那么多。
 
 clang diagnostic 是#pragma 第一个常用命令:
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-相关命令"
 // 你自己的代码
 #pragma clang diagnostic pop
 */
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // 在这里写 自己的代码;
    // 这三行 #pragma clang diagnostic 代码,是忽略警告的固定格式.
    // 如果你确定不会发生内存泄漏的情况下,可以使用如下的语句来忽略掉这条警告 "-Warc-performSelector-leaks"
    
    // 以对象作为参数 向接收者发送消息。
    // 中介的核心代码,目标通过参数执行动作以后,得到Target_A执行后返回的对象.
    // 实际上,真正创建出所需要的实例对象的类 是Target_A的类.(Target_A是真正干活的)
    id Target_Object = [target performSelector:action withObject:params];
    return Target_Object;
    
#pragma clang diagnostic pop
    
}

#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
    if (_cachedTarget == nil) {
        _cachedTarget = [[NSMutableDictionary alloc] init];
    }
    return _cachedTarget;
}

@end

模块部分

Target_A

//
//  Target_A.h
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Target_A : NSObject

/// 动作 本地获取详情视图控制器
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
/// 动作 本地模态弹出图片
- (id)Action_nativePresentImage:(NSDictionary *)params;
/// 动作 展示警报
- (id)Action_showAlert:(NSDictionary *)params;

// 容错
- (id)Action_nativeNoImage:(NSDictionary *)params;

@end






















//
//  Target_A.m
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "Target_A.h"
#import "DemoModuleADetailViewController.h"

typedef void (^CTUrlRouterCallbackBlock)(NSDictionary *info);

@implementation Target_A

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

- (id)Action_nativePresentImage:(NSDictionary *)params
{
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = @"this is image";
    // 通过参数字典 取出字典中的对象
    viewController.imageView.image = params[@"image"];
    // 窗口的根控制器 模态弹出 视图控制器
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
    // id 的返回值可以为空
    return nil;
}

- (id)Action_showAlert:(NSDictionary *)params
{
    // 创建并返回具有指定标题和行为的操作。
    // handler : 处理程序
    // 当用户选择操作时要执行的块。这个块没有返回值,只接受选择的action对象作为它的唯一参数。
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        // 重点
        // 通过参数字典 取出回调的block;参数字典中,包含回调block对象.
        CTUrlRouterCallbackBlock callback = params[@"cancelAction"];
        // 如果回调有值,执行回调动作.
        if (callback) {
            callback(@{@"alertAction":action});
        }
    }];
    
    UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"confirm" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        CTUrlRouterCallbackBlock callback = params[@"confirmAction"];
        if (callback) {
            callback(@{@"alertAction":action});
        }
    }];
    
    // 创建 警报控制器
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"alert from Module A" message:params[@"message"] preferredStyle:UIAlertControllerStyleAlert];
    // 添加动作
    [alertController addAction:cancelAction];
    [alertController addAction:confirmAction];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
    return nil;
}

/// 动作 本地没有图片(用来容错处理)
- (id)Action_nativeNoImage:(NSDictionary *)params
{
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = @"no image";
    viewController.imageView.image = [UIImage imageNamed:@"noImage"];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
    
    return nil;
}

/// 通过动作拿到cell
- (UITableViewCell *)Action_cell:(NSDictionary *)params
{
    UITableView *tableView = params[@"tableView"];
    NSString *identifier = params[@"identifier"];
    
    // 这里的TableViewCell的类型可以是自定义的,我这边偷懒就不自定义了。
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    return cell;
}

- (id)Action_configCell:(NSDictionary *)params
{
    NSString *title = params[@"title"];
    NSIndexPath *indexPath = params[@"indexPath"];
    UITableViewCell *cell = params[@"cell"];
    
    // 这里的TableViewCell的类型可以是自定义的,我这边偷懒就不自定义了。
    cell.textLabel.text = [NSString stringWithFormat:@"%@,%ld", title, (long)indexPath.row];
    
//    if ([cell isKindOfClass:[XXXXXCell class]]) {
//        正常情况下在这里做特定cell的赋值,上面就简单写了
//    }
    
    return nil;
}

@end

DemoModuleADetailViewController

//
//  DemoModuleADetailViewController.h
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface DemoModuleADetailViewController : UIViewController

/// 只读;外界只能拿到数据 但是不能修改
@property (nonatomic, strong, readonly) UILabel *valueLabel;
@property (nonatomic, strong, readonly) UIImageView *imageView;

@end


















//
//  DemoModuleADetailViewController.m
//  CTMediator
//
//  Created by casa on 16/3/13.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "DemoModuleADetailViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>

@interface DemoModuleADetailViewController ()

/// 读写;重写属性,使 点m文件 中的属性 可以被操作
@property (nonatomic, strong, readwrite) UILabel *valueLabel;
@property (nonatomic, strong, readwrite) UIImageView *imageView;

@property (nonatomic, strong) UIButton *returnButton;

@end

@implementation DemoModuleADetailViewController

#pragma mark - life cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor grayColor];
    
    [self.view addSubview:self.valueLabel];
    [self.view addSubview:self.imageView];
    [self.view addSubview:self.returnButton];
}

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    
    [self.valueLabel sizeToFit];
    [self.valueLabel topInContainer:70 shouldResize:NO];
    // 横坐标居中
    [self.valueLabel centerXEqualToView:self.view];
//    // 等效于
//    self.valueLabel.ct_x = self.view.ct_centerX;
    
    self.imageView.ct_size = CGSizeMake(100, 100);
    [self.imageView centerEqualToView:self.view];
    
    self.returnButton.ct_size = CGSizeMake(100, 100);
    [self.returnButton bottomInContainer:0 shouldResize:NO];
    [self.returnButton centerXEqualToView:self.view];
}

#pragma mark - event response 事件响应
- (void)didTappedReturnButton:(UIButton *)button
{
    // 判断当前导航控制器是否为空;从而得知当前控制器弹出的方式
    
    if (self.navigationController == nil) {
        // 解散由视图控制器模态呈现的视图控制器。
        [self dismissViewControllerAnimated:YES completion:nil];
    } else {
        [self.navigationController popViewControllerAnimated:YES];
    }
}

#pragma mark - getters and setters
- (UILabel *)valueLabel
{
    if (_valueLabel == nil) {
        _valueLabel = [[UILabel alloc] init];
        _valueLabel.font = [UIFont systemFontOfSize:30];
        _valueLabel.textColor = [UIColor blackColor];
    }
    return _valueLabel;
}

- (UIImageView *)imageView
{
    if (_imageView == nil) {
        _imageView = [[UIImageView alloc] init];
        _imageView.contentMode = UIViewContentModeScaleAspectFit;
    }
    return _imageView;
}

- (UIButton *)returnButton
{
    if (_returnButton == nil) {
        _returnButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [_returnButton addTarget:self action:@selector(didTappedReturnButton:) forControlEvents:UIControlEventTouchUpInside];
        [_returnButton setTitle:@"return" forState:UIControlStateNormal];
        [_returnButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    }
    return _returnButton;
}

@end

主框架调用的TableView二级页面

TableViewController

//
//  TableViewController.h
//  CTMediator
//
//  Created by casa on 2016/10/20.
//  Copyright © 2016年 casa. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface TableViewController : UIViewController

@end
















//
//  TableViewController.m
//  CTMediator
//
//  Created by casa on 2016/10/20.
//  Copyright © 2016年 casa. All rights reserved.
//

#import "TableViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>
#import "CTMediator+CTMediatorModuleAActions.h"

@interface TableViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UIButton *closeButton;

@end

@implementation TableViewController

#pragma mark - life cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.tableView];
    [self.view addSubview:self.closeButton];
}

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    
    [self.tableView fillWidth];
    [self.tableView topInContainer:0 shouldResize:YES];
    [self.tableView bottomInContainer:50 shouldResize:YES];
    
    [self.closeButton fillWidth];
    [self.closeButton top:0 FromView:self.tableView];
    [self.closeButton bottomInContainer:0 shouldResize:YES];
}

- (void)dealloc
{
    // 在Controller被回收的时候,把相关的target也回收掉
    [[CTMediator sharedInstance] CTMediator_cleanTableViewCellTarget];
//    [CTMediator.sharedInstance CTMediator_cleanTableViewCellTarget];
}

#pragma mark - UITableViewDelegate
// cell即将显示的时候
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 通过Mediator来获取cell实例,由于target已经被cache了,高频调用不是问题。
    [[CTMediator sharedInstance] CTMediator_configTableViewCell:cell withTitle:@"cell title" atIndexPath:indexPath];
}

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 100;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 通过mediator来配置cell实例,由于target已经被cache了,高频调用不是问题。
    return [[CTMediator sharedInstance] CTMediator_tableViewCellWithIdentifier:@"cell" tableView:tableView];
}

#pragma mark - event response  事件响应
// 点击关闭按钮
- (void)didTappedCloseButton:(UIButton *)button
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - getters and setters
- (UITableView *)tableView
{
    if (_tableView == nil) {
        _tableView = [[UITableView alloc] init];
        _tableView.delegate = self;
        _tableView.dataSource = self;
    }
    return _tableView;
}

- (UIButton *)closeButton
{
    if (_closeButton == nil) {
        _closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [_closeButton setTitle:@"Close" forState:UIControlStateNormal];
        [_closeButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
        // 点击按钮要执行的方法
        [_closeButton addTarget:self action:@selector(didTappedCloseButton:) forControlEvents:UIControlEventTouchUpInside];
        _closeButton.backgroundColor = [UIColor grayColor];
    }
    return _closeButton;
}

@end

Demo链接

https://github.com/casatwy/CTMediator

引用

组件化架构漫谈 https://www.jianshu.com/p/67a6004f6930

iOS应用架构谈 组件化方案 https://casatwy.com/iOS-Modulization.html

iOS中的三种路由方案实践 https://www.jianshu.com/p/72d705ecc177

iOS 组件化 —— 路由设计思路分析 https://www.jianshu.com/p/76da56b3bd55

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容