随着移动互联网的快速发展,项目的迭代速度越来越快,需求改变越来越频繁,传统开发方式的工程所面临的一些,如代码耦合严重、维护效率低、开发不够敏捷等问题就凸现了出来。于是越来越多的公司开始推行"组件化",通过对原有业务或新业务进行组件(或模块)拆分来提高并行开发效率。

在笔者面试过程中发现,很多同学口中的"组件化"也只是把代码分库,然后在主项目中使用 CocoaPods 把各个子库聚合起来。对于怎样合理地对组件分层、如何管理组件(主要包括组件的生命周期管理和组件的通信管理),如何管理不同版本的依赖,以及是否有整套集成和发布工具,这类问题的知之甚少。如果完全不了解这些问题,那么只是简单的对主项目进行组件拆分,并不能提高多少开发效率。

笔者认为合理地进行组件拆分和管理各个组件之间的通信是组件化过程中最大的难点。合理地进行组件拆分是为了解耦,并且各个组件能更容易地独立变化。而对于一个完整的应用来说,每个组件不可能孤零零地存在,必定会互相调用。这样不同组件之间必须能进行通信而又没有编译期的依赖

组件生命周期管理

可能很多同学在实施组件化的过程中知道要解决组件通信的问题,却很少关注组件的生命周期。这里的生命周期主要是指 AppDelegate 中的生命周期方法。有时候一些组件需要在这些钩子方法中做一些事情,这时候就需要一个能够管理组件的工具,并在适当的时机执行组件相应的逻辑。

比如笔者在项目中是这样做的:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{[[Ant shareInstance] application:application didFinishLaunchingWithOptions:launchOptions];return YES;
}- (void)applicationWillResignActive:(UIApplication *)application
{[[Ant shareInstance] applicationWillResignActive:application];
}- (void)applicationDidEnterBackground:(UIApplication *)application
{[[Ant shareInstance] applicationDidEnterBackground:application];
}- (void)applicationWillEnterForeground:(UIApplication *)application
{[[Ant shareInstance] applicationWillEnterForeground:application];
}
复制代码

所有注册的组件(模块)会在 AppDelegate 相应的生命周期方法调用时自动调用。例如有如下组件定义:

ANT_MODULE_EXPORT(Module1App)@interface Module1App() <ATModuleProtocol> {NSInteger state;
}
@end@implementation Module1App
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {state = 0;NSLog(@"Module A state: %zd", state);return YES;
}- (void)applicationWillEnterForeground:(UIApplication *)application {state += 1;NSLog(@"Module A state: %zd", state);
}
@end
复制代码

上面示例代码中第一行的 ANT_MODULE_EXPORT(Module1App) 是导出组件。Ant 会在 dyld 加载完 image 后将导出的组件进行注册。当应用生命周期方法被调用时,会实例化所有注册过的组件,调用组件相应的方法,并进行缓存,之后再次调用就会从缓存中取出组件的实例对象。

一般拥有完整生命周期的组件一般称为一个模块,一个模块其实也是一个独立的组件,它一般是包含一个完整的业务,列如:登录模块,外卖模块,消息模块等。

组件的生命周期管理并不复杂,实现方案都没有太大区别,但它也是组件化中必不可少的部分。

组件通信

业界关于组件通信的方案比较多,主要有:url-block, target-action, protocol-class。下面笔者会对这三种方案做个简单的介绍。

URL-Block

这是蘑菇街在组件化过程中使用的一种组件间通信方式,在应用启动时注册组件提供的服务,把调用组件使用的url和组件提供的服务block对应起来,保存到内存中。在使用组件的服务时,通过url找到对应的block,然后获取服务。

[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];[MGJRouter openURL:@"mgj://foo/bar"];
复制代码

笔者是在15年开始学习组件化,那个时候就是使用的蘑菇街的这种发案。不过笔者从来没有在实际项目中使用这种方案。casa 在这篇文章中批判了这种方案。笔者对 case 的观点很是赞同。

如果项目中需要很多组件的服务,那么就需要在内存中维护大量的 url-block项,造成内存问题,对于服务注册的代码应该放在什么地方也是一个问题。笔者一直认为 url-block 注册是一种很粗暴的方式,比如某个应用在启动时注册了100个服务,但某些服务在用户使用过程中根本就没有触发,这就造成了内存浪费。比如我们点击应用中的按钮跳转到某个页面,如果用户没有点击按钮,下个页面就永远不会创建,我们一般不会提前创建这个页面的。笔者更倾向于在需要服务的时候才进行服务对象的创建,在特定场景下也提供服务对象的缓存。

使用 url 传参也是一个不可忽略的问题,对于一些基础数据类型,使用这种方案倒是没有问题,但是对于一些非常规对象就无能为力了,如 UIImage, NSData 等类型。

还有一个问题是 casa 在文章中没有指出的,这个问题在他的 target-action 方案中也存在。下面用一个例子来说明一下。

比如在一个组件 A 中提供了一个服务:

[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
复制代码

然后在一个组件 B 中使用了服务:

[MGJRouter openURL:@"mgj://foo/bar"];
复制代码

从上面示例代码中可以看到,两个不同组件能通信其实是通过一个字符串来定义的。如果服务使用方在写代码时写错了一个字符,那么使用方根本就不可能调起正确的服务,一旦出现这个问题,在开发过程中很难被发现。如果我们对组件多,注册的服务多,那么在使用时就存在很大的沟通问题,提供方和接入方可能会在每个字符串所代表的意义上浪费大量的时间。而这些问题都可以在工程设计上避免的。虽说我们在写代码时要低耦合,但并不代表不要耦合,有时候需要一些耦合来提高代码的健壮性和可维护性。

在 Swift 中可以使用枚举来解决上面的问题,我们可以像下面这样做:

protocol URLPatternCompatible {var URLPattern: String { get }
}enum SomeService {case orderDetailcase others
}enum SomeService: URLPatternCompatible {var URLPattern: String {switch self {case .orderDetail:return "mgj://foo/bar/orderdetail"case .others:return "mgj://foo/bar/others"}}
}// 组件 A (服务提供方)
MGJRouter.register(.orderDetail) { ... }// 组件 B (服务使用方)
MGJRouter.open(.orderDetail)
复制代码

SomeService 的定义可以放到一个专门的组件中,服务提供方和使用方都依赖这个专门的组件。我们这里不仅将字符串放到了一个统一的地方进行维护,而且还将一些在运行期才能发现的问题提前暴露到编译器。这里我们通过耦合来达到提高代码的健壮性和可维护性的目的。

Target-Action

Target-actin 是 casa 在批判蘑菇街的方案时提出的一种方案。它解决了 url-block 方案中内存问题、url 传参问题、没有区分本地调用和远程调用等问题。其核心就是使用了 NSObject 的 - (id)performSelector:(SEL)aSelector withObject:(id)object; 方法。

在本地应用调用中,本地组件A在某处调用 [[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]CTMediator 发起跨组件调用,CTMediator 根据获得的 target 和 action 信息,通过 objective-C 的 runtime 转化生成 target 实例以及对应的 action 选择子,然后最终调用到目标业务提供的逻辑,完成需求。

casa 在文章中也给出了 demo,在具体的项目中,我们可以这样使用:

// CTMediator+SomeAction.h
- (UIViewController *)xx_someAction:(NSDictionary *)params;// CTMediator+SomeAction.m
- (UIViewController *)xx_someAction:(NSDictionary *)params {return [self performTarget:@"A" action:@"someAction" params:params shouldCacheTarget:NO]
}
复制代码

上面是提供给服务调用方的一个简洁的接口。其实就是对 CTMediator 方法的封装。我们一般将 CTMediator 的这个分类放到一个独立的组件中。调用方依赖这个独立的组件就可以了。

在某个组件中调用服务:

// 组件 A 中
UIViewController *vc = [CTMediator sharedInstance] xx_someAction:@{@"key": value}];
复制代码

针对上面服务的定义,服务提供方的定义就必须是下面这样:

// TargetA.h
@interface Target_A : NSObject
- (UIViewController *)someAction:(NSDictionary *)params;
@end// TargetA.m
- (UIViewController *)someAction:(NSDictionary *)params { ... }复制代码

在这整个过程中可以看到,服务的调用方只需要依赖 CTMediator 这个中间件及其分类(定义服务)。服务提供方和调用方没有任何依赖。确实做到了组件解耦。可以肯定的是 target-action 方案确实解决了 url-block 方案的一些问题。但是仔细一看,也是存在一些问题的。

跟 url-block 方案一样,两个不同组件能通信其实仍然是通过一个字符串来定义的。为什么这么说呢,我们可以看一下下面的代码:

// CTMediator+SomeAction.m
- (UIViewController *)xx_someAction:(NSDictionary *)params {return [self performTarget:@"A" action:@"someAction" params:params shouldCacheTarget:NO]
}// TargetA.h
@interface Target_A : NSObject
- (UIViewController *)someAction:(NSDictionary *)params;
@end
复制代码

从上面的代码中可以看到,服务能调起主要是调用了 CTMediator 的

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget; 方法。这里不管是 targetName 还是 action 都是字符串,在实现中 CTMediator 会示例化一个 Target_targetName 类的对象,并且创建一个 Action_actionName 的 selector,所有我们在服务提供的组件中的 Target 以及 Action 是不能随便定义的。Target 必须是以 Target_开头,方法必须以 Action_开头。这种强制要求感觉不是一种工程师的思维。这里想去耦合,却以一种不是很正确的方式造成了隐式的耦合。这也是让我抛弃 CTMediator 转而去开发自己的组件化通信方案的原因之一。

Protocol-Class

Protocol-Class 方案也是常用的组件化通信方式之一。这里把它放到最后,肯定是因为笔者使用的是这种方案咯(笑)。

Protocol-Class 方案就是通过 protocol 定义服务接口,服务提供方通过实现该接口来提供接口定义的服务。具体实现就是把 protocol 和 class 做一个映射,同时在内存中保存一张映射表,使用的时候,就通过 protocol 找到对应的 class 来获取需要的服务。这种方案的优缺点先不说,可以先看一下具体的实践:

示例图:

示例代码:

// TestService.h (定义服务)
@protocol TestService <NSObject>
/// 测试
- (void)service1;@end// 组件 A (服务提供方)
ANT_REGISTER_SERVICE(TestServiceImpl, TestService)
@interface TestServiceImpl() <TestService> @end@implementation TestServiceImpl- (void)service1 {NSLog(@"Service test from Impl");
}@end// 组件 B (服务使用方)
id <TestService> obj = [Ant serviceImplFromProtocol:@protocol(TestService)];
[obj service1];
复制代码

像上面的方案一样,我们会将服务的定义放到独立的组件中。这个组件仅仅只包含了服务的声明。不管是服务提供方还是服务使用方都依赖这个独立的组件,服务提供方还是服务使用方互不依赖。

这里将系统提供的服务定义为协议,通过耦合提高了代码的健壮性和可维护性。这里定义服务的 protocol 对服务提供方做了一个限定:你可以提供哪些服务,同时也给服务使用方做了限定:你可以使用哪些服务。这种设计将系统有哪些服务都交代的清清楚楚,通过服务的 protocol 我们就知道了每个服务的功能,调用需要的参数,返回值等。这里的定义服务的同时也可以作为系统服务的接口文档,这节省了服务提供方和使用方很多的沟通时间,让其能关注业务的开发。这在大型项目,多团队开发中优势尤为明显。

当然 protocol-class 这种方案缺点也很明显,需要在内存中保存 protocol 到 Class 的映射关系。但是我们可以通过将服务分类,让系统注册的 protocol-class 项尽量少一些,不要一个服务定义一个实现。对于一个有100个服务的系统,定义10个服务实现,每个实现提供10个服务,肯定要比100个服务实现占用的内存少很多。这就要求我们在实践过程中能对系统中的服务能做好划分。

总结

以上就是笔者对组件化的一些思考,很多观点可能也不太成熟,如果有什么不合理的地方,也欢迎各位同学提出建议。组件解耦在 iOS 中其实有多种解决方案,各位同学可以根据项目实际情况选择适合自己的方案。

上面代码中的 Ant 是笔者最近开发的一个负责组件生命周期管理和通信的开源工具。因为笔者公司从17年开始就一直使用 Swift 进行开发,原来的工具是用 Swift 编写的,使用了很多 Swift 的特性,在 OC 中使用就显得不伦不类了,就针对 OC 进行了重新设计,于是就有了 Ant 。

转载于:https://juejin.im/post/5c6b576d5188256204690203

组件生命周期管理和通信方案相关推荐

  1. android 组件生命周期,Android组件化开发实践(五):组件生命周期管理

    每个Android应用启动时,都会先创建一个Application.通常在Application里我们会做一些应用初始化的操作,常见的有第三方SDK初始化.在应用组件化之后,组件与壳工程是隔离开来的, ...

  2. android组件化数据生命周期,Android组件化开发实践(五):组件生命周期管理

    每个Android应用启动时,都会先创建一个Application.通常在Application里我们会做一些应用初始化的操作,常见的有第三方SDK初始化.在应用组件化之后,组件与壳工程是隔离开来的, ...

  3. Android 插件化原理解析——Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  4. Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  5. ES--索引生命周期管理

    1,为什么要对elasticsearch进行生命周期管理? ES索引存活数量过多,会给ES集群带来较大压力,不仅严重影响数据录入和数据查询效率,而且导致磁盘.CPU占用比过高,加大节点"驾崩 ...

  6. Tomcat中组件的生命周期管理(三)

    本文主要目的是自定义举例分析Catalina内部生命周期管理的运作方式以及拿内部具体源码来具体分析 假设我们有一台电脑由主机(我们用cpu表示)和显示器构成,那么我们要运用上篇文章学到的内容,来管理整 ...

  7. Elasticsearch 索引生命周期管理方案

    Elasticsearch索引生命周期管理方案 文章目录 Elasticsearch索引生命周期管理方案 1.生命周期 1.1 阶段介绍 2.模拟过程(基础) 2.1 创建索引生命周期策略 2.2 创 ...

  8. 微信小程序自定义标签组件component封装、组件生命周期,组件通信

    微信小程序自定义标签组件component封装.组件生命周期,组件通信 本文来说下小程序的自定义标签组件封装. 相比于vue,react的非路由组件,微信小程序的component组件要麻烦些,而且生 ...

  9. 钢铁电商行业方案:钢铁工业产品全生命周期管理解决方案

    近几年来物联网.云计算.大数据.人工智能等技术得到了快速的发展,新型的的产业变革.技术革命也得到了快速的兴起,各行业更注重产品质量.产品服务.需求个性化需求响应速度等.钢铁制造业尤为明显,钢铁行业通过 ...

最新文章

  1. 编解码器架构中的桥(bridge)指什么
  2. 笔记本电脑设置触摸板双指滑动
  3. JDeodorant 的使用
  4. 小米8 android9手势,这么全面的小米手机操作手势你一定没见过
  5. 办公室小野与爆米花视频身亡女孩家属和解:补偿金额保密
  6. 集宁哪有计算机培训班,集宁区有这么一个空间,叫共享自习室
  7. 实验一 winrunner的安装使用
  8. AD9361开发:接收与发送滤波器配置
  9. 微信小程序图片布满整个窗口
  10. BAT智能硬件布局 争搭平台卡位各异
  11. SAP数据接口技术类型
  12. 设计一个密码登录程序。要求: 设定用户名为lili,密码是123321。若用户名正确,密码也正确,则显示:“lili,欢迎您”
  13. Java 匿名类热更新失败的原因
  14. python中endswith函数什么意思_Python中endswith()函数的基本使用
  15. 数据库常用操作,会持续更新
  16. 「ZBrush」学习ZB出来可以从事什么工作
  17. a-select设置默认值
  18. 今日错题(10.6)
  19. 【数据结构】 八大排序实现简析+复杂度及稳定性分析
  20. 利用nid更改数据库的名称(OCM---OCP)

热门文章

  1. 《孙鑫老师谈如何学好编程》摘要
  2. 在Access和 SQL Server中通配符的应用方法
  3. 用WMI获取远程机器操作系统的详细信息
  4. UA SIE545 优化理论基础 用Farkas定理证明Farkas类的结论
  5. UA MATH566 统计理论1 充分统计量例题答案3
  6. win32 断言函数和转换到COFF期间失败错误
  7. 记一次内核模块查看 - 初步通过文件厂商判断有无可疑内核模块
  8. Windows控制台程序处理消息编程实例二则
  9. react生命周期-新增与替换
  10. GIS输出PDF为什么标注有问题