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:什么情况不需要组件化
- 项⽬较⼩,模块间交互简单,耦合少
- 模块没有被多个外部模块引⽤,只是⼀个单独的⼩模块
- 模块不需要重⽤,代码也很少被修改
- 团队规模很⼩
- 只能上层对下层依赖
- 项⽬公共代码资源 下沉
- 横向的依赖 最好下沉
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"''
这里我们就解决了三方依赖。现在我们需要解决模块间的依赖。
我们需要给他一个查询路径,在JCommonUIModule
的Podfile
中添加 ,解决本地依赖
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:中间层依赖
VC1 想PUSH VC2 需要有响应+参数+回调
VC2需要有响应方法
通过runtime我们知道方法都有消息接收者和消息主体。
这个时候就需要暴露接口:target-action,VC2需要来维护他。
这个时候我们就需要
Mediator
。Mediator
来帮助找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:¶ms 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'
调试一下就可以了。
二:组件化方案
目前主流的主要有以下三种方式:
-
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
的架构图:
[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"]
蘑菇街为了统一iOS
和Android
的平台差异性,专门用后台来管理url
,然后针对不同的平台,生成不同类型的文件,来方便使用。
使用url-block
的方案的确可以组建间的解耦,但是还是存在其它明显的问题,比如:
- 需要在内存中维护
url-block
的表,组件多了可能会有内存问题 url
的参数传递受到限制,只能传递常规的字符串参数,无法传递非常规参数,如UIImage
、NSData
等类型- 没有区分本地调用和远程调用的情况,尤其是远程调用,会因为
url
参数受限,导致一些功能受限 - 组件本身依赖了中间件,且分散注册使的耦合较多
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比较典型的三方框架就是阿里的BeeHive。BeeHive
借鉴了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
文件的格式也是数组中包含多个字典。字典里面有两个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、应用事件
:官方给出的流程图,其中modSetup
、modInit
等,可以用于编码实现各插件模块的设置与初始化。
自定义事件
BHModuleManager
的triggerEvent:
来处理。- (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的获取
Protocol
与Module
的区别在于,Protocol
比Module
多了一个方法,可以返回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,就能进一步的把implInstance
和serviceStr
对应的加到BHContext
的servicesByName
字典里面缓存
起来。这样就可以随着上下文传递了
- 进入
serviceImplClass
实现,从这里可以看出 protocol和类是通过字典
绑定的,protocol
作为key
,serviceImp
(类的名字)作为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
与类进行绑定,key
为protocol
,value
为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方案的优点。
- 解耦:只存在组件依赖Mediator中介的这一层依赖关系。
- Target-Action方案也能有一定的安全保证,它对URL中的Native前缀进行安全验证。因此,Target-Action方案不管从维护性、可读性、扩展性来说,都是一个比较完美的方案。
- 统一处理所有组件间调用入口,方便管理:都是调用“performTarget: action: params: shouldCacheTarget:”方法。第三个参数是一个字典,这个字典里面可以传很多参数,只要Key-Value写好就可以了。
Target-Action方案处理错误的方式也统一在一个地方了,Target没有,或者是Target无法响应的Action方法,都可以在Mediator这里进行统一出错处理。
Target-Action方案的缺点
- 每个组件的Category对应一个Target类,Categroy中的Action方法对应Target类中的Action场景。代码量增多
- 伪解耦,通过字符串的方式来创建实例,依赖各种定义的String,维护String
注意