iOS 组件间通信,另一种与众不同的实现方式
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。
那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:
- URL 路由
- target-action
- protocol
iOS:组件化的三种通讯方案 这篇写的挺不错,没了解的同学可以看一下
也有很多第三方组件代表,MGJRouter
、CTMediator
、BeeHive
、ZIKRouter
等(排名不分前后[手动狗头])。
但他们或多或少都有各自的优缺点,这里也不展开说,但基本上的有这么几种问题:
- 使用起来比较繁琐,需要理解成本,开发起来也需要写很多冗余代码。
- 基本都需要先注册,再实现。那就无法保证代码一定存在实现,也无法保证实现是否跟注册出现不一致(当然你可以增加一些校验手段,比如静态检测之类的)。这一点在比较大型的项目里都是很痛的,要不就不敢删除历史代码来积债,要不就是莽过去,测试或者线上出现问题[手动狗头]。
- 如果存在 Model 需要传递,要不下沉到公共模块,要不就是转
NSDictionary
。还是公共层积债或者模型变更导致运行时出问题。
那有没有银弹呢?这就是本次要讲的实现方式,换个角度解决问题。
与众不同的方案
通过上述的问题,想一下我们想要的实现是什么样:
- 不需要增加开发成本,也不需要理解整体的实现原理。
- 由组件提供方提供,先有实现再有定义,保证 API 是完全可用的,如果实现发生变更,调用方会编译时报错(问题暴露前置)。且其他模块不依赖但又可以准确调用到这个方法。
- 各类模型在模块内是正常使用的,且对外暴露也是可以正常使用的,但又不用去下沉在公共模块。
是不是感觉要求很过分?就像一个渣男既不想跟你结婚,又想跟你生孩子[手动狗头] 。
但能不能实现呢,确实是可以的。但解决办法不在 iOS 本身,而在 codegen。铺垫到这里,我们来看看具体实现。
GDAPI 原理
在笔者所在的稿定,之前用的是 CTMediator
方案做组件间通信,当然也就有上面的那些问题,甚至线上也出现过因为 Protocol
找不到 Mediator
导致的线上 crash。
为了解决定义和实现不匹配的问题,我们希望定义一定要有实现,实现一定要跟定义一致。
那是否就可以换个思路,先有实现,再有定义,从实现生成定义。
这点参考了 JAVA 的注解机制,我们定义了一个宏 GDM_EXPORT_MODULE()
,用于说明哪些方法是需要开发给其他模块使用的。
// XXLoginManager.h
/// 判断是否登陆
- (BOOL)isLogin GDM_EXPORT_MODULE();
这样在组件开发方就完成了 API 开放,剩下的工作就是如何生成一个调用层代码。
调用层代码其实也就是 CTMediator
的翻版,通过 iOS 的运行时反射机制去寻找实现类
// XXService.m
static id<GDXXXAPI> _mXXXService = nil;
+ (id<GDXXXAPI>)XXXService {
if (_mXXXService == nil) {
_mXXXService = [self implementorOfName:@"GDXXXManager"];
}
return _mXXXService;
}
我们把这些生成的方法调用,生成到一个 GDAPI
模块统一存储,当然这个模块除了上述模块的 Service 层是要有具体的 .m
来做落地,其他都是 .h
的头文件。
那调用侧只需要 pod 增加依赖 s.dependency 'GDAPI/XXXXService'
即可调用到具体实现了
@import GDAPI;
...
bool isLogin = [GDAPI.XXService isLogin];
这里肯定有同学会问,生成过程呢???
笔者是用 Ruby
代码实现了整个 codegen 过程,当时没选择 Python
主要是为了跟 cocoapods
使用相同的开发语言,易于做侵入设计,但其实用其他语言都没问题,通过 shell
脚本做中转即可。
这里源码有些定制化实现,放出来现在也是徒增大家烦恼,所以讲一下生成关键过程:
- 遍历组件所在目录,取出所有的
.h
文件,缓存在Map<文件路径,文件内容>
(一级缓存) - 解析存在
GDM_EXPORT_MODULE()
的方法,将方法的名称、参数、注释通过正则手段分解成相应的属性,存储到Map<模块名,API 模型列表>
(二级缓存) - 对于每一个 API 模型进行进一步解析,解析入参和出参,判断参数类型是否为自定义类型(模型、代理、枚举、包括复杂的
NSArray<CustomModel *> *
等),如果有存在,则遍历一级缓存,找到自定义类型的定义,生成对应的 Model -> Procotol 等,且存储在多个 Map 中Map<类名/代理名/枚举名,具体解析后的模型>
(三级缓存) - 有了 AST 生成就变得很简单,模版代码 + 模版输出即可
有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。0
可以看到已经有大量模块生成了相应的 GDAPI
执行时长在 2S 左右,因为有一个预执行的过程,来做组件项目化,这个也算是特殊实现了。
实质上执行也就 1S 即可。
还有一点要说的是执行时机是在 pod install / update
之前,这个是通过 hooks cocoapods 的执行过程做到的。
一些难点
嵌套模型
上面虽然粗略的讲了下 Model / Procotol 会生成 Protocol,但其实这一部分确实是最困难的,也是因为历史积债问题,下沉在公共模块的庞大的模型在各个组件里传输。
那要把它完全的 API 化,就需要对它的属性进行递归解析,生成完全符合的 protocol
例如:
... 举例为伪代码,OC 代码确实很啰嗦
class A extends B {
C c;
NSArray<D> d;
}
/// 测试
- (void)test:(A *)a GDM_EXPORT_MODULE();
生成结果就如下图(伪代码):
@protocol GDAPI_A {
NSObject<GDAPI_C> c;
NSArray<NSObject<GDAPI_D>> d;
}
@protocol GDAPI_B {
}
@protocol GDAPI_C {
}
@protocol GDAPI_D {
}
以及调用服务
@protocol GDXXXAPI <NSObject>
/// 测试
- (void)test:(NSObject<GDAPI_A, GDAPI_B>)a;
这个在落地过程中坑确实非常多。
B 模块想创建 A 模块的模型
当然这个是很不合理的,但现实中确实很多这样的历史问题。
当然也不能用模型下沉开倒车,那解决上用了一个巧劲
/// 创建 XX
- (XXXModel *)createXXX GDM_EXPORT_MODULE();
提供一个创建模型的 API 给外部使用,这样对于 Model 的管理还是在模块内,外部模块使用上从 new XXX()
改为 [GDAPI.XXService createXX];
即可。
零零碎碎
用正则判断抓取 AST,在一些二三方库中也是很常见的,但来处理 OC
确实挺痛苦的,再加上历史代码很多没什么规范,空格、注释各式各样,写个通用的适配算是比较耗时的。
还有就是一些个性化的兼容,也存在一些硬编码的情况,比如有些组件去关联到的 Model 在 framework 中,维护一个对应表,用 @class
来兼容解决。
后续
篇(jing)幅(li)有限,就不再展开说明,这个实现思路影响了笔者后续的很多开发过程,有兴趣可以看下笔者 Flutter 的文章,里面也是 codegen 的广泛运用。
如果有任何问题,都可以评论区一起讨论。
手敲不易,如果对你学习工作上有所启发,请留个赞, 感谢阅读 ~~
链接:https://juejin.cn/post/7137240001112178702
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。