路由的意义
路由并非只是指的界面跳转,还包括数据获取等几乎所有业务。
(一)项目中WKWebview
& UIWebView
与原生Native 交互路由
路由URL 例如 QJ://detail?name=xx&id=xxx
缺点很明显:字符串 URI 并不能表征 iOS 系统原生类型,要阅读对应模块的使用文档,大量的硬编码
项目中实现代码大概就是这样
//先维护一张路由表
- (NSDictionary *)getHostDictionary {
static NSDictionary *dicts = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dicts = @{
HOST_LOGOUT: @"logoutAction:",
HOST_ORDER_DETAIL: @"orderDetail:"
};
});
return dicts;
}
- (UIViewController *)logoutAction:(NSDictionary *)param {
//做一些登录登出的业务逻辑
}
通俗来讲
解析URI ---> 等到 target params ---> 调用原生
三、组件化的意义
前面对路由的分析提到了使用目标和参数 (aim/params
) 动态定位到具体业务的技术点。实际上在 iOS Objective-C 中大概有反射和依赖注入两种思路:
- 将
aim
转化为具体的Class
和SEL
,利用 runtime 运行时调用到具体业务。 - 对于代码来说,进程空间是共享的,所以维护一个全局的映射表,提前将
aim
映射到一段代码,调用时执行具体业务。
可以明确的是,这两种方式都已经让Mediator
免去了对业务模块的依赖:
而这些解耦技术,正是 iOS 组件化的核心。
组件化主要目的是为了让各个业务模块独立运行,互不干扰,那么业务模块之间的完全解耦是必然的,同时对于业务模块的拆分也非常考究,更应该追求功能独立而不是最小粒度。
(一) Runtime 解耦
为 Router 定义了一个统一入口方法:
/// 此方法就是一个拦截器,可做容错以及动态调度
- (id)performTarget:(NSString *)target action:(NSString *)action params:(NSDictionary *)params {
Class cls; id obj; SEL sel;
cls = NSClassFromString(target);
if (!cls) goto fail;
sel = NSSelectorFromString(action);
if (!sel) goto fail;
obj = [cls new];
if (![obj respondsToSelector:sel]) goto fail;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [obj performSelector:sel withObject:params];
#pragma clang diagnostic pop
fail:
NSLog(@"找不到目标,写容错逻辑");
return nil;
}
复制代码
简单写了下代码,原理很简单,可用 Demo 测试。对于内部调用,为每一个模块写一个分类:
@implementation BMediator (BAim)
- (void)gotoBAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
[self performTarget:@"BTarget" action:@"gotoBAimController:" params:@{@"name":name, @"callBack":callBack}];
}
@end
复制代码
可以看到这里是给BTarget
发送消息:
@interface BTarget : NSObject
- (void)gotoBAimController:(NSDictionary *)params;
@end
@implementation BTarget
- (void)gotoBAimController:(NSDictionary *)params {
BAimController *vc = [BAimController new];
vc.name = params[@"name"];
vc.callBack = params[@"callBack"];
[UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
复制代码
为什么要定义分类
定义分类的目的前面也说了,相当于一个语法糖,让调用者轻松使用,让 hard code 交给对应的业务工程师。
为什么要定义 Target “靶子”
- 避免同一模块路由逻辑散落各地,便于管理。
- 路由并非只有控制器跳转,某些业务可能无法放代码(比如网络请求就需要额外创建类来接受路由调用)。
- 便于方案的接入和摒弃(灵活性)。
可能有些人对这些类的管理存在疑虑,下图就表示它们的关系(一个块表示一个 repo):
image.png图中“注意”处箭头,B 模块是否需要引入它自己的分类 repo,取决于是否需要做所有界面跳转的拦截,如果需要那么 B 模块仍然要引入自己的 repo 使用。
完整的方案和代码可以查看 Casa 的 CTMediator,设计得比较完备,笔者没挑出什么毛病。
(二) Block 解耦
下面简单实现了两个方法:
- (void)registerKey:(NSString *)key block:(nonnull id _Nullable (^)(NSDictionary * _Nullable))block {
if (!key || !block) return;
self.map[key] = block;
}
/// 此方法就是一个拦截器,可做容错以及动态调度
- (id)excuteBlockWithKey:(NSString *)key params:(NSDictionary *)params {
if (!key) return nil;
id(^block)(NSDictionary *) = self.map[key];
if (!block) return nil;
return block(params);
}
复制代码
维护一个全局的字典 (Key -> Block),只需要保证闭包的注册在业务代码跑起来之前,很容易想到在+load
中写:
@implementation DRegister
+ (void)load {
[DMediator.share registerKey:@"gotoDAimKey" block:^id _Nullable(NSDictionary * _Nullable params) {
DAimController *vc = [DAimController new];
vc.name = params[@"name"];
vc.callBack = params[@"callBack"];
[UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
return nil;
}];
}
@end
复制代码
至于为什么要使用一个单独的DRegister
类,和前面“Runtime 解耦”为什么要定义一个Target
是一个道理。同样的,使用一个分类来简化内部调用(这是蘑菇街方案可以优化的地方):
@implementation DMediator (DAim)
- (void)gotoDAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
[self excuteBlockWithKey:@"gotoDAimKey" params:@{@"name":name, @"callBack":callBack}];
}
@end
复制代码
可以看到,Block 方案和 Runtime 方案 repo 架构上可以基本一致(见图6),只是 Block 多了注册这一步。
为了灵活性,Demo 中让 Key -> Block,这就让 Block 里面要写很多代码,如果缩小范围将 Key -> UIViewController.class 可以减少注册的代码量,但这样又难以覆盖所有场景。
注册所产生的内存占用并不是负担,主要是大量的注册可能会明显拖慢启动速度。
(三) Protocol 解耦
这种方式仍然要注册,使用一个全局的字典 (Protocol -> Class) 存储起来。
- (void)registerService:(Protocol *)service class:(Class)cls {
if (!service || !cls) return;
self.map[NSStringFromProtocol(service)] = cls;
}
- (id)getObject:(Protocol *)service {
if (!service) return nil;
Class cls = self.map[NSStringFromProtocol(service)];
id obj = [cls new];
if ([obj conformsToProtocol:service]) {
return obj;
}
return nil;
}
复制代码
定义一个协议服务:
@protocol CAimService <NSObject>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack;
@end
复制代码
用一个类实现协议并且注册协议:
@implementation CAimServiceProvider
+ (void)load {
[CMediator.share registerService:@protocol(CAimService) class:self];
}
#pragma mark - <CAimService>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
CAimController *vc = [CAimController new];
vc.name = name;
vc.callBack = callBack;
[UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
复制代码
至于为什么要使用一个单独的ServiceProvider
类,和前面“Runtime 解耦”为什么要定义一个Target
是一个道理。
使用起来很优雅:
id<CAimService> service = [CMediator.share getObject:@protocol(CAimService)];
[service gotoCAimControllerWithName:@"From C" callBack:^{
NSLog(@"CAim CallBack");
}];
复制代码
看起来这种方案不需要硬编码很舒服,但是它有个致命的问题 ——— 无法拦截所有路由方法。
这也就意味着这种方案做不了自动化动态调用。
阿里的 BeeHive 是目前的最佳实践。注册部分它可以将待注册的类字符串写入 Data 段,然后在 Image 加载的时候读取出来注册。这个操作只是将注册的执行放到了+load
方法之前,仍然会拖慢启动速度,所以这个处理并不是为了提速,而是将注册代码更加优雅的分散到具体业务方。
为什么 Protocol -> Class 和 Key -> Block 需要注册?
想象一下,解耦意味着调用方只有系统原生的标识,如何定位到目标业务? 必然有个映射。 而 runtime 可以直接调用目标业务,其它两种方式只有建立映射表。 当然 Protocol 方式也可以不建立映射表,直接遍历所有类,找出遵循这个协议的类也能找到,不过明显这样是低效且不安全的。
组件化总结
经过对比作者选择的方式是 target-action + runtime
本文Demo
参考文章
解读 iOS 组件化与路由的本质
网友评论