iOS货运用户App组件路由器设计与实践

一、背景

在 iOS 应用程序开发中,组件路由方案是一种用于管理应用程序不同模块之间导航和通信的设计模式。随着应用程序规模的增长和功能的复杂性,需要一种可靠的机制来实现模块之间的解耦和交互。组件路由方案的目标是提供一种结构化的方式来管理模块之间的跳转和数据传递,以实现灵活的模块化架构。

在传统的 iOS 应用程序中,通常使用故事板(Storyboard)或导航控制器(Navigation Controller)来处理应用程序的导航。这种方法在小型应用中可能是有效的,但在大型应用中会带来一些问题。例如,当应用程序规模增大时,导航关系变得复杂,故事板中的视图控制器之间的耦合度也增加,导致维护和扩展变得困难。

另一个问题是模块之间的解耦和复用。在应用程序中,可能存在多个模块(如用户模块、支付模块、消息模块等),它们具有不同的功能和界面。传统的导航方式难以将这些模块分离开来,导致模块间的依赖性增加,难以进行单独开发和测试。

为了解决这些问题,出现了组件路由方案的设计。组件路由方案将导航和通信的责任分散到各个模块中,使每个模块能够独立管理自己的导航和数据传递。它提供了一种解耦的方式,允许模块间通过定义和触发路由事件来进行通信,而不需要直接引用其他模块的具体实现。

组件路由方案的设计背景可以总结为以下几点:

  1. 解耦和复用:通过组件路由方案,模块之间的依赖性得以降低,模块可以独立开发、测试和维护。每个模块只需要关注自己的功能和界面,而不需要了解其他模块的具体实现。
  2. 灵活性和扩展性:组件路由方案提供了一种结构化的方式来管理模块之间的跳转和数据传递。它可以轻松地扩展和修改导航关系,而不会对其他模块产生影响。这使得应用程序的功能可以快速迭代和演进。
  3. 统一的导航控制:组件路由方案提供了一种统一的方式来管理导航控制,无论是通过界面跳转还是以编程方式进行导航。这使得应用程序的导航行为更加一致和可预测。
  4. 模块间通信:组件路由方案通过定义和触发路由事件,实现了模块间的通信。模块可以通过路由事件传递数据和消息,以完成特定的操作和交互。

总的来说,组件路由方案的设计背景是为了解决大型应用程序中导航和通信的复杂性,提供一种解耦和模块化的架构,同时保持灵活性和可扩展性。它能够帮助开发人员构建更可维护、可扩展和可测试的 iOS 应用程序。

二、目标

  1. 降本增效,提升大型APP多人协作的开发效率。

  2. 组件调用接口文档化、可视化,方便使用和维护。

  3. 保持组件解偶的特性,调用方无所依赖组件名,只面向组件接口编程。

  4. 独立的组件可单独编译开发,保持最小化的业务开发体验。

在 iOS 开发领域,有几种常见的组件路由设计方案可供参考。以下是其中一些方案以及它们的优势和劣势:

URL注册方案

优点:

  1. 最容易想到的最简单的方式
  2. 可以统一三端的调度
  3. 线上bug动态降级处理

缺点:

  1. 硬编码,URL需要专门管理
  2. URL规则需要提前注册
  3. 有常驻内存,可能发生内存问题
Protocol-Class方案

优点:

  1. 无硬编码
  2. 参数无限制,甚至可以传递model

缺点:

  1. 增加了新的中间件以及很多protocol,调用编码复杂
  2. route的跳转逻辑分散在各个类中,不好维护
Target-Action方案

优点:

  1. 利用runtime实现解耦调用,无需注册
  2. 有一定的安全处理
  3. 统一App外部和组件间的处理

缺点:

  1. 需要为每一个组件另外创建一个category,作者建议category也是一个单独的pod
  2. 调用内部也是硬编码,要求Target_ ,Action_ 命名规则

这些方案都有各自的优势和劣势,选择适合的方案取决于项目需求、团队技术栈和个人偏好。在实际开发中,也可以根据具体情况结合不同方案的特点,进行定制化的组件路由设计。

对于组件化方案的架构设计优劣直接影响到架构体系能否长远地支持未来业务的发展,对App的组件化不只是仅仅的拆代码和跨业务调页面,还要考虑复杂和非常规业务参数参与的调度,非页面的跨组件功能调度,组件调度安全保障,组件间解耦,新旧业务的调用接口修改等问题。模块化解耦需求的更准确的描述应该是"如何在保证开发质量和效率的前提下做到无代码依赖的跨模块通信",我们当前所做的路由方案是为后述的组件化业务拆分做好铺垫。

三、TTSpringRouter方案

方案选型 Protocol-Class 路由方案是一种基于协议和类的组件路由设计方案,它通过协议定义路由规范,使用类来实现具体的路由逻辑。下面是 Protocol-Class 路由方案的优点和缺点的详细分析:

优点:

  1. 解耦和模块化:Protocol-Class 路由方案能够实现模块之间的解耦,每个模块只需要遵循定义的协议来实现自己的路由逻辑。不同模块之间的通信通过协议进行,降低了模块间的依赖性,增加了模块的独立性和可复用性。
  2. 灵活性和扩展性:由于 Protocol-Class 路由方案基于协议和类的设计,可以方便地扩展和修改路由逻辑。新增模块只需要遵循协议并实现相应的类,不会对其他模块产生影响,同时也不需要修改已有的代码。
  3. 类型安全:通过协议的定义,可以确保路由逻辑的类型安全。只有实现了协议的类才能被用作路由的目标,避免了潜在的类型错误和运行时异常。
  4. 单一职责原则:Protocol-Class 路由方案鼓励每个模块关注自己的路由逻辑,遵循了单一职责原则。每个模块负责自己的路由实现,使代码更加清晰和可维护。
  5. 可测试性:Protocol-Class 路由方案使得路由逻辑可以更方便地进行单元测试。由于路由逻辑与具体的视图控制器解耦,可以使用模拟对象来进行测试,提高了测试的灵活性和可靠性。

缺点:

  1. 全局路由管理:Protocol-Class 路由方案通常需要一个全局的路由管理器来协调不同模块之间的路由行为。这可能导致路由管理器的职责过重,需要维护路由注册和调度逻辑,增加了一定的复杂性。

  2. 依赖管理:Protocol-Class 路由方案在模块之间可能存在较多的依赖关系。模块之间需要知道对方的协议定义,以便进行通信。这要求开发团队需要良好的协作和协议约定,以避免依赖关系的混乱和冲突。

  3. 路由规范定义:Protocol-Class 路由方案需要明确定义和遵循统一的路由规范。这涉及到协议的设计和维护

设计思路

阿里巴巴集团开发的开源项目:BeeHive框架 ,它是一种基于iOS平台的轻量级框架,用于实现模块化开发和解耦。在提供一种灵活、可扩展的架构,以便开发人员可以更高效地构建iOS应用程序。

BeeHive组件的核心思想是通过组件化的方式将一个复杂的iOS应用程序拆分为多个独立的模块。每个模块都可以独立开发、测试和维护,并且可以在不同的应用程序中重用。这种模块化的架构有助于降低代码的耦合度,提高代码的可维护性和可测试性。

其中,BeeHive框架还提供了一种服务注册和获取的服务机制(Service)。组件可以将自己的功能封装成服务,并将其注册到BeeHive中。其他组件可以通过BeeHive获取所需的服务实例,实现组件之间的交互和协作。其核心思想也基于protocol-class方案的运行模式。

为此我们决定剥离BeeHive框架,抽取Service模块,通过改造加强其运行时的性能,和开发上的效率。能力描述如下:

基于protocol-class模式下的路由组件。 包含三大模块:

  1. 核心层包含路由注册、服务实现创建。
  2. 安全协议接口调用,用于保护未实现的协议方法调用
  3. 运行时的路由日志输出,包含:日志、监控、异常堆栈上报。

基于Spring框架Service的理念,通过protocol-class注册绑定关系来实现模块间的API解耦。

  1. 各个组件以服务的形式存在,每个都可独立,以达到相互解耦的目的。

  2. 各个服务具体实现与接口调用分离。 解决网状依赖的问题。

  3. 面向协议编程。 即服务与服务调用都是基于服务协议进行配合,通过TTSpringRouter路由达到调用目的。

整体架构

上下游服务,子模块关系等。

TTSpringRouter路由器架构设计

  1. TTSpringRouter:

基于Spring框架Service的理念,通过protocol-class注册绑定关系来实现模块间的API解耦。

  • 1。各个组件以服务的形式存在,每个都可独立,以达到相互解耦的目的。
  • 2。各个服务具体实现与接口调用分离。 解决网状依赖的问题。
  • 3。面向协议编程。 即服务与服务调用都是基于服务协议进行配合,通过TTSpringRoute路由达到调用目的。
  1. TTSafetyService:

组件服务实现基类,用于处理方法调用异常的问题。比如未实现某个协议的方法,却被调用了。 每个组件服务实现都可继承此类。举例:

less 复制代码
@interface TTIMService : TTSafetyService 
@end
  1. TTSpringReportLog:

当前路由工具在运行时的内部日志输出,用于调试分析以及线上运行情况收集。

TTSpringRouter核心流程时序图

  1. TTSpringRouter做为路由服务的核心,将以单例形式存在
  2. 所有组件将继承自TTSafetyService,确保基于组件协议的组件方法调用出现异常情况下,不导致App产生Crash。
  3. 每个服务可配置成单例,或者多对象实例。可根据自己业务场景进行选择配置。

组件化实施架构图

  1. 将整体的App架构分为5层,分别是端侧工程、业务集合层、服务接口层、服务实现层、基础层,分别实现不的角色职责。

  2. 业务集合层将包含所有业务线实现,每条业务线可以依赖多个服务,可以是基础服务、也可以是业务服务。但只关心服务接口协议,并不需要关注具体的实现,从而达到解偶。

  3. 服务接口层做为整个路由中心,需要管理好接口协议变更,做为一个组件调用中台集合,每个API声明为其它组件调用的接口,内部的实现通过protocol-class模式进行调用,完全隔离了对其它组件的依赖。

业务流程

抽象和描述业务流程。

Demo架构设计

売工程Profile依赖配置

各组件工程结构

  1. TTServiceProtocol PodSpec描述
  1. IMService PodSpec描述

IM组件实现:

  1. LoginService PodSpec描述

Login组件实现

组件调用代码示例

ini 复制代码
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  [tableView deselectRowAtIndexPath:indexPath animated:YES];
   
   
  if (indexPath.row == 0) {
    // crash测试
    [TTRoute(TTIMServiceProtocol) class_CrashUnComp];
  }   
  //改造后的调用方式
  else if (indexPath.row == 1) {
     
    NSLog(@"> 手机登录");
     
    [TTRoute(TTLoginServiceProtocol) LoginWithPhone:@"1354858" instanceResponse:^(id instance) {
       
      //跳转到组件VC页面。
      [navigationController pushViewController:instance
                         animated:YES];
       
    } dataResponse:nil];
  }
  else if (indexPath.row == 2) {
     
    NSLog(@"> 短信登录");
     
    [TTRoute(TTLoginServiceProtocol) LoginWithSMSCode:@{@"phone":@"1354858", @"token":@"10086"} instanceResponse:^(id instance) {
      [self。navigationController pushViewController:instance
                         animated:YES];
    } dataResponse:^(id data) {
       
    }];
  }
  else if (indexPath.row == 3) {
    [TTRoute(TTLoginServiceProtocol) accountLogoutWithVC:self];
  }
  else if (indexPath.row == 4) {

    [TTRoute(TTIMServiceProtocol) IMLoginWithParmater:self
         instanceResponse:nil
           dataResponse:nil];
  }
  else if (indexPath.row == 5) {
    [TTRoute(TTIMServiceProtocol) IMLogout:self];
  }
  else if(indexPath.row == 6) {
    [TTRoute(TTIMServiceProtocol) selectDriverListWithPhone:@"135489922" instanceResponse:^(id instance) {
       
      [navigationController pushViewController:instance animated:YES];
       
        } dataResponse:^(id data) {
           
        }];
  }
  else if (indexPath.row == 7) {
     
    TTLoginServiceModel *model = [[TTLoginServiceModel alloc] init];
    model.phone = @"1354868333";
    model.token = @"110073";
     
    [TTRoute(TTLoginServiceProtocol) LoginWithModel:model instanceResponse:^(id instance) {
       
      [self.navigationController pushViewController:instance animated:YES];
       
        } dataResponse:^(id data) {
           
        }];
  }
  else if (indexPath.row == 8) {
     

    TTIMServiceModel *model = [[TTIMServiceModel alloc] init];
    model.showInVC = self;
    model.phone = @"1378543344";
    model.token = @"998suuw1j913";
     
    [TTRoute(TTIMServiceProtocol) IMLogin:model instanceResponse:^(id instance) {
       
      [navigationController pushViewController:instance animated:YES];
    }];
  }
   
  return;
   
}

四、项目实施实践

原用户端工程是基于CTMediator路由器框架下的组件设计,为了进行重构工作,我和同事进入了如下操作

  1. 梳理当前端上的服务组件列表。
  2. 以业务模块为单元拆解任务,规划排期。

经过资源盘点,我们规划了4期的渐近式重构,在不影响业务迭代的情况下,和业务需求开发同学一起完成各组件、模块的重构工作,具体操作如下:

4.1 首先明确TTSpringRouter的使用接入方式:

[服务提供者]:

  1. 编写您组件的协议文件,包含哪些外部可以使用的API接口。 需要将这个协议.h 文件放到 TTServiceProtocol组件中统一管理。
  2. 基于第一步编写的协议头文件,创建一个服务类,可继承于TTSafetyService类,将您需要实现的协议加入到此服务类中,并实现里面所有协议方法。(有声明,必须现实)
  3. 在你的服务类.m文件中,加入 TTServiceRegister(协议名),以绑定服务类与实现的协议。
  4. 举例: @ interface TTIMService : TTSafetyService @end

[接口调用者]:

  1. 在你的代码处引入您想使用某组件的具体的协议头文件。
  2. 使用TTServiceInstance(协议名) 调用协议里的具体API方法。
  3. 举例: [TTRoute(TTIMServiceProtocol) Login:@"1354858"];

其次

准备工作一(组件接口重构)

准备要改造的组件名: TTInvitetionAndCustomerService

头文件变更:(添加父类TTSafetyService)

objectivec 复制代码
#import <Foundation/Foundation.h>
#import <TTSpringRouter/TTSafetyService.h>

@interface TTInvitetionAndCustomerService : TTSafetyService

@end

实现文件变更:(添加协议支持)

objectivec 复制代码
#import <TTSpringRouter/TTSpringRouter.h>

@interface TTInvitetionAndCustomerService()<TTInvitetionAndCustomerServiceProtocol>

@end

@implementation TTInvitetionAndCustomerService

//将当前类与协议进行路由注册绑定。
TTServiceRegister(TTInvitetionAndCustomerServiceProtocol)

编写组件的对外接口方法:

ini 复制代码
/// 打开客服中心
/// @param urlStr 客服中心url地址,如果为空,使用内部自定义地址。
/// @param isFromOrderDetailPage 是否从订单详情页面过来
/// @param instanceHandler 实例回调block
- (void)getServerCenterWebVCWithUrlStr:(NSString* _Nullable) urlStr
       isFromOrderDetailPage:(BOOL) isFromOrderDetailPage
          instanceResponse:(void (^ _Nonnull )(id instance))instanceHandler {
  if (instanceHandler == nil) {
    NSParameterAssert(instanceHandler != nil);
    return;
  }
   
  NSString* serverCenterUrl = [NSString isEmpty:urlStr] ? [TTUrlManager serverCenterUrlWithUserToken:[TTUserManager sharedInstance]。loginToken andDisplayId:0 orderUuid:nil]:urlStr;
   
  if (!isFromOrderDetailPage) {
    int cityId = [self cityIdForStartAddress];
    serverCenterUrl = [serverCenterUrl urlAddCompnentForValue:@(cityId)。stringValue key:@"city_id"];
  }

  ServerCenterVC * vc = [[ServerCenterVC alloc] initWithTitle:@"客服中心" url:serverCenterUrl autoTitle:NO];
  if (instanceHandler) {
    instanceHandler(vc);
  }
   
  return;
}


/// 获取不同标题的webview
/// @param webType [枚举: WebType] 不同的网页类型
/// @param instanceHandler TTWebCustomVC实例回调block
- (void)getTTWebVCWithWebType:(int) webType
       instanceResponse:(void (^ _Nonnull)(id instance))instanceHandler{
  if (instanceHandler == nil) {
    NSParameterAssert(instanceHandler != nil);
    return;
  }
   
  TTWebCustomVC *vc = [[TTWebCustomVC alloc] initWithWebType:webType];
   
  if (instanceHandler) {
    instanceHandler(vc);
  }
  return;
}

- (void)getCargoAgreementWebVCWithInstanceResponse:(void (^ _Nonnull )(id _Nullable instance))instanceHandler {
   
  NSString *urlString = [TTCommonShareManager sharedInstance].commonConfig.insurance。cargo_agreement_page;
  if (urlString。length>0) {
    TTWebNewVC *webVC = [[TTWebNewVC alloc]initWithTitle:nil url:urlString autoTitle:YES];
    if (instanceHandler) {
      instanceHandler(webVC);
    }
  }
  return;
}

/// 仅侧边栏开票跳转。 侧边栏跳转的地址从umeta获取 city_id不能使用公参里面的
/// @param instanceHandler TTWebNewVC实例回调block
- (void)getBillWebVCWithInstanceResponse:(void (^ _Nonnull )(id instance))instanceHandler {
   
  if (instanceHandler == nil) {
    NSParameterAssert(instanceHandler != nil);
    return;
  }
   
  int cityId = [self cityIdForStartAddress];
  NSString * url = @"";
  TTWebNewVC * vc = [[TTWebNewVC alloc] initWithTitle:@"发票" url: url autoTitle:YES];
  if (instanceHandler) {
    instanceHandler(vc);
  }
   
  return;
}

改造原CTMeditor路由方法:

ini 复制代码
/**
 此方法为原TTMeditor 路由方式,现阶段保留,后续TTSpringRouter线上验证成功后,可移除。
 */
-(void)requestWithParameter:(NSDictionary *)parameter instanceResponse:(void (^)(id))instanceHandler dataResponse:(void (^)(id))dataHandler{
  if (!parameter) {
    return;
  }
  int type = [[parameter objectForKey:@"routeType"] intValue];
  switch (type) {
    case 0:
    {
      if (!parameter) {
        return;
      }
      bool isFromOrderDetailPage = NO;
      if ([parameter objectForKey:@"orderDetail"]) {
        isFromOrderDetailPage = YES;
      }
       
      NSString *urlStr = [parameter objectForKey:@"urlStr"];
       
      [self getServerCenterWebVCWithUrlStr:urlStr
              isFromOrderDetailPage:isFromOrderDetailPage
                instanceResponse:instanceHandler];
    }
      break;
   
      case 1:
    {
      if (!parameter) {
        return;
      }
      int WebType = [[parameter objectForKey:@"WebType"] intValue];
       
      [self getTTWebVCWithWebType:WebType
            instanceResponse:instanceHandler];
    }
      break;
      case 2:
    {
      [self getLiftAgreementWebVCWithInstanceResponse:instanceHandler];
    }
      break;
    case 3:
    {
      [self getBillWebVCWithInstanceResponse:instanceHandler];
    }
      break;
  }
   
}

准备工作二(组件接口编写)

  1. 在TTUpodDepeLock建立自己要改造组件的接口头文件
  2. 将组件的方法头文件copy到protocol.h里,实现同步。
objectivec 复制代码
@protocol TTInvitetionAndCustomerServiceProtocol <NSObject>

/// 打开客服中心
/// @param urlStr 客服中心url地址,如果为空,使用内部自定义地址。
/// @param isFromOrderDetailPage 是否从订单详情页面过来
/// @param instanceHandler 实例回调block
- (void)getServerCenterWebVCWithUrlStr:(NSString* _Nullable) urlStr
         isFromOrderDetailPage:(BOOL) isFromOrderDetailPage
           instanceResponse:(void (^ _Nonnull )(id _Nullable instance))instanceHandler;

/// 获取不同标题的webview
/// @param webType [枚举: WebType] 不同的网页类型
/// @param instanceHandler TTWebCustomVC实例回调block
- (void)getTTWebVCWithWebType:(int) webType
       instanceResponse:(void (^ _Nonnull )(id _Nullable instance))instanceHandler;


/// 打开意外险保障协议页面
/// @param instanceHandler TTWebNewVC实例回调block
- (void)getCargoAgreementWebVCWithInstanceResponse:(void (^ _Nonnull )(id _Nullable instance))instanceHandler;


/// 仅侧边栏开票跳转。 侧边栏跳转的地址从umeta获取 city_id不能使用公参里面的
/// @param instanceHandler TTWebNewVC实例回调block
- (void)getBillWebVCWithInstanceResponse:(void (^ _Nonnull )(id _Nullable instance))instanceHandler;
@end

4.2 进入组件接口调用替换

工作一(查找替换)

  1. 在现有的LALA工程下全局搜索你要改造的组件接口调用名。
  2. 根据上述列表,逐一替换成以SpringRouter模式的调试方式。

工作二(单元测试)

编写相关的调用测试用例:

  • 界面调用Debug
  • 单元测试用例

五、实践总结

不足:

  1. 有基础协议,存在代码侵入,组件以及调用方需要根据接口进行调用或实现。
  2. TTSpringRouter注册时通过协议识别模块,因此每个模块必须定义自己的协议;
  3. 每个组件服务的注册在+load方法中执行,会占用启动时长,同时有个字典维护protocol-class的关系表,会占用一定的内存。
  4. 解耦还不够彻底, 服务和调用方都需要依赖接口定义的头文件,通过模块增加得越多,维护接口定义的也有一定工作量。

优点:

  1. 模块不存在或未注册时,不会获取到实例,可以灵活处理;
  2. 可以编译时检查发现接口的变更,从而及时修正接口问题;
  3. 提供模块生命周期管理,扩展了应用的系统事件,可为未来插件化做准备。
  4. 支持模型对象的传输调用,很好的做到模型-组件的管理。
  5. 解决了硬编码的问题。

六、复盘&收益

文章引用:

juejin.cn/post/684790...

相关推荐
我命由我123452 小时前
35.Java线程池(线程池概述、线程池的架构、线程池的种类与创建、线程池的底层原理、线程池的工作流程、线程池的拒绝策略、自定义线程池)
java·服务器·开发语言·jvm·后端·架构·java-ee
喵叔哟9 小时前
14.【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--CAP
微服务·架构·.net
黎明鱼儿12 小时前
高可用架构:Keepalived、Nginx与Docker深度解析
nginx·docker·架构
文火冰糖的硅基工坊12 小时前
[创业之路-366]:投资尽职调查 - 尽调核心逻辑与核心影响因素:价值、估值、退出、风险、策略
架构·管理·公司·战略·治理
白露与泡影16 小时前
Nginx 是什么?Nginx高并发架构拆解指南
运维·nginx·架构
探索为何16 小时前
SQL解析器系列:实现ALTER TABLE语句
后端·架构
Funny Valentine-js17 小时前
swift菜鸟教程29-30(泛型,访问控制)
开发语言·ios·swift
爱的叹息19 小时前
关于 微服务负载均衡 的详细说明,涵盖主流框架/解决方案的对比、核心功能、配置示例及总结表格
微服务·架构·负载均衡
forestsea19 小时前
微服务面试题:服务网关和链路追踪
微服务·云原生·架构
严文文-Chris19 小时前
MySQL逻辑架构有什么?
数据库·mysql·架构