随着项目的发展,很多规模较大的App,例如淘宝、美团之类的App,已经变成了多个App的大集合。有众多的业务线和不同的开发团队,分别负责开发各自的部分(模块)。相互之间需要调用(通信)的时候,如果直接引用并调用,就会变成这样。
可以看出,变现在工程中就是一个工程中,多个模块之间的相互import,耦合在一起。
1. 所有模块在一个工程里,无法分开编译,开发时每次编译,需要编译全部源码,严重影响开发效率。
2. 由于所有模块耦合在一起,开发A模块功能时,可能需要动到B模块里面的代码。可能会导致B模块的问题。
3. 如果淘宝App里面的C模块,想要挪到天猫App里面,几乎肯定是要在天猫App中重写一遍C模块的代码了,因为C模块耦合在淘宝App中根本拆不出来。以后C模块的需求开发都要分别在两个App中开发一次。
诉求:
1. 每个模块分开编译,互不影响。
2. 每个模块分成不同的仓库,不同团队进行维护。
3. 整个模块可以轻松的复用。
根据现象猜实现
首先看下BeeHive给出的结果,demo中的解耦结果是这样:
#import "HomeServiceProtocol.h"
id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
// 尽情操作 homeVc,调用 协议HomeServiceProtocol中提供的方法。
createService:方法,只要传进去一个参数protocol,就能创建出来一个homeVC
显然,HomeServiceProtocol和homeVC肯定提前偷偷注册进一个字典里了。key是HomeServiceProtocol,value是homeVC。然后给一个protocol就能找到对应的对象。不然不可能根据协议找到实现的。(除非你遍历所有类,扯远了)
homeVC遵守HomeServiceProtocol,所以可以在下面随意调用HomeServiceProtocol中提供的方法,即其他模块需要调用homeVC的所有方法,全都暴露在这个协议中,供其他模块调用。
(这样做就只需要依赖抽象的protocol,而不需要依赖class了)
我在我的模块里调用homeVC,只要protocol中定义了方法,就能调用,并编译通过。进行模块的发版。
代码实现
从上面的猜测中可知,有一个全局的字典注册了所有protocol与class的对应关系,使后来可以在工程的任意地方都能根据protocol创建出来具体的class,并调用protocol里所有的方法。
看一下注册方式和读取时机。
注册方式一:
BeeHiveService的宏定义:
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
展开,将参数带进去之后
@class BeeHive;
char * kHomeServiceProtocol_service BeeHiveDATA(BeehiveServices) = "{ ""HomeServiceProtocol"" : ""BHViewController""}";
BeeHiveDATA的宏定义:
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
完全展开带进去之后,得到
@class BeeHive;
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,"BeehiveServices" "))) = "{ ""HomeServiceProtocol"" : ""BHViewController""}";
上面的意思是,会将等号后面的字符串,{ ""HomeServiceProtocol"" : ""BHViewController""}
,存储到mach-o文件的__DATA段的一个名叫BeehiveServices的section中。
读取时机一:
在BHAnnotation.m 中(main函数之前)
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
首先,__attribute__((constructor))修饰的函数,会在main函数之前被调用。
_dyld_register_func_for_add_image函数,dyld加载每个库的时候,都会调用其中的回调block,即dyld_callback
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
//register services
// 这里是读取之前注册在BeehiveServices的section中的字符串
NSArray<NSString *> *services = BHReadConfiguration("BeehiveServices",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) {
// 这里解析出来 protocol 和对应的class
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
// 注册进全局的BHServiceManager的一个字典里
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
出去出来后转换成键值对,存放在[BHServiceManager shareManager].allServiceDict的字典中。
整个流程图:
注册方式二:
放进一个plist文件中来维护。
读取时机二:
application:didFinishLauchingWithOptions:(main函数之后)
- (void)registerLocalServices
{
NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
[self.lock lock];
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];
}
通过上述方式,结构就变成了下图:
ABCD之间相互依赖被解除,如果是模块A的开发者,开发时只需要编译模块A,可以选择性编译模块BCD,不影响运行调试。