iOS 组件化通信
最近新接触的项目开发方式使用的组件化开发的,以前从来没有接触过,也去网上找了些资料进行同步。
组件化的每个模块都应该是要相互独立的,且能够独立运行,所以最多可以引入通用工具类,但不应该引入其他同级业务模块的文件。
举个栗子:Home模块想要通过User模块中getUserInfo方法获取用户信息,普通的项目我们可以直接进行import "User.h",但是组件化的项目该怎么做才合适?
目前看到网络上的方式主要有几种,三种方法的核心都是由中间人进行管理和消息传递:
1、URL-Block的方式
这种方式的核心是预先将url对应的方法先注册给中间人,然后调用方调用的时候,中间人就会去哈希表或者数组里找到对应的方法进行调用。
以蘑菇街开发的MGJRouter为例,
[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
[self appendLog:@"匹配到了 url,以下是相关信息"];
[self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
}];
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];
整个哈希表的结构是类似树形的结构:
(lldb) po self.routes
{
mgj = {
"_" = "<__NSMallocBlock__: 0x610000050680>";
category = {
travel = {
"_" = "<__NSMallocBlock__: 0x6180000554e0>";
};
};
detail = {
"_" = "<__NSGlobalBlock__: 0x10f9bb448>";
};
search = {
":keyword" = {
"_" = "<__NSGlobalBlock__: 0x10f9bb4c8>";
};
":query" = {
"_" = "<__NSMallocBlock__: 0x610000051880>";
};
};
"~" = {
"_" = "<__NSMallocBlock__: 0x61000004cfc0>";
};
};
}
在调用registerURLPattern方法的时候,会将URL整理,和Block一起保存进哈希表,关键字 “_”作为存放Block的key,通过key-value的方式找到对应的block进行调用。
这种方式就是URL与Block之间的匹配,调用者和中间人连方法的持有者是谁都不需要知道。
总结:
1、被调用方需要提前register相应的block才能调用,而且被注册的block似乎并没有很好的释放时机?内存方面是否存在问题。
2、调用方和被调用方都需要依赖中间件。
3、单纯的URL拼接的方式并没有办法满足复杂的参数传递,所以才会衍生多种调用的方法。
2、Protocol-Class的方式
这种方案类似于第一种方案,它是保存和Protocol和Class的映射关系,每次调用方通过中间人拿到注册时候的Class,直接通过对象进行方法调用;
“大概是这样?”
[Router registerClass:[SomeClass Class] forProtocol:aProtocol];
id<aProtocol> someOne =[Router classForProtocol:aProtocol];
[someOne call];
总结:
1、类似于block的注册方式,也需要提前register对象。
2、调用双方都需要依赖中间管理类。
3、调用方式是直接调用,比较符合常用习惯。
3、Target-Selector的方式
这个方案是由CTMediator的作者提出的,他指出了一些URL-Block方式的缺点,然后提出了这种方案。
这个方案的核心就是我们平常使用的performSelector的方式。
// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;
中间人负责找到具体的target,然后实现方法转发。这个方式还强调了要区分本地调用和远程调用的方式,以防止黑客直接通过浏览器或OpenUrl的方式直接访问到APP内部模块。
核心实现主要是:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
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.cachedTarget[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.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
"消息转发,safePerformAction使用NSInvocation进行转发"
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];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
总结:
1、需要额外维护中间类拓展,只有调用方需要引用中间类。