简介
组件化方案中会对组件分层和解耦,上层业务组件依赖下层基础组件,不能反向依赖,而同层之间的组件需要通过组件通信方案来避免相互依赖的问题。
组件通信方案解决的是同层组件之间相互调用,而在编译层面上不产生代码依赖的问题。
- 模块可以理解成高内聚,低耦合的业务或者功能集合
- 组件更倾向于可复用的逻辑
- 订阅,编辑,日记属于业务层
- Markdown功能和高级工具属于基础业务层
- 网络库属于基础组件层
上层对下层直接依赖,下层不能对上层反向依赖,订阅,编辑和日记这些同层业务直接的调用,需要使用组件通信方案做同层解耦
由于解耦是解开编译依赖上的耦合,所以需要根据语言和系统库的特性找到除代码直接调用外的调用方式,可以看成是一种间接调用。直接代码调用的优点,编译检查,执行效率,缺点在于强耦合。从解耦角度考虑,很容易可以想到使用Protocol协议做解耦,而从动态化能力的角度考虑,基于Runtime也有灵活解耦的方式。
实现方案有几种,使用Protocol-Service实现三方库主要有BeeHive库,而CTMediator使用了Target-Action方式
- Protocol-Service
- Target-Action
Protocol-Service
protocol与service是一一对应的关系,protocol用于声明组件间的依赖接口,service对protocol中提供的能力做对应的实现。其中不同的protocol,由哪个service来实现就需要一个映射关系,也可以看成protocol-service的映射表。
能力提供方实现对应的protocol,并在映射表注册。调用方需要使用protocol的能力,从表里找到对应的service实现,从而完成调用
BeeHive
BeeHive除了提供了组件间通信的功能外,还提供了模块注册,应用和模块生命周期的事件分发等功能。此处主要分析组件通信能力。
使用
声明需要提供的接口Protocol,继承于BHServiceProtocol
@protocol HomeServiceProtocol <NSObject, BHServiceProtocol>
-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName;
@end
注册服务
动态注册
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];
静态注册
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>HomeServiceProtocol</key>
<string>BHViewController</string>
</dict>
</plist>
调用
获取到对应的Service,调用协议中声明的接口
#import "BHService.h"
id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
// use homeVc do invocation
原理
Protocol-Service表
- key是protocol,value是protocolImplClass
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
注册
写入到Mach-O中DATA段
- Mach-O的DATA段是可读可写的
- Service信息,写入到Mach-O中DATA段
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ ""#servicename"" : ""#impl""}";
从Mach-O中DATA段读取存储的信息
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp);
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;
}
attribute((constructor))中添加_dyld_register_func_for_add_image回调
-
attribute((constructor))在main函数之前被调用
-
虽然此时dyld相关的操作比如add_image的已经完成,_dyld_register_func_for_add_image的回调有2种情况
- 一个是在add_image的时机
- 如果注册回调的时候add_image已经完成,也会直接回调
__attribute__((constructor))
void init() {
_dyld_register_func_for_add_image(dyld_callback);
}
dyld_callback回调函数中,完成动态注册
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)];
}
}
}
}
}
注册时机
BeeHive中也使用静态注册的方式,使用plist文件。动态注册是使用了Annotation注解的方式,结合在Mach-O文件的__DATA段存储,之后在_dyld_register_func_for_add_image的回调中,读取对应的数据再注册到映射表中
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>service</key>
<string>UserTrackServiceProtocol</string>
<key>impl</key>
<string>BHUserTrackViewController</string>
</dict>
</array>
</plist>
纯Swift的项目主要有几个问题,一个是难以对Mach-O直接操作,还有一个是无法灵活获得反射关系,实例,类,protocol与父protocol的关系。对于无法直接使用Mach-O的DATA段实现Annotation的问题,我们只能把注册时机后移到Application -didFinishLaunchingWithOptions阶段。只要service与protocol的绑定,在service调用之前完成。
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Codes
ServiceManager.shared.register(service: "(HomeServiceProtocol.self)", implClass: HomeServiceImpelementor.self)
// Codes
}
获取
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
if (!serviceName.length) {
serviceName = NSStringFromProtocol(service);
}
id implInstance = nil;
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];
}
Target-Action
Target指的是需要交互的对象,Action指的是Target中被调用的方法,Target-Action模式在iOS开发中使用广泛。得益于OC的动态特性,使用和实现都很方便。
使用
创建提供给外部依赖的module,在module中给CTMediator类添加分类
@interface CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail;
@end
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativeFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
@end
在内部实现的module中,提供对应的Target,实现对应的Action
#import "Target_A.h"
@implementation Target_A
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
调用的模块,引入刚才对外提供的module,不用引入实际实现Target_Action的module,直接调用
#import "CTMediator+CTMediatorModuleAActions.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
[self.navigationController pushViewController:viewController animated:YES];
}
@end
原理
CTMediator中的中间件代码非常简短,主要是对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;
}
}
}
结构关系
适配层
从调用者的角度来说,由于模块外部依赖的不稳定性,通常会设计适配层(adapter)来隔离外部可能的api变更导致的内部多处调用都需要修改的情况。
模块中适配层要做的事情,举例来说无论是依赖SDWebImage还是Kingfisher实现图片加载,对业务模块内部都适配成loadImage一类的接口,外部接口不会对内部造成侵入。
调停者
原本各个module的Protocol都集中在ServiceLib库中,或者以文件夹的方式,或者以subspec的方式做区分。所以所有的module都只需要依赖ServiceLib即可,但是对于ServiceLib这个基础组件来说,它对应的指责和代码权限都不应该开放给所有业务线,Protocol作为各个module对外提供的能力是跟业务相关的能力和代码,并不适合放在Lib这样的基础组件中。
ServiceLib作为一个基础组件,不应该有业务代码的侵入,从而演变为以下结构。对于Target-Action方案,从调用规范的角度,把CTMediator扩展的分类设计成单独的module,提供给外部依赖。
Protocol都放在每个Extension的库中,由对应的业务域负责模块维护。由于Service需要实现Protocol,这边的业务库会对Extension有依赖关系。