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

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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

那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:

  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 脚本做中转即可。

image.png

这里源码有些定制化实现,放出来现在也是徒增大家烦恼,所以讲一下生成关键过程:

  1. 遍历组件所在目录,取出所有的 .h 文件,缓存在 Map<文件路径,文件内容>(一级缓存)
  2. 解析存在 GDM_EXPORT_MODULE() 的方法,将方法的名称、参数、注释通过正则手段分解成相应的属性,存储到 Map<模块名,API 模型列表> (二级缓存)
  3. 对于每一个 API 模型进行进一步解析,解析入参和出参,判断参数类型是否为自定义类型(模型、代理、枚举、包括复杂的 NSArray<CustomModel *> * 等),如果有存在,则遍历一级缓存,找到自定义类型的定义,生成对应的 Model -> Procotol 等,且存储在多个 Map 中 Map<类名/代理名/枚举名,具体解析后的模型>(三级缓存)

有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。

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

image.png

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

image.png

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

如果有任何问题,都可以评论区一起讨论。

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

猜你喜欢

转载自juejin.im/post/7137240001112178702