iOS 组件化实践(一):中间层计划挑选
这儿首要剖析 casatwy/CTMediator、alibaba/BeeHive、lyujunwei/MGJRouter 三个库的源码。
CTMediator
首要咱们只看 CTMediator 单例类的内容,能够看到 CTMediator 以 Target-Action 的方法凭借字符串和 runtime 运用 NSInvocation(NSMethodSignature、SEL)或许 NSObject 协议的 - performSelector:withObject:
来完成函数的调用履行,这样运用 CTMediator 这个中间件咱们就能够抹掉咱们日常在文件顶部运用 #import 引进的依靠(类)。凭借字符串则是指在 CTMediator 类的中心函数:- performTarget:action:params:shouldCacheTarget:
中:
- (id _Nullable )performTarget:(NSString * _Nullable)targetName
action:(NSString * _Nullable)actionName
params:(NSDictionary * _Nullable)params
shouldCacheTarget:(BOOL)shouldCacheTarget;
可看到 targetName/actionName/params 三者根本以字符串类型传入,然后运用 runtime 创立 targetName 方针、创立 SEL,然后调用咱们熟悉的 NSObject 协议的 - (BOOL)respondsToSelector:(SEL)aSelector;
函数判别对应的 SEL 是否被 targetName 方针所完成,然后当对应的 SEL 回来根本类型时运用 NSInvocation 的方法进行函数调用,或许运用咱们熟悉的 NSObject 协议的 - (id)performSelector:(SEL)aSelector withObject:(id)object;
函数进行函数调用,当 targetName 方针创立失利或许对应的 SEL 函数不存在时都会进行安全的兜底操作。这样咱们就能够凭借 CTMediator 单例类不进行任何注册操作,在当时上下文环境中直接运用方针方针的类姓名符串和方针函数的字符串姓名完成函数调用了,把当时上下文环境与方针方针完全解耦。
CTMediator 单例类的内容根本就上面那些了,下面咱们看一下它是如安在组件化计划中发挥效果的。
首要咱们创立模块(组件)时都需求依靠 CTMediator 这个单例类,然后把模块(组件)的揭露 API 统一放在 CTMediator 类的一个分类中(Swift 中运用的是 CTMediator 类的 extension),而在 CTMediator 分类的完成中经过 performTarget...
函数指定 Target 和 Action 的字符串并把参数包装在字典中进行函数调用。然后当模块(组件)之间需求通讯时,直接经过模块对应的 CTMediator 分类中界说的揭露 API 完成通讯,完全不需求模块中的原始文件引用依靠,这样经过 CTMediator 单例类及其分类就解除了需求通讯的各个模块之间的强依靠关系,一起 CTMediator 分类中界说好的揭露 API 也对函数的参数进行了必定的校验。
然后咱们每个模块(组件)需求创立一个对应 CTMediator 分类中的 Target 姓名的类,并让它实践完成 CTMediator 分类中揭露的 API,那么当模块之间产生通讯时就会实践履行到这儿。
刚刚描述的三部分内容正对应了 CTMediator 项目中的三个文件夹:
- Categories(它里面是每个模块的揭露 API 对应的 CTMediator 的一个分类,实践运用中,这是一个独自的 repo,所用需求调度其他模块的人,只需求依靠这个 repo。这个 repo 由 target-action 保护者保护)
- CTMediator(这也是独自的 repo,完整的中间件就这 100 行代码)
- DemoModule(target-action 地点的模块,也就是供给服务的模块,这也是独自的 repo,但无需被其他人依靠,其他人经过 CTMediator category 调用到这儿的功用)
CTMediator 文件夹中最中心的是 CTMediator 单例类的完成,它供给了两种方法的 Target-Action 调用,一种是咱们直接传入 targetName、actionName、params 进行调用,一种是经过相似 scheme://[target]/[action]?[params]
(url sample: aaa://targetA/actionB?id=1234
)URL 的形式,内部则是对这个 URL 进行处理,首要提取出其间的 Target/Action/Params 然后再进行直接的 Target-Action 调用。
- (BOOL)respondsToSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
The mediator with no regist process to split your iOS Project into multiple project. 没有注册流程的 mediator 将你的 iOS Project 拆分为多个 project。
CTMediator 帮助你将项目划分为多个项目(这儿是指引进多个自己制作的 pod 库),并运用 Target-Action 形式让 subprojects 彼此通讯。没有注册进程!
BeeHive
BeeHive 不同于 CTMediator,它供给了完全不同的解藕方法。BeeHive 选用了 Protocol 与完成 Protocol 的指定类绑定的方法完成解耦,看起来它比 CTMediator 难理解一些,看起来更杂乱一些,首要是它内部自界说多个名词,以及更多的源码,其实也没什么,咱们很容易就能看懂,下面咱们一点一点深入学习一下它。
首要 BeeHive 有一个注册的进程,这儿也对应了上面 CTMediator 中提到了 CTMediator 不需求注册的进程。而这个注册的效果,咱们先不明说,留给咱们进行考虑,咱们先看一下 BeeHive 供给的三种不同的注册方法,下面咱们分别来看一下这些个注册进程,看懂了这三种注册进程,那么这个注册的效果咱们也就一目了然了。
Annotation 方法注册/注解的方法进行注册
经过注解的方法进行注册,注册进程中所涉及的完成细节是与 BeeHive 项目中的 BHAnnotation 类文件绑定在一起的,实践 BHAnnotation 类是一个空类,它其间没有界说任何内容,它的 .h .m 文件仅是用来盛放注解所涉及到的代码的。下面咱们直接学习 BHAnnotation.h .m 中的内容。
首要是 BHAnnotation.h 中的预处理句子和几个宏界说:
#ifndef BeehiveModSectName
#define BeehiveModSectName "BeehiveMods"
#endif
#ifndef BeehiveServiceSectName
#define BeehiveServiceSectName "BeehiveServices"
#endif
#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"\"}";
BeehiveModSectName 和 BeehiveServiceSectName 两个字符串宏界说,分别用来给 module 和 service 起的在 DATA 段中寄存数据的 section 名,这儿必定要有 Mach-O 的基础知识,要不然会不理解这儿的含义。
下面的 BeeHiveMod 和 BeeHiveService 两个宏就是在 __DATA
段的指定 section 中存入指定的内容(字符串)。直接把咱们需求的 mod 和 service 信息在 main 函数调用之前就注入到 Mach-O 中去。
在 BeeHive Example 项目中看到:@BeeHiveMod(ShopModule)
、@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
、@BeeHiveService(HomeServiceProtocol,BHViewController)
三个宏的运用,把它们展开的话分别如下,看着更明晰一些:
@class BeeHive; // BeeHive 类的前向声明
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";
@class BeeHive;
char * kUserTrackServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""UserTrackServiceProtocol""\" : \"""BHUserTrackViewController""\"}";
@class BeeHive;
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";
在 DATA 段的 BeehiveMods 区中写入 ""ShopModule""
字符串。在 DATA 段的 BeehiveServices 区中写入 { \"""UserTrackServiceProtocol""\" : \"""BHUserTrackViewController""\"}
和 { \"""HomeServiceProtocol""\" : \"""BHViewController""\"}
字符串,这儿表明在当时项目注入了 Shop 模块、UserTrackServiceProtocol 协议的完成类是 BHUserTrackViewController、HomeServiceProtocol 协议的完成类是 BHViewController,这也对应了在 BeeHive 项目中经过协议创立方针:
id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
创立的 homeVc 和 V4 两个变量分别就是 BHViewController 和 BHUserTrackViewController 控制器实例。
下面咱们接着来看 BHAnnotation.m 文件中的几个函数:
BHReadConfiguration
读取指定 image section 中的数据,保存的是作为配置信息的一些字符串。mhp 是当时可履行文件发动进程中加载的 image 的 header 指针。
单纯看 BHReadConfiguration 函数的话,其实其内容很简略,传入 image header(mhp)指针和 sectionName 字符串,然后在这个 image 中读取 DATA 段中此 section 的内容。其间指针转换、循环取内容的代码看起来可能有点绕,其实是当咱们在 section 中保存的是字符串时,此刻 section 并不是直接保存字符串的内容,而是字符串的指针(地址),字符串的实践内容位于 TEXT 段的 __cstring
section 中,其实项目中呈现的字符串字面量都会保存在这个 section 中,如截图中所示:
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;
// #define SEG_DATA "__DATA" /* the tradition UNIX data segment */
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
// sectionName 区中的数据长度,然后除以一个指针长度,可得出指针个数,这儿是因为咱们在指定的 section 中保存的都是字符串,所以咱们直接读取时,是一个指针。
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;
}
initProphet
initProphet 函数比较特殊,它被添加了 __attribute__((constructor))
修饰,这样 initProphet 函数会在 main 函数之前得到调用,而它的内部只有一行代码,即把 dyld_callback 函数注册为 dyld 加载 image 的回调,这样在 APP 发动进程中每一个 image 被加载后 dyld_callback 函数就会被调用一次,打印一下可发现在 BeeHive 发动进程中 dyld_callback 函数被调用了多次。
/*
The following functions allow you to install callbacks which will be called by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image() the callback func is called for every existing image. Later, it is called as each new image is loaded and bound (but initializers not yet run). The callback registered with _dyld_register_func_for_remove_image() is called after any terminators in an image are run and before the image is un-memory-mapped.
*/
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
extern void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
假如经过函数 _dyld_register_func_for_add_image
注册 image 被加载时的回调函数,那么每逢后续有新的 image 被加载但未初始化前 dyld 就会调用注册的回调函数,回调函数的两个入参分别表明加载的 image 的头结构和对应的 Slide 值(虚拟内存偏移值)。假如在调用 _dyld_register_func_for_add_image
时体系现已加载了某些 image,则会分别对这些加载结束的每个 image 调用注册的回调函数。假如你经过函数 _dyld_register_func_for_remove_image
注册了 image 被卸载时的回调函数时,那么每逢 image 被卸载前都会调用注册的回调函数,回调函数的两个入参分别表明卸载的 image 的头结构和对应的 Slide 值。这两个函数的效果通常用来做程序加载 image 的监控以及一些计算处理。
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
那么在 dyld_callback 函数中调用下面的打印 image name 的函数,能够看到在 main 函数之前在模拟器环境下现已有 335 个 image 被加载,而咱们保存在 section 中的自界说数据位于 xxx/BeeHive_Example.app/BeeHive_Example
image 中,这儿假如咱们把咱们的模块拆分做一个私有 pod 的话,经过 pod 方法把模块引进主工程中,那么咱们在模块中经过注解在指定 section 中添加的自界说数据就会位于模块的 image 中,或许说是 pod 子项意图 image 中。即这儿对应了尽管 BeeHive 需求一个注册进程,但是并不是说咱们必须在主工程中一个一个的把需求的模块进行手动注册,而是咱们只要把咱们需求的模块导入工程即可(常选 pod 方法),然后在程序发动的进程中扫描一切的 image,自动找出其间一切需求进行注册的 moudels 和 services 进行注册。这种方法注册简略方便,每个模块(组件)能够在自己自行注册,不需求集中注册,整体流程对开发者比较友爱。
void printImagePath(const struct mach_header *mhp) {
int dyld_count = _dyld_image_count();
NSLog(@"☘️☘️☘️ %d", dyld_count);
for (int i = 0; i < dyld_count; i++) {
const struct mach_header* image_header_pointer = _dyld_get_image_header(i);
if (image_header_pointer == mhp) {
const char * imagePath = _dyld_get_image_name(i);
NSString *res = [NSString stringWithUTF8String:imagePath];
NSString *imageName = [res componentsSeparatedByString:@"/"].lastObject;
NSLog(@" %@", imageName);
// NSLog(@" %@", res);
}
}
}
xcode 控制台部分打印截取。
...
2022-07-23 22:00:58.765894+0800 BeeHive_Example[47221:3018663] ☘️☘️☘️ 335
2022-07-23 22:00:58.766079+0800 BeeHive_Example[47221:3018663] /Users/hmc/Library/Developer/CoreSimulator/Devices/377B8219-5922-46DB-9112-79701E6CC006/data/Containers/Bundle/Application/EC97BACE-90F1-4C31-B852-FDC525CEBEB0/BeeHive_Example.app/BeeHive_Example
2022-07-23 22:00:58.766805+0800 BeeHive_Example[47221:3018663] config = ShopModule
2022-07-23 22:00:58.767542+0800 BeeHive_Example[47221:3018663] ShopModule init
2022-07-23 22:00:58.767784+0800 BeeHive_Example[47221:3018663] config = { "HomeServiceProtocol" : "BHViewController"}
2022-07-23 22:00:58.767854+0800 BeeHive_Example[47221:3018663] config = { "UserTrackServiceProtocol" : "BHUserTrackViewController"}
...
dyld_callback
dyld_callback 函数中首要进行读取 BeehiveMods 和 BeehiveServices section 中的数据,然后把 module 信息和 services 信息保存到 BHModuleManager 和 BHServiceManager 单例类的特点中去,即完成了注册进程。
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
printImagePath(mhp);
// register dynamic module
// 读取 Data 段 BeehiveMods 区中的数据,假如存在的话
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
// 在本项目中读出了:ShopModule
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
// 把读出的 module 注册到 BHModuleManager 单例类的特点中去
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
// register services
// 读取 Data 段 BeehiveServices 区中的数据,假如存在的话
NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
// 在本项目中读出了:{ "HomeServiceProtocol" : "BHViewController"}、{ "UserTrackServiceProtocol" : "BHUserTrackViewController"}
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) {
// 把 protocol 和完成该 protocol 的 clsName 成对注册到 BHServiceManager 单例类的 allServicesDict 特点中去
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
到这儿经过注解的方法注册就看完了,粗心也就是在 APP 发动之前读取一些自界说数据待后续进行运用。下面咱们看第二种注册方法。
读取 .plist 文件内容的方法进行注册
把需求的 modules 和 services 数据保存在 .plist 文件中,然后在 application:didFinishLaunchingWithOptions:
回调函数中进行读取。
在 TestAppDelegate.m 文件中有如下代码:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// 记载 application 和 launchOptions 两个特点
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
// 指定存储 module 和 service 数据的 .plist 文件的位置
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive"; // 可选,默以为 BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
// 是否敞开 Exception,假如敞开的话当产生异常情况时会进行抛错
[BeeHive shareInstance].enableException = YES;
// 对 BeeHive 单例类的 context 特点进行赋值,而且而且而且读取保存在本地 .plist 文件中的 module 和 service 数据,并注册它们
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
// 仅记载 DEBUG 下的 event time
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:@"BeeHive::super start launch"];
[super application:application didFinishLaunchingWithOptions:launchOptions];
...
return YES;
}
BHContext 是一个单例类,保存许多上下文信息。其间 moduleConfigName 和 serviceConfigName 特点记载 .plist 文件的途径和姓名。在 BeeHive 单例类的 context 特点的 setter 函数中会对 .plist 文件内容进行读取,并注册其间的 module 和 service(并不是什么高深的操作,就是把这些数据放进 allServicesDict 这个全局变量内):
-(void)setContext:(BHContext *)context
{
_context = context;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 运用 dispatch_once 包裹,确保全局只调用一次
[self loadStaticServices];
[self loadStaticModules];
});
}
loadStaticModules 函数读取本地 .plist 文件中的内容,然后把它们添加到 allServicesDict 字典中即完成了注册进程。
BeeHive.bundle/BHService.plist 文件的内容如:{"service":"UserTrackServiceProtocol", "impl":"BHUserTrackViewController"}
此类字符串数组,协议名和完成协议的类名成对寄存。
<?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>
-(void)loadStaticServices
{
// 传递是否敞开 Exception 的值
[BHServiceManager sharedManager].enableException = self.enableException;
// 读取并注册本地的 service 信息
[[BHServiceManager sharedManager] registerLocalServices];
}
- (void)registerLocalServices
{
// @"BeeHive.bundle/BHService"
NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
// 读取 BeeHive.bundle/BHService.plist 文件
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) {
// 然后直接把协议和完成协议的类的姓名添加到 allServicesDict 字典中,没有进行验证协议是否存在,这个类是否遵循这个协议,刚刚在注解注册的进程中进行了验证
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
}
[self.lock unlock];
}
经过 .plist 文件注册的方法看完了,还有一种运用 +load 函数的方法。
运用 +load 的方法进行注册
无需保存文件名直接在 +load 方法中运用 serviceCenter 进行 protocol 与 class 的注册,等候运用的时候进行初始化,防止内存常驻。比较简略,就不再过多的剖析了。
+ (void)load
{
[BeeHive registerDynamicModule:[self class]];
}
BH_EXPORT_MODULE(NO)
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
MGJRouter
MGJRouter 超简略,只有一对 MGJRouter.h .m 文件,总共不到 220 行,下面咱们以 lyujunwei/MGJRouter 库房为例,来看一下它的运用。
MGJRouter 是一个单例类,它有一个 @property (nonatomic) NSMutableDictionary *routes;
特点用来记载注册的 URL,例如下面最简略的注册:
[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
[self appendLog:@"匹配到了 url,以下是相关信息"];
[self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
}];
mgj://foo/bar
注册结束后,咱们打印 routes 可看到如下,即把注册 URL 时的 handler block 沿着 URL 的 path 保存在 routes 字典中(假如 path 很长,那么这个 block 会嵌好深)。
mgj://foo/bar
{
mgj = {
"~" = {
foo = {
bar = {
"_" = "<__NSMallocBlock__: 0x600003bd21f0>";
};
};
};
};
}
然后咱们经过 URL 调用时:[MGJRouter openURL:@"mgj://foo/bar"]
,此刻就是根据指定 URL 从 routes 中提取出 block 履行,示例代码中提取出的履行数据如下,然后以 { "MGJRouterParameterURL": "mgj://foo/bar" }
为参数,履行上面注册的 handleer block。
{
MGJRouterParameterURL = "mgj://foo/bar";
block = "<__NSMallocBlock__: 0x600003bd21f0>";
}
然后还有一些其他方法的注册和调用,例如:
- 调用 open 时,能够传递 userinfo 作为参数:
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil]
。 - 假如有可变参数(包含 URL Query Parameter)会被自动解析:
[MGJRouter openURL:@"mgj://search/bicycle?color=red"]
,此刻可变参数的姓名需求双方协定好。 - 当 Open 结束时,履行 Completion Block:
[MGJRouter openURL:@"mgj://detail" withUserInfo:nil completion:^{ }]
。 …
MGJRouter 主页介绍的贼详细,函数封装的也贼明晰,这儿就不再复述了。
至此 CTMediator、BeeHive、MGJRouter 三种不同的解耦方法方法咱们就看完了,个人更偏向 BeeHive 一些,尽管现在选用的是更简略的 CTMediator 的计划。
参考链接
参考链接:
- casatwy/CTMediator
- alibaba/BeeHive
- lyujunwei/MGJRouter
- iOS运用架构谈 组件化计划
- 深入iOS体系底层之 image 文件操作API介绍