组件化概述
在一个App长时间的发展的过程中,必然存在着一下的问题:
- 项目臃肿不堪:除了必要的,如AFN,SD等三方库外,其代码都存在于主工程中,每一次都要编译整个工程的代码,且组员之间的修改及其容易出现冲突,效率极低
- 团队规模变化:由于公司的人员之间的变动,负责的代码相互交接到处职责不清,代码冲突混乱
- 业务增长迭代:先如今公司业务都增长迅速,敏捷开发盛行,所以就需要一个灵活多变的结构来应对不同的需求
- 代码混编:当出现新技术时,一般都是需要来尝试的,比武Swift, RN,Flutter等,当这些代码和现有代码结合,就会产生相当多意想不到的问题,所以必须要做到良好的代码隔离
当出现以上情况时,说明你的代码就急需要使用组件化来规避这些问题了。使用组件化,主要有一下好处
- 加快编译速度,不用再编译组件 / 模块外没有被依赖到的代码;
- 便于将每个模块指定给不同负责人进行管理;
- 降低合并难度,减小冲突和出错概率,提高业务开发效率;
- 将 其他代码 和 OC 代码进行分离,不同语言开发顺畅,可替换性强;
- 可为模块编写单元测试,提高工作效率,同时方便测试人员进行有针对性的测试。
组件化的模块拆分原则
当我们梳理目前项目的代码时,需要按照一下3个原则来进行,这样能够对业务和架构进行更好的拆分:
高层依赖底层,下层不能对上层有依赖的关系
这点是基本的设计原则,可以通过依赖倒置来设计。同层级的模块不依赖或者尽量少依赖
这点同时也是基本的设计原则,可以通过控制反转来设计,典型的就是使用观察者模式来实现同一个层级模块的解耦。最小知识原则和自完备性
一个独立的模块尽量减少对其他低层模块的依赖,比如一个模块只是依赖低层模块的某个类的方法,不妨把这个方法拷贝到此模块中,如此一来这个模块就具有了更好的自完备性。
组件化的模块分层结构
根据以上组件化拆分的原则,拆分后项目的主要结构如下:
拆分后的主要实现的目标如下:
- 基础组件独立:保证所有的底层功能组件从主工程抽出,独立与主工程之外,便于复用、业务模块的调用
- 业务模块划分与拆解:将业务按对应用途进行划分和拆解,想办法切断各业务之间的强依赖;
- 所有组件 / 模块独立编译:所有功能组件和业务模块能够独立于主工程进行编译,有各自的 Demo 工程;
- CocoaPods 发布:在内网 GitLab 进行发布,并且之后对每个模块用 GitFlow 工作流进行管理和后续发布工作。【关于使用CocoaPods的拆分,可以参考此博客:iOS 组件化-使用cocoapods集成实战演练】
组件化解耦的几种方式
目前市面上主要存在着3种组件化解耦方案,分别是URL解耦
,Target-Action中间层
,register-protocol注册法
。
通过URL的统跳解耦
URL解耦的概述
统跳路由是页面解耦的最常见方式,大量应用于前端页面。通过把一个 URL 与一个页面绑定,需要时通过 URL 可以方便的打开相应页面。
它通过URL来请求资源。不管是H5,RN,Weex,iOS界面或者组件请求资源的方式就都统一了。URL里面也会带上参数,这样调用什么界面或者组件都可以。所以这种方式是最容易,也是最先可以想到的。
优点:
服务器可以动态的控制页面跳转,可以统一处理页面出问题之后的错误处理,可以统一三端,iOS,Android,H5 / RN / Flutter 的请求方式。
缺点:
- URL的map规则是需要注册的
- URL链接里面关于组件和页面的名字都是硬编码,参数也都是硬编码。而且每个URL参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担
- URL短连接散落在整个App四处,维护起来有点麻烦
- 对于传递NSObject的参数,URL是不够友好的,它最多是传递一个字典
URL的统跳解耦的简单实现
- 注册统跳路由,建立页面与统跳的一一对应的管理,维护Map。
注册路由的地方,根据项目实际情况来定夺,可以放在load
方法里,或者在启动时,防止在调用时,无注册的情况
[[Router defaultRouter] registerWithPName:@"hallfollow" handler:[HallRouter class]];
- 建立对应
Router
的处理类,用来处理相对应的通跳
对于其参数的处理,可以直接拼接在url后面,或者增加一个extraData
的字典用来进行传递。
+ (BOOL)openRequest:(IKRouteRequest *)request application:(UIApplication *)application annotation:(id)annotation target:(UIViewController *)target {
if ([request.pName isEqualToString:@"hallfollow"]) {
NSDictionary *options = request.options;
NSString *tab = options[@"tab"];
NSDictionary *dict =[[NSDictionary alloc] initWithObjectsAndKeys:tab,@"tab", nil];
id navigationCenter = [[ServiceManager sharedInstance] clsServiceForProtocol:@protocol(NavigationCenterProtocol)];
[navigationCenter popToRootFromTarget:target completion:^(UIViewController *currentVC) {
}];
return YES;
}
}
- 在其
Router
的内部,通过对URL的解析和封装,然后进行对应的分发
通过Target-Action方案
众所周知,如果要解决两个模块之间的耦合关系,那么在其中间提取出一个中间层用来处理其事务是比较合理的手段。其中中间层的核心逻辑就是如下面代码所示,通过字符串获取到类,并通过performSelector
来调用相对应的函数。
Class manager = NSClassFromString(@"GoodsManager");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
//code to handle the list
但是只通过上面是无法实现解耦的,这种方式存在大量的 hardcode 字符串。无法触发代码自动补全,容易出现拼写错误,而且这类错误只能在运行时触发相关方法后才能发现。无论是开发效率还是开发质量都有较大的影响。
所以通过上面的分析,实现一个方法最主要就是
-
Target
:调用方 -
Action
:调用方法 -
Param
:调用参数
那么我们在中间层就是要实现这3方面,具体的方案可以参考CTMediator的实现。其主要思想是利用了Target-Action
简单粗暴的思想,利用Runtime
解决解耦的问题。
如果单纯增加中间层,那么就会如上图一样,中间层会导入各模块,导致所以的耦合都在中间层,那么中间层就会变得无比庞大。
由于直接使用performSelector
,对于参数传递并不友好,并且有太多的硬编码和崩溃的隐患,所以需要对其进行改造。主要逻辑如下:
- 获取到对应targetName,并拼接前缀
Target_
获取到对应的实现类 - 获取到对应的actionName,并拼接前缀
Action_
获取到对应的方法 - 通过
NSInvocation
对消息进行转发并做基础类型的容错处理 - 最终调用
performSelector:
方法
- (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) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[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) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
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];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
- (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) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms 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
}
中间层实现了消息转发后,还是不能做到有效的解耦。我们需要一个Target_类名
的类来暴露外部调用的接口,和一个对应模块的分类,用于上层封装CTMediator
的方法,形成隔离,减少对库的依赖。
其分类内部分实现也就是对接口的封装,也是调用的CTMediator
的方法,这些模块的分类可以放在一个Pod库中,这样不同模块依赖该库,就可以直接调用方法了。
- (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];
}
有个暴露的分类Pod库,我们还要在自己的模块中实现其对应暴露接口的实现,因为此时和模块高度绑定,所以可以和对应模块放在一起,不必暴露。形成最终的实现,这样一套消息流程就结束了。
- (id)Action_showAlert:(NSDictionary *)params
{
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
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;
}
最终解耦实现了下图所表示的结构
优缺点
Target-Action方案的优点:
- 充分的利用Runtime的特性,无需注册这一步。
- Target-Action方案只有存在组件依赖Mediator这一层依赖关系。
- 在Mediator中维护针对Mediator的Category,每个category对应一个Target,Categroy中的方法对应Action场景。
- Target-Action方案也统一了所有组件间调用入口。
- Target-Action方案也能有一定的安全保证,它对url中进行Native前缀进行验证。
Target-Action方案的缺点:
- Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这就造成了一部分的硬编码。
通过注册协议方案
如果仅仅通过Target-Action
的方案来解决模块间的解耦问题,还是有部分的硬编码,而且对于一对多的事件分发处理还是不到位,所以可以采用注册协议方案来解决,比较典型有BeeHive框架。
根据框架图我们可以知道,其解耦的主要方式是将各模块暴露的接口和App全局的时间,沉淀到底层去,并通过Protocol
的方式进行分发。其核心即注册-分发
的模式
模块的注册
由于我们要使用Protocol
的方式,来进行消息的分发,那么必须要有Protocol
的调用方和实现方。且Protocol
要与实现方一一对应,所以必须要有注册的步骤,不然无法实现分发。
模块的注册主要分为静态注册和动态注册两种方式。都需要维护一个全局的Map
来进行查找
静态注册
对于静态注册的方式比较简单,总结来说基本就2种:
-
plist
的形式维护模块与协议的对应
- (void)loadLocalModules
{
NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
return;
}
NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
NSArray *modulesArray = [moduleList objectForKey:kModuleArrayKey];
NSMutableDictionary *moduleInfoByClass = @{}.mutableCopy;
[self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
}];
[modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
[self.BHModuleInfos addObject:obj];
}
}];
}
- 在合适的时机让模块调用相对应的注册方法
- (void)registerService:(Protocol *)service implClass:(Class)implClass
{
NSParameterAssert(service != nil);
NSParameterAssert(implClass != nil);
if (![implClass conformsToProtocol:service]) {
if (self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ module does not comply with %@ protocol", NSStringFromClass(implClass), NSStringFromProtocol(service)] userInfo:nil];
}
return;
}
if ([self checkValidService:service]) {
if (self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol has been registed", NSStringFromProtocol(service)] userInfo:nil];
}
return;
}
NSString *key = NSStringFromProtocol(service);
NSString *value = NSStringFromClass(implClass);
if (key.length > 0 && value.length > 0) {
[self.lock lock];
[self.allServicesDict addEntriesFromDictionary:@{key:value}];
[self.lock unlock];
}
}
动态注册
对于动态注册,即没有对应的注册方法和配置文件,自动注册。那么是怎么实现的呢?
动态注册的实现主要是利用注解和宏定义。因为宏定义可以在编译时就写入了Mach-O
文件中的__DATA
段中了,只需要在dyld
链接镜像文件时,把数据取出来,然后存入对应的字典,数组中即完成了注册流程
如下面代码,当模块注册时,宏定义中会在load
方法中增加一个注册方法,将相关模块注册金管理类,方便时间分发
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
当模块之间调用时,必须指定协议和实现类的绑定,从而可以在别的模块调用,其绑定的动态注册也是一个宏定义注解。我们可以看到宏定义就是将协议名和实现类名存入了__DATA
段
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
---------------------------------------------------------------------------------------------------
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
那么他是怎么从Mach-O
文件中读取到内存的呢,我们知道在App的启动过程中,是通过dyld
来加载相关的镜像文件的,那么只需要在启动链接的过程中,把相关数据加载到内存中就可以了
我们可以再实现中发现一个全局的静态函数initProphet()
,其调用实在dyld
链接之后调用,会收到一个全局的dyld_callback
,其中就有Mach-O中存取的数据。
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
通过调用BHReadConfiguration
方法,我们可以在__DATA
中取出我们需要的数据,并返回一个数组
NSArray* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;
BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
return configs;
}
通过对数组的遍历,可以获得相关的模块和协议,从而调用注册方法,完成注册
NSArray* BHReadConfiguration(char *sectionName,const struct mach_header *mhp);
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
//register services
NSArray *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
for (NSString *map in services) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
全局事件的分发
了解了注册的流程,对于事件的分发就比较简单了。对于全局事件,即App的启动,闪屏,登录成功,前后台等事件。当一个模块完成了注册,想要获取到这些事件时,只需要遵循相关协议
,就可以再其回调中得到相关方法的调用。
对于全局事件的收集,可以看下图。在didLanch
等方法中,我们需要初始化我们的管理模块,并保存相关上下文,用于模块的使用,并且在静态注册时,指定相关的资源文件。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
[BeeHive shareInstance].enableException = YES;
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:@"BeeHive::super start launch"];
[super application:application didFinishLaunchingWithOptions:launchOptions];
id homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
if ([homeVc isKindOfClass:[UIViewController class]]) {
UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:(UIViewController*)homeVc];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.rootViewController = navCtrl;
[self.window makeKeyAndVisible];
}
return YES;
}
对于一些需要打点的事件,我们也可以自定义AppDelegate
文件,在BHAppDelegate
中实现我们的一些基本操作,比如埋点等操作
@interface TestAppDelegate : BHAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];
[[BHModuleManager sharedManager] triggerEvent:BHMInitEvent];
dispatch_async(dispatch_get_main_queue(), ^{
[[BHModuleManager sharedManager] triggerEvent:BHMSplashEvent];
});
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
if ([UIDevice currentDevice].systemVersion.floatValue >= 10.0f) {
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
}
#endif
#ifdef DEBUG
[[BHTimeProfiler sharedTimeProfiler] saveTimeProfileDataIntoFile:@"BeeHiveTimeProfiler"];
#endif
return YES;
}
对于事件的分发,我们在对应的注册的事件中,通过字符串的转换,通过performSelector:
调用相关协议方法即可。
- (void)handleModuleEvent:(NSInteger)eventType
forTarget:(id)target
withSeletorStr:(NSString *)selectorStr
andCustomParam:(NSDictionary *)customParam
{
BHContext *context = [BHContext shareInstance].copy;
context.customParam = customParam;
context.customEvent = eventType;
if (!selectorStr.length) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
}
SEL seletor = NSSelectorFromString(selectorStr);
if (!seletor) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
seletor = NSSelectorFromString(selectorStr);
}
NSArray> *moduleInstances;
if (target) {
moduleInstances = @[target];
} else {
moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
}
[moduleInstances enumerateObjectsUsingBlock:^(id moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
对于事件的接受,遵循协议,然后实现需要的方法即可。
模块事件的调用
对于同等级模块之间事件的调用如果来解耦,也是类似的方法,但是他不像系统事件一样,有系统调用的代理回调。
这时候应该通过注册管理的方式,将协议和实现方通过一一对应的方式进行注册管理。调用方通过调用协议中的相关方法来转发到实现方,从而完成解耦。
首先要提供该模块暴露的Protocol
,用来方便外部进行调用,这也是该模块的"接口"
@protocol HomeServiceProtocol
-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName;
@end
然后编写该协议具体的实现方并注册进管理模块
@BeeHiveService(HomeServiceProtocol,BHViewController)
@interface BHViewController ()
@property(nonatomic,strong) NSMutableArray *registerViewControllers;
@end
-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName
{
vc.tabBarItem.image = [UIImage imageNamed:[NSString stringWithFormat:@"Home.bundle/%@", iconName]];
vc.tabBarItem.title = title;
[self.registerViewControllers addObject:vc];
self.viewControllers = self.registerViewControllers;
}
最后再需要调用的地方,通过管理类取出协议,调用方法即可
id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
[homeVc registerViewController:self title:@"" iconName:@""];
优缺点
这种方案ModuleEntry是同时需要依赖ModuleManager和组件里面的页面或者组件两者的。当然ModuleEntry也是会依赖ModuleEntryProtocol的,但是这个依赖是可以去掉的,比如用Runtime的方法NSProtocolFromString,加上硬编码是可以去掉对Protocol的依赖的。但是考虑到硬编码的方式对出现bug,后期维护都是不友好的,所以对Protocol的依赖还是不要去除。
最后一个缺点是组件方法的调用是分散在各处的,没有统一的入口,也就没法做组件不存在时或者出现错误时的统一处理。
参考
CTMediator
蜂鸟商家版 iOS 组件化
iOS 组件化 —— 路由设计思路分析
浅谈 iOS 组件化开发