本文已参与「新人创造礼」活动,一同开启创造之路。

组件间通讯,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通讯问题。

那如何不互相依赖模块,又能够彼此传输音讯呢?网上的计划是有许多了,比方:

  1. URL 路由
  2. target-action
  3. protocol

iOS:组件化的三种通讯计划 这篇写的挺不错,没了解的同学能够看一下

也有许多第三方组件代表,MGJRouterCTMediatorBeeHiveZIKRouter 等(排名不分前后[手动狗头])。

但他们或多或少都有各自的优缺点,这儿也不展开说,但根本上的有这么几种问题:

  1. 运用起来比较繁琐,需要了解本钱,开发起来也需要写许多冗余代码。
  2. 根本都需要先注册,再完成。那就无法确保代码必定存在完成,也无法确保完成是否跟注册出现不一致(当然你能够添加一些校验手法,比方静态检测之类的)。这一点在比较大型的项目里都是很痛的,要不就不敢删去前史代码来积债,要不便是莽曩昔,测验或者线上出现问题[手动狗头]。
  3. 假如存在 Model 需要传递,要不下沉到公共模块,要不便是转 NSDictionary。仍是公共层积债或者模型改变导致运行时出问题。

那有没有银弹呢?这便是本次要讲的完成办法,换个角度处理问题。

与众不同的计划

经过上述的问题,想一下咱们想要的完成是什么样:

  1. 不需要添加开发本钱,也不需要了解全体的完成原理。
  2. 由组件供给方供给,先有完成再有界说,确保 API 是彻底可用的,假如完成产生改变,调用方会编译时报错(问题露出前置)。且其他模块不依赖但又能够准确调用到这个办法。
  3. 各类模型在模块内是正常运用的,且对外露出也是能够正常运用的,但又不用去下沉在公共模块。

是不是感觉要求很过火?就像一个渣男既不想跟你成婚,又想跟你生孩子[手动狗头] 。

但能不能完成呢,的确是能够的。但处理办法不在 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 脚本做中转即可。

iOS 组件间通信,另一种与众不同的实现方式

这儿源码有些定制化完成,放出来现在也是徒增我们烦恼,所以讲一下生成要害进程:

  1. 遍历组件所在目录,取出所有的 .h 文件,缓存Map<文件途径,文件内容>(一级缓存)
  2. 解析存在 GDM_EXPORT_MODULE() 的办法,将办法的称号、参数、注释经过正则手法分解成相应的特点,存储到 Map<模块名,API 模型列表> (二级缓存)
  3. 关于每一个 API 模型进行进一步解析,解析入参和出参,判别参数类型是否为自界说类型(模型、署理、枚举、包含杂乱的 NSArray<CustomModel *> * 等),假如有存在,则遍历一级缓存,找到自界说类型的界说,生成对应的 Model -> Procotol 等,且存储在多个 Map 中 Map<类名/署理名/枚举名,详细解析后的模型>(三级缓存)

有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成进程,至于为什么是用的正则而不是 iOS 的 AST 东西,首要原因是想做的很轻,尽量减少我们的构建时长,不要经过编译来完成。

  1. 有了 AST 生成就变得很简单,模版代码 + 模版输出即可

iOS 组件间通信,另一种与众不同的实现方式

能够看到已经有大量模块生成了相应的 GDAPI

iOS 组件间通信,另一种与众不同的实现方式

履行时长在 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 的广泛运用。

假如有任何问题,都能够评论区一同讨论。

手敲不易,假如对你学习作业上有所启示,请留个赞, 感谢阅读 ~~