iOS架构优化 - 组件化

组件化概述

在一个App长时间的发展的过程中,必然存在着一下的问题:

  1. 项目臃肿不堪:除了必要的,如AFN,SD等三方库外,其代码都存在于主工程中,每一次都要编译整个工程的代码,且组员之间的修改及其容易出现冲突,效率极低
  2. 团队规模变化:由于公司的人员之间的变动,负责的代码相互交接到处职责不清,代码冲突混乱
  3. 业务增长迭代:先如今公司业务都增长迅速,敏捷开发盛行,所以就需要一个灵活多变的结构来应对不同的需求
  4. 代码混编:当出现新技术时,一般都是需要来尝试的,比武Swift, RN,Flutter等,当这些代码和现有代码结合,就会产生相当多意想不到的问题,所以必须要做到良好的代码隔离

当出现以上情况时,说明你的代码就急需要使用组件化来规避这些问题了。使用组件化,主要有一下好处

  • 加快编译速度,不用再编译组件 / 模块外没有被依赖到的代码;
  • 便于将每个模块指定给不同负责人进行管理;
  • 降低合并难度,减小冲突和出错概率,提高业务开发效率;
  • 将 其他代码 和 OC 代码进行分离,不同语言开发顺畅,可替换性强;
  • 可为模块编写单元测试,提高工作效率,同时方便测试人员进行有针对性的测试。

组件化的模块拆分原则

当我们梳理目前项目的代码时,需要按照一下3个原则来进行,这样能够对业务和架构进行更好的拆分:

  • 高层依赖底层,下层不能对上层有依赖的关系
    这点是基本的设计原则,可以通过依赖倒置来设计。

  • 同层级的模块不依赖或者尽量少依赖
    这点同时也是基本的设计原则,可以通过控制反转来设计,典型的就是使用观察者模式来实现同一个层级模块的解耦。

  • 最小知识原则和自完备性
    一个独立的模块尽量减少对其他低层模块的依赖,比如一个模块只是依赖低层模块的某个类的方法,不妨把这个方法拷贝到此模块中,如此一来这个模块就具有了更好的自完备性。

组件化的模块分层结构

根据以上组件化拆分的原则,拆分后项目的主要结构如下:


结构示意图

拆分后的主要实现的目标如下:

  1. 基础组件独立:保证所有的底层功能组件从主工程抽出,独立与主工程之外,便于复用、业务模块的调用
  2. 业务模块划分与拆解:将业务按对应用途进行划分和拆解,想办法切断各业务之间的强依赖;
  3. 所有组件 / 模块独立编译:所有功能组件和业务模块能够独立于主工程进行编译,有各自的 Demo 工程;
  4. 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 的请求方式。

缺点

  1. URL的map规则是需要注册的
  2. URL链接里面关于组件和页面的名字都是硬编码,参数也都是硬编码。而且每个URL参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担
  3. URL短连接散落在整个App四处,维护起来有点麻烦
  4. 对于传递NSObject的参数,URL是不够友好的,它最多是传递一个字典

URL的统跳解耦的简单实现

  1. 注册统跳路由,建立页面与统跳的一一对应的管理,维护Map。

注册路由的地方,根据项目实际情况来定夺,可以放在load方法里,或者在启动时,防止在调用时,无注册的情况

[[Router defaultRouter] registerWithPName:@"hallfollow" handler:[HallRouter class]];
  1. 建立对应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<NavigationCenterProtocol> navigationCenter = [[ServiceManager sharedInstance] clsServiceForProtocol:@protocol(NavigationCenterProtocol)];
        [navigationCenter popToRootFromTarget:target completion:^(UIViewController *currentVC) {
        }];
        return YES;
    }
}
  1. 在其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,对于参数传递并不友好,并且有太多的硬编码和崩溃的隐患,所以需要对其进行改造。主要逻辑如下:

  1. 获取到对应targetName,并拼接前缀Target_获取到对应的实现类
  2. 获取到对应的actionName,并拼接前缀Action_获取到对应的方法
  3. 通过NSInvocation对消息进行转发并做基础类型的容错处理
  4. 最终调用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:&params 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:&params 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:&params 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:&params 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:&params 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框架。

BeeHive框架图

根据框架图我们可以知道,其解耦的主要方式是将各模块暴露的接口和App全局的时间,沉淀到底层去,并通过Protocol的方式进行分发。其核心即注册-分发的模式

模块的注册

由于我们要使用Protocol的方式,来进行消息的分发,那么必须要有Protocol的调用方和实现方。且Protocol要与实现方一一对应,所以必须要有注册的步骤,不然无法实现分发。
模块的注册主要分为静态注册和动态注册两种方式。都需要维护一个全局的Map来进行查找

静态注册

对于静态注册的方式比较简单,总结来说基本就2种:

  1. 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<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
    NSMutableDictionary<NSString *, NSNumber *> *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];
        }
    }];
}

  1. 在合适的时机让模块调用相对应的注册方法
- (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<NSString *>* 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<NSString *>* 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<NSString *> *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<HomeServiceProtocol> 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 <UIApplicationDelegate>
- (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<BHModuleProtocol>)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<id<BHModuleProtocol>> *moduleInstances;
    if (target) {
        moduleInstances = @[target];
    } else {
        moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
    }
    [moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> 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 <NSObject, BHServiceProtocol>


-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName;

@end

然后编写该协议具体的实现方并注册进管理模块

@BeeHiveService(HomeServiceProtocol,BHViewController)
@interface BHViewController ()<HomeServiceProtocol>

@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 组件化开发

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