031*:组件化(url-block 、target-action、protocol-class)

问题

简单来说:一个模块对应一个 Mediator的分类 ,一个分类对应一个target,一个target可以包含多个action,每个action都是进行界面跳转。

1、url-block 路由:路由

2、target-action:runtime+分类。

3、protocol-class匹配:通过协议和类对应。

目录

1:组件化

2:组件化方案 

预备

 

正文

一:组件化

组件化其实就是将模块单独抽离、分层,并指定模块间的通讯方式,从而实现解耦的一种方式,主要运用与团队开发

1:为什么需要组件化?

主要有以下四个原因

1、模块间解耦

2、模块重用

3、提高团队协作开发效率

4、单元测试

2:什么情况不需要组件化

  • 项⽬较⼩,模块间交互简单,耦合少
  • 模块没有被多个外部模块引⽤,只是⼀个单独的⼩模块
  • 模块不需要重⽤,代码也很少被修改
  • 团队规模很⼩
  • 只能上层对下层依赖
  • 项⽬公共代码资源 下沉
  • 横向的依赖 最好下沉
组件开发完成,会上传Spec到这里,包含着所有的版本信息,然后通过这么文件找到地址信息。
我们在本地都有个隐藏文件,可以看到本地的Specs,例如afn的Spec信息/Users/xxx/.cocoapods/repos/trunk/Specs/a/7/5/AFNetworking

3:组件化分层

组件化之前我们需要继续解耦,模块分级.
首先创建基础模块,在项目目录下,终端
cd  [文件夹目录]
pod lib create JMacroAndCategoryModule

通过这个我们可以创建一个基础模板https://github.com/CocoaPods/pod-template.git在本地。
生成的信息在 JMacroAndCategoryModule.podspec

#
# Be sure to run `pod lib lint JMacroAndCategoryModule.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
  s.name             = 'JMacroAndCategoryModule'
  #发版版本号,每更新一次代码就改变一次版本号
  s.version          = '0.1.0'
  #一个简单的总结,随便写
  s.summary          = 'A short description of JMacroAndCategoryModule.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?
#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!

#描述,随便写 但是要比 s.summary 长度长
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  #你的 git 仓库首页的网页 url,注意并不是 https/ssh这种代码仓库地址
  s.homepage         = 'https://github.com/jscmkt/JMacroAndCategoryModule'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  #直接写 MIT
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  #你是谁
  s.author           = { 'jscmkt' => 'xxx@qq.com' }
  #这里就是你 git 仓库的 https/ssh 地址了
  s.source           = { :git => 'https://github.com/jscmkt/JMacroAndCategoryModule.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '8.0'
  
  #这里的文件夹下的内容就是这个 pods 被pod install 的时候会被下载下来的文件,不在这个文件夹,将不会被引用
  # Classes 目录和.podspec 目录是平级的。
  #你可以随便指定文件夹名称,只要这个文件夹是真实存在的
  #Classes/**/*.{h,m},表示 Classes 文件夹及其文件夹下的所有.h,.m 文件。
  s.source_files = 'JMacroAndCategoryModule/Classes/**/*'
  
  #资源文件地址,下面的所有.png资源都被打包成 s.name.bundle
  s.resource = ['Images/*.png','Sounds/*']
  
  #资源文件地址,和 resource 的区别是,这个属性可以指定 bundle 的名字,下面的所有.png文件都会被打包成 ABC_section.bundle
  # s.resource_bundles = {
  #   'JMacroAndCategoryModule' => ['JMacroAndCategoryModule/Assets/*.png']
  # }
  #指定公有头文件,如果没有写,那么所有 pod 中的头文件都默认公有,可以被 import。如果指定了某些头文件,那么只有这些被指定的头文件才可以被 import。
  # s.public_header_files = 'Pod/Classes/**/*.h'
  # s.frameworks = 'UIKit', 'MapKit'
  
  #这个 pods 还依赖于其他哪些 pods
  # s.dependency 'AFNetworking', '~> 2.3'
end

我们可以根据官网信息来进行配置

4:头文件处理

我们把基础模块的代码放在Classes位置
进入Example终端执行pod install,更新依赖。
我们在通过pod lib create JCommonUIModule创建一个JCommonUIModule.
build一下,发现会有三方库的报错,我们在JCommonUIModule.podspec添加

  s.dependency 'AFNetworking'
  s.dependency 'Masonry'
  
  s.prefix_header_contents = '#import "Masonry.h"','#import "UIKit+AFNetworking.h"''
  

这里我们就解决了三方依赖。现在我们需要解决模块间的依赖。
我们需要给他一个查询路径,在JCommonUIModulePodfile中添加 ,解决本地依赖

  pod 'JMacroAndCategoryModule', :path => '../../JMacroAndCategoryModule'

5:资源文件处理

我们通过self.imageView.image = [UIImage imageNamed:@"share_wechat"发现无法加载资源文件。

首先我们需要在将资源文件放在Assets文件夹中,然后在podspec中添加资源目录。
   s.resource_bundles = {
     'LGModuleTest' => ['LGModuleTest/Assets/*']
   }

执行pod install
在项目中通过资源包加载。

    NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JModuleTest.bundle"];
    NSBundle *resoure_bundle = [NSBundle bundleWithPath:bundlePath];
    
    self.imageView.image = [UIImage imageNamed:@"share_wechat" inBundle:resoure_bundle compatibleWithTraitCollection:nil];

xib和json也是这样处理

*****************xib********************
  NSString *bundlePath = [NSBundle bundleForClass:[self class]].resourcePath;
    
  [self.tableView registerNib:[UINib nibWithNibName:className bundle:[NSBundle bundleWithPath:bundlePath]] forCellReuseIdentifier:className];

*****************json********************
    NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JHomeModule.bundle"];
    NSString *path = [[NSBundle bundleWithPath:bundlePath] pathForResource:[NSString stringWithFormat:@"Home_TableView_Response_%@", channelId] ofType:@"json"];
    

6:中间层依赖

为了解决模块和模块间通讯,我们使用Mediator
VC1 想PUSH VC2 需要有响应+参数+回调
VC2需要有响应方法
通过runtime我们知道方法都有消息接收者和消息主体。
这个时候就需要暴露接口:target-action,VC2需要来维护他。
这个时候我们就需要MediatorMediator来帮助找target-action。
- (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 safeFetchCachedTarget: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 safeSetCachedTarget:target key:targetClassString];
    }

    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];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}

这就是Mediator主要方法。

- (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;
    }
.......

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

简单的返回值类型直接处理,其他的交给target performSelector处理.

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}

最后直接返回需要跳转的页面。
使用方法

//需要调用的地方
   UIViewController *vc = [[CTMediator sharedInstance] CTMediator_naviagetPlayerVC:((HomeTemplateData *)data).videoId title:((HomeTemplateData *)data).title];
    [[UIViewController currentViewController].navigationController pushViewController:vc animated:YES];

新建CTMediator分类

///处理跳转方法

#import "CTMediator+LGPlayerModuleAction.h"

NSString * const kCTMediatorTargetLGPlayerModule = @"LGPlayerModule";
NSString * const kCTMediatorActionNativeLGPlayerViewController = @"nativeLGPlayerViewController";

@implementation CTMediator (LGPlayerModuleAction)

- (UIViewController *)CTMediator_naviagetPlayerVC:(NSString *)videoID title:(NSString *)videoTitle{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetLGPlayerModule
                                                    action:kCTMediatorActionNativeLGPlayerViewController
                                                    params:@{@"videoID":videoID,
                                                             @"videoTitle":videoTitle
                                                    }
                                         shouldCacheTarget:NO
                                        ];
    
    return viewController;
}

@end

整合项目

新建个工程,导入写好的模块

    pod 'JHomeModuleCategory', :path => '../JHomeModuleCategory'
    pod 'JMacroAndCategoryModule', :path => '../JMacroAndCategoryModule'
    pod 'JCommonUIModule', :path => '../JCommonUIModule'
    pod 'JPlayerModuleCategory', :path => '../JPlayerModuleCategory'
    pod 'JHomeModule', :path => '../JHomeModule'
    pod 'JPlayerModule', :path => '../JPlayerModule'
    pod 'JDLNAModule', :path => '../JDLNAModule'

调试一下就可以了。

组件化demo

二:组件化方案

目前主流的主要有以下三种方式:

  • 1、url-block 路由

  • 2、target-action

  • 3、protocol-class匹配

1、url-block

目前iOS上大部分路由工具都是基于URL匹配的,或者是根据命名约定,用runtime方法进行动态调用

这些动态化的方案的优点是实现简单,缺点是需要维护字符串表,或者依赖于命名约定,无法在编译时暴露出所有问题,需要在运行时才能发现错误。

URL路由方式主要是以蘑菇街为代表的的MGJRouter

1:其实现思路是:

  • App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册

  • 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。 

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

下图是url-block的架构图:

 
3:注册调用
注册:
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
    NSNumber *id = routerParameters[@"id"];
    // create view controller with id
    // push view controller
}];

调用:

[MGJRouter openURL:@"mgj://detail?id=404"]

蘑菇街为了统一iOSAndroid的平台差异性,专门用后台来管理url,然后针对不同的平台,生成不同类型的文件,来方便使用。

使用url-block的方案的确可以组建间的解耦,但是还是存在其它明显的问题,比如:

  1. 需要在内存中维护url-block的表,组件多了可能会有内存问题
  2. url的参数传递受到限制,只能传递常规的字符串参数,无法传递非常规参数,如UIImageNSData等类型
  3. 没有区分本地调用和远程调用的情况,尤其是远程调用,会因为url参数受限,导致一些功能受限
  4. 组件本身依赖了中间件,且分散注册使的耦合较多

URL 路由的优点

  • 极高的动态性,适合经常开展运营活动的app,例如电商

  • 方便地统一管理多平台的路由规则

  • 易于适配URL Scheme

URl 路由的缺点

  • 传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都是通过字符串转换而来

  • 只适用于界面模块,不适用于通用模块

  • 参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。

  • 不支持storyboard

  • 依赖于字符串硬编码,难以管理,蘑菇街做了个后台专门管理。

  • 无法保证所使用的的模块一定存在

  • 解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大

除了CTMediator,还有以下这些三方框架

2、target-action

这个方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector + NSInvocation动态调用方法

其主要的代表框架是casatwy的CTMediator

其实现思路是:

  • 1、利用分类为路由添加新接口,在接口中通过字符串获取对应的类

  • 2、通过runtime创建实例,动态调用实例的方法

其模块间的引用关系如下图所示

优点

  • 利用 分类可以明确声明接口,进行编译检查

  • 实现方式轻量

缺点

  • 需要在mediator和 target中重新添加每一个接口,模块化时代码较为繁琐

  • 在 category中仍然引入了字符串硬编码,内部使用字典传参,一定程度上也存在和 URL 路由相同的问题

  • 无法保证使用的模块一定存在,target在修改后,使用者只能在运行时才能发现错误

  • 可能会创建过多的 target 类

CTMediator源码分析

  • 通过分类中调用的performTarget来到CTMediator中的具体实现,即performTarget:action:params:shouldCacheTarget:,主要是通过传入的name,找到对应的target和 action
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target 生成target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        //swift中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        //OC中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //缓存中查找target
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    //缓存中没有target
    if (target == nil) {
        //通过字符串获取对应的类
        Class targetClass = NSClassFromString(targetClassString);
        //创建实例
        target = [[targetClass alloc] init];
    }

    // generate action 生成action方法名称
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    //通过方法名字符串获取对应的sel
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    //是否需要缓存
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }
    //是否响应sel
    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];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}
  • 进入safePerformAction:target:params:实现,主要是通过invocation进行参数传递+消息转发
- (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];
    //void类型
    if (strcmp(retType, @encode(void)) == 0) {
        ...
    }
    //...省略其他类型的判断
}

3、protocol-class

protocol class

protocol匹配的实现思路是:

  • 1、将protocol和对应的进行字典匹配

  • 2、通过用protocol获取class,在动态创建实例

protocol比较典型的三方框架就是阿里的BeeHiveBeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能基础功能模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务。

BeeHive 核心思想

  • 1、各个模块间调用从直接调用对应模块,变成调用Service的形式,避免了直接依赖。

  • 2、App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在。

示例如下
//******** 1、注册
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

//******** 2、使用
#import "BHService.h"

id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

优点

  • 1、利用接口调用,实现了参数传递时的类型安全

  • 2、直接使用模块的protocol接口,无需再重复封装

缺点

  • 1、用框架来创建所有对象,创建方式不同,即不支持外部传入参数

  • 2、用OC runtime创建对象,不支持swift

  • 3、只做了protocol和 class的匹配,不支持更复杂的创建方式 和依赖注入

  • 4、无法保证所使用的protocol 一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块

除了BeeHive,还有Swinject

BeeHive 模块注册

BeeHive主要是通过BHModuleManager来管理各个模块的。BHModuleManager中只会管理已经被注册过的模块。

BeeHive提供了三种不同的调用形式,静态plist动态注册annotation。Module、Service之间没有关联,每个业务模块可以单独实现Module或者Service的功能。

1、 Annotation方式注册
这种方式主要是通过BeeHiveMod宏进行Annotation标记

//***** 使用
BeeHiveMod(ShopModule)

//***** BeeHiveMod的宏定义
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

//***** BeeHiveDATA的宏定义 
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

//*****  全部转换出来后为下面的格式
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";

这里针对__attribute需要说明以下几点

  • 第一个参数used:用来修饰函数,被used修饰以后,意味着即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器下会去掉没有被引用的段。

  • 通过使用__attribute__((section("name")))来指明哪个段。数据则用__attribute__((used))来标记,防止链接器会优化删除未被使用的段,然后将模块注入到__DATA

此时Module已经被存储到Mach-O文件的特殊段中,那么如何取呢?

  • 进入BHReadConfiguration方法,主要是通过Mach-O找到存储的数据段,取出放入数组中
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
    
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    // 找到之前存储的数据段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片内存
    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; 
}

2、读取本地Pilst文件

  • 首先,需要设置好路径
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
创建plist文件,Plist文件的格式也是数组中包含多个字典。字典里面有两个Key,一个是@"moduleLevel",另一个是@"moduleClass"。注意的数组的名字叫@“moduleClasses”
进入loadLocalModules方法,主要是从Plist里面取出数组,然后把数组加入到BHModuleInfos数组里面。
//初始化context时,加载Modules和Services
-(void)setContext:(BHContext *)context
{
    _context = context;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self loadStaticServices];
        [self loadStaticModules];
    });
}
👇
//加载modules
- (void)loadStaticModules
{
    // 读取本地plist文件里面的Module,并注册到BHModuleManager的BHModuleInfos数组中
    [[BHModuleManager sharedManager] loadLocalModules];
    //注册所有modules,在内部根据优先级进行排序
    [[BHModuleManager sharedManager] registedAllModules];
    
}
👇
- (void)loadLocalModules
{
    //plist文件路径
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
    //判断文件是否存在
    if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
        return;
    }
    //读取整个文件[@"moduleClasses" : 数组]
    NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
    //通过moduleClasses key读取 数组 [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]]
    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]]) {
            //存储到 BHModuleInfos 中
            [self.BHModuleInfos addObject:obj];
        }
    }];
}

3、load方法注册

+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}

该方法注册Module就是在Load方法里面注册Module的类

+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}

进入registerDynamicModule实现

+ (void)registerDynamicModule:(Class)moduleClass
{
    [[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
👇
- (void)registerDynamicModule:(Class)moduleClass
{
    [self registerDynamicModule:moduleClass shouldTriggerInitEvent:NO];
}
👇
- (void)registerDynamicModule:(Class)moduleClass
       shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
{
    [self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent];
}

其底层还是同第一种方式一样,最终会走到addModuleFromObject:shouldTriggerInitEvent:方法中

  • load方法,还可以使用BH_EXPORT_MODULE宏代替
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}

BH_EXPORT_MODULE宏里面可以传入一个参数,代表是否异步加载Module模块,如果是YES就是异步加载,如果是NO就是同步加载

2、BeeHive 模块事件

BeeHive会给每个模块提供生命周期事件,用于与BeeHive宿主环境进行必要信息交互,感知模块生命周期的变化。

BeeHive各个模块会收到一些事件。在BHModuleManager中,所有的事件被定义成了BHModuleEventType枚举。如下所示,其中有2个事件很特殊,一个是BHMInitEvent,一个是BHMTearDownEvent

typedef NS_ENUM(NSInteger, BHModuleEventType)
{
    //设置Module模块
    BHMSetupEvent = 0,
    //用于初始化Module模块,例如环境判断,根据不同环境进行不同初始化
    BHMInitEvent,
    //用于拆除Module模块
    BHMTearDownEvent,
    BHMSplashEvent,
    BHMQuickActionEvent,
    BHMWillResignActiveEvent,
    BHMDidEnterBackgroundEvent,
    BHMWillEnterForegroundEvent,
    BHMDidBecomeActiveEvent,
    BHMWillTerminateEvent,
    BHMUnmountEvent,
    BHMOpenURLEvent,
    BHMDidReceiveMemoryWarningEvent,
    BHMDidFailToRegisterForRemoteNotificationsEvent,
    BHMDidRegisterForRemoteNotificationsEvent,
    BHMDidReceiveRemoteNotificationEvent,
    BHMDidReceiveLocalNotificationEvent,
    BHMWillPresentNotificationEvent,
    BHMDidReceiveNotificationResponseEvent,
    BHMWillContinueUserActivityEvent,
    BHMContinueUserActivityEvent,
    BHMDidFailToContinueUserActivityEvent,
    BHMDidUpdateUserActivityEvent,
    BHMHandleWatchKitExtensionRequestEvent,
    BHMDidCustomEvent = 1000
    
};

主要分为三种

1、系统事件:主要是指Application生命周期事件!

 

一般的做法是AppDelegate改为继承自BHAppDelegate

@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>

2、应用事件:官方给出的流程图,其中modSetupmodInit等,可以用于编码实现各插件模块的设置与初始化。

3、自定义事件
以上所有的事件都可以通过调用BHModuleManagertriggerEvent:来处理。
- (void)triggerEvent:(NSInteger)eventType
{
    [self triggerEvent:eventType withCustomParam:nil];
    
}
👇
- (void)triggerEvent:(NSInteger)eventType
     withCustomParam:(NSDictionary *)customParam {
    [self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam];
}
👇
#pragma mark - module protocol
- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id<BHModuleProtocol>)target
          withCustomParam:(NSDictionary *)customParam
{
    switch (eventType) {
            //初始化事件
        case BHMInitEvent:
            //special
            [self handleModulesInitEventForTarget:nil withCustomParam :customParam];
            break;
            //析构事件
        case BHMTearDownEvent:
            //special
            [self handleModulesTearDownEventForTarget:nil withCustomParam:customParam];
            break;
            //其他3类事件
        default: {
            NSString *selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
            [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam];
        }
            break;
    }
    
}
从上面的代码中可以发现,除去BHMInitEvent初始化事件和BHMTearDownEvent拆除Module事件这两个特殊事件以外,所有的事件都是调用的handleModuleEvent:forTarget:withSeletorStr:andCustomParam:方法,其内部实现主要是遍历 moduleInstances实例数组,调用performSelector:withObject:方法实现对应方法调用
- (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 实例数组,调用performSelector:withObject:方法实现对应方法调用
    [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)]];
            
        }
    }];
}

注意:这里所有的Module必须是遵循BHModuleProtocol的,否则无法接收到这些事件的消息。

3、BeeHive模块调用

在BeeHive中是通过BHServiceManager来管理各个Protocol的。BHServiceManager中只会管理已经被注册过的Protocol

注册Protocol的方式总共有三种,和注册Module是一样一一对应的

1、Annotation方式注册

//****** 1、通过BeeHiveService宏进行Annotation标记
BeeHiveService(HomeServiceProtocol,BHViewController)

//****** 2、宏定义
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

//****** 3、转换后的格式,也是将其存储到特殊的段
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";

2、读取本地plist文件

  • 首先同Module一样,需要先设置好路径

 

同样也是在setContext时注册services
//加载services
-(void)loadStaticServices
{
    [BHServiceManager sharedManager].enableException = self.enableException;
    
    [[BHServiceManager sharedManager] registerLocalServices];
    
}
👇
- (void)registerLocalServices
{
    NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
    //获取plist文件路径
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
    if (!plistPath) {
        return;
    }
    
    NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
    
    [self.lock lock];
    //遍历并存储到allServicesDict中
    for (NSDictionary *dict in serviceList) {
        NSString *protocolKey = [dict objectForKey:@"service"];
        NSString *protocolImplClass = [dict objectForKey:@"impl"];
        if (protocolKey.length > 0 && protocolImplClass.length > 0) {
            [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
        }
    }
    [self.lock unlock];
}

3、load方法注册

在Load方法里面注册Protocol协议,主要是调用BeeHive里面的registerService:service:完成protocol的注册

+ (void)load
{
   [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
👇
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
    [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}

到此,三种方式就创建完成了

Protocol的获取

ProtocolModule的区别在于,ProtocolModule多了一个方法,可以返回Protocol实例对象

- (id)createService:(Protocol *)proto;
{
    return [[BHServiceManager sharedManager] createService:proto];
}
👇
- (id)createService:(Protocol *)service
{
    return [self createService:service withServiceName:nil];
}
👇
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {
    return [self createService:service withServiceName:serviceName shouldCache:YES];
}
👇
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
    if (!serviceName.length) {
        serviceName = NSStringFromProtocol(service);
    }
    id implInstance = nil;
    //判断protocol是否已经注册过
    if (![self checkValidService:service]) {
        if (self.enableException) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
        }
        
    }
    
    NSString *serviceStr = serviceName;
    //如果有缓存,则直接从缓存中获取
    if (shouldCache) {
        id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
        if (protocolImpl) {
            return protocolImpl;
        }
    }
    //获取类后,然后响应下层的方法
    Class implClass = [self serviceImplClass:service];
    if ([[implClass class] respondsToSelector:@selector(singleton)]) {
        if ([[implClass class] singleton]) {
            if ([[implClass class] respondsToSelector:@selector(shareInstance)])
                //创建单例对象
                implInstance = [[implClass class] shareInstance];
            else
                //创建实例对象
                implInstance = [[implClass alloc] init];
            if (shouldCache) {
                //缓存
                [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
                return implInstance;
            } else {
                return implInstance;
            }
        }
    }
    return [[implClass alloc] init];
}

createService会先检查Protocol协议是否是注册过的。然后接着取出字典里面对应的Class,如果实现了shareInstance方法,那么就创建一个单例对象,如果没有,那么就创建一个实例对象。如果还实现了singleton,就能进一步的把implInstanceserviceStr对应的加到BHContextservicesByName字典里面缓存起来。这样就可以随着上下文传递了

  • 进入serviceImplClass实现,从这里可以看出 protocol和类是通过字典绑定的,protocol作为keyserviceImp(类的名字)作为value
- (Class)serviceImplClass:(Protocol *)service
{
    //通过字典将 协议 和 类 绑定,其中协议作为key,serviceImp(类的名字)作为value
    NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
    if (serviceImpl.length > 0) {
        return NSClassFromString(serviceImpl);
    }
    return nil;
}

Module & Protocol

这里简单总结下:

  • 对于Module:数组存储

  • 对于Protocol:通过字典将protocol与类进行绑定,keyprotocolvalue为 serviceImp即类名

辅助类

  • BHConfig类:是一个单例,其内部有一个NSMutableDictionary类型的config属性,该属性维护了一些动态的环境变量,作为BHContext的补充存在

  • BHContext类:是一个单例,其内部有两个NSMutableDictionary的属性,分别是modulesByName和 servicesByName。这个类主要用来保存上下文信息的。例如在application:didFinishLaunchingWithOptions:的时候,就可以初始化大量的上下文信息

//保存信息
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
  • BHTimeProfiler类:用来进行计算时间性能方面的Profiler

  • BHWatchDog类:用来开一个线程,监听主线程是否堵塞

 

注意:

1:获取当前视图最顶层的ViewController

+ (UIViewController *)currentViewController {
    UIWindow * window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal){
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(UIWindow * tmpWin in windows){
            if (tmpWin.windowLevel == UIWindowLevelNormal){
                window = tmpWin;
                break;
            }
        }
    }
    UIViewController *currentVC = window.rootViewController;
    while (currentVC.presentedViewController) {
        currentVC = currentVC.presentedViewController;
    }
    if ([currentVC isKindOfClass:[UITabBarController class]]) {
        currentVC = [(UITabBarController *)currentVC selectedViewController];
    }
    if ([currentVC isKindOfClass:[UINavigationController class]]) {
        currentVC = [(UINavigationController *)currentVC topViewController];
    }
    return currentVC;
}

2:如何通过一个view查找它所在的viewController

我们可以为UIView写一个分类,如下:

#import <UIKit/UIKit.h>
@interface UIView (KFGetController)

//获取当前view所在的控制器
- (UIViewController *)viewController;
@end

#import "UIView+KFGetController.h"
@implementation UIView (KFGetController)

- (UIViewController *)viewController
{
    //获取当前view的superView对应的控制器
    UIResponder *next = [self nextResponder];
    do {
        if ([next isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)next;
        }
        next = [next nextResponder];
    } while (next != nil);
    return nil;
    
}
@end

3:  Mediator:类名转化类对象、跳转、传值。

[target performSelector:sel withObject:parmas];
//OBMediator.h
@interface OBMediator : NSObject
+(OBMediator *)shared;

- (void)performTarget:(NSString *)targetName Action:(NSString *)actionName Params:(NSDictionary *)parmas;

@end

//OBMediator.m
#import "OBMediator.h"
#import "ViewController.h"
//#import "PersonalVC.h"

@implementation OBMediator
+(OBMediator *)shared {
    static OBMediator *mediator = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[OBMediator alloc] init];
    });
    return mediator;
}
- (void)performTarget:(NSString *)targetName Action:(NSString *)actionName Params:(NSDictionary *)parmas {
    
    Class cls = NSClassFromString(targetName);
    NSObject *target = [[cls alloc] init];
    SEL sel = NSSelectorFromString(actionName);
    if (![target respondsToSelector:sel]) {
        NSLog(@"未知错误");
        return ;
    }
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [target performSelector:sel withObject:parmas];
}
@end

4:中间类分类:管理每个组件

// OBMediator+Personal.h
#import "OBMediator.h"
@interface OBMediator (Personal)

- (void)openPersonalVCWithParams:(NSDictionary *)parmas;

@end

//OBMediator+Personal.m
#import "OBMediator+Personal.h"
NSString * const PersonalTarget = @"Target_Personal";
NSString * const ActionPushPersonal = @"pushToPersonalVCWithParams:";

@implementation OBMediator (Personal)


- (void)openPersonalVCWithParams:(NSDictionary *)parmas {
    //内部通过Target_Personal字符串找到该对象,执行他的selector,就是找到最终的跳转代码
    [self performTarget:PersonalTarget Action:ActionPushPersonal Params:parmas];
    
}
@end

5:Target

@interface Target_Personal : NSObject

@end


#import "Target_Personal.h"
#import "PersonalVC.h"

@implementation Target_Personal


- (void)pushToPersonalVCWithParams:(NSDictionary *)parmas {
    
    PersonalVC * vc =[[PersonalVC alloc] init];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:vc animated:YES completion:nil];
}

@end

6:调用Target

#import "OBMediator+Personal.h"

- (void)click {
    [[OBMediator shared] openPersonalVCWithParams:@{@"key":@"value"}];
}

Target-Action方案的优点。

  1. 解耦:只存在组件依赖Mediator中介的这一层依赖关系。
  2. Target-Action方案也能有一定的安全保证,它对URL中的Native前缀进行安全验证。因此,Target-Action方案不管从维护性、可读性、扩展性来说,都是一个比较完美的方案。
  3. 统一处理所有组件间调用入口,方便管理:都是调用“performTarget: action: params: shouldCacheTarget:”方法。第三个参数是一个字典,这个字典里面可以传很多参数,只要Key-Value写好就可以了。
    Target-Action方案处理错误的方式也统一在一个地方了,Target没有,或者是Target无法响应的Action方法,都可以在Mediator这里进行统一出错处理。

Target-Action方案的缺点

  1. 每个组件的Category对应一个Target类,Categroy中的Action方法对应Target类中的Action场景。代码量增多
  2. 伪解耦,通过字符串的方式来创建实例,依赖各种定义的String,维护String

注意

 

引用

 1:iOS底层原理 - 组件化

2:iOS-底层原理 35:组件化(一)方案

3:iOS-底层原理 35:组件化(二)组件间通讯方式

4:iOS组件化方案的几种实现

5:iOS组件化实践(一):简介

6:iOS组件化实践(二):准备

7:iOS组件化实践(三):实施

8:iOS-组件化(OC篇)

9: iOS组件化(上篇)- 拆分基础组件

10:BeeHive —— 一个优雅但还在完善中的解耦框架

11:BeeHive,一次iOS模块化解耦实践

12:iOS:组件化之Target-Action模式

posted on 2020-12-04 20:30  风zk  阅读(427)  评论(0编辑  收藏  举报

导航