内容简介:随着移动互联网的快速发展,项目的迭代速度越来越快,需求改变越来越频繁,传统开发方式的工程所面临的一些,如代码耦合严重、维护效率低、开发不够敏捷等问题就凸现了出来。于是越来越多的公司开始推行"组件化",通过对原有业务或新业务进行组件(或模块)拆分来提高并行开发效率。在笔者面试过程中发现,很多同学口中的"组件化"也只是把代码分库,然后在主项目中使用 CocoaPods 把各个子库聚合起来。对于怎样合理地对组件分层、如何管理组件(主要包括组件的生命周期管理和组件的通信管理),如何管理不同版本的依赖,以及是否有整套
随着移动互联网的快速发展,项目的迭代速度越来越快,需求改变越来越频繁,传统开发方式的工程所面临的一些,如代码耦合严重、维护效率低、开发不够敏捷等问题就凸现了出来。于是越来越多的公司开始推行"组件化",通过对原有业务或新业务进行组件(或模块)拆分来提高并行开发效率。
在笔者面试过程中发现,很多同学口中的"组件化"也只是把代码分库,然后在主项目中使用 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 orderDetail case 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个服务实现占用的内存少很多。这就要求我们在实践过程中能对系统中的服务能做好划分。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 探讨父组件和兄弟组件的生命周期
- React 组件生命周期详解
- 【Tomcat学习笔记】组件声明周期
- 【Tomcat学习笔记】组件声明周期
- Vue 组件生命周期钩子函数
- 生命周期组件 Lifecycle 源码解析(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。