この記事は「新人創造式」活動に参加しており、一緒に金塊創造への道を歩み始めます。
コンポーネント間の通信ですが、大規模なプロジェクトではモジュラー開発が行われ、兄弟コンポーネント間の分離と通信の問題が必然的に発生します。
モジュールが互いに依存せずにメッセージを送信するにはどうすればよいでしょうか? オンラインには、次のような多くのソリューションがあります。
- URL ルーティング
- ターゲット アクション
- プロトコル
iOS:コンポーネント化のための 3 つの通信ソリューション
MGJRouter
、、、などCTMediator
のサードパーティ製コンポーネントの代表も多数あります。BeeHive
ZIKRouter
しかし、それらには多かれ少なかれ独自の長所と短所があり、ここでは詳細には触れませんが、基本的にいくつかの問題があります。
- 使用するのが面倒で、コストを理解する必要があり、開発のために冗長なコードをたくさん書く必要があります。
- 基本的には登録してから実装する必要があります。その場合、コードに実装が必要であるという保証はありません。また、実装が登録と矛盾しているかどうかも保証できません (もちろん、静的検出などの検証方法を追加できます)。これは比較的大規模なプロジェクトでは非常に苦痛であり、負債を蓄積するために履歴コードをあえて削除しないか、過去に無謀であり、テストまたはオンラインに問題がある [手動ドッグ ヘッド] です。
- 渡す必要があるモデルがある場合は、パブリック モジュールにシンクするか、または転送し
NSDictionary
ます。または、パブリック層の負債またはモデルの変更により、実行時の問題が発生します。
銀の弾丸はありますか?これが今回お話しする実装方法で、別の角度から問題を解決します。
さまざまなソリューション
上記の質問を通して、達成したいことを考えてください。
- 開発コストを増やす必要はなく、全体的な実装原理を理解する必要もありません。
- Provided by the component provider, there is an implementation first and then a definition to ensure that the API is fully available. 実装が変更された場合、呼び出し元はコンパイルしてエラーを報告します (問題が明らかになる前に)。また、他のモジュールはそれに依存しませんが、このメソッドを正確に呼び出すことができます。
- すべての種類のモデルは、モジュール内で正常に使用でき、外部に公開された場合も正常に使用できますが、パブリック モジュールにシンクする必要はありません。
求めすぎって感じ?あなたと結婚したくないが、あなたと子供が欲しい[手動の犬の頭].
但能不能实现呢,确实是可以的。但解决办法不在 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 (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。
- 有了 AST 生成就变得很简单,模版代码 + 模版输出即可
可以看到已经有大量模块生成了相应的 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
本当にです. また、標準化されていない歴史的なコードが多く、さまざまなスペースとコメント. 一般的な適応を書くことは、比較的時間がかかると考えられています.
また、一部のパーソナライズされた互換性があり、一部のハードコーディングされた状況もあります. たとえば、一部のコンポーネントが関連付けられているモデルはフレームワーク内にあり、対応するテーブルは互換性@class
のためにます.
ファローアップ
記事 (jing) と長さ (li) は限られているので、これ以上は説明しません. この実装のアイデアは、著者のその後の開発プロセスの多くに影響を与えました. 興味がある場合は、Flutter に関する著者の記事を読むことができます. codegenも広く使用しています。
質問がある場合は、コメント欄で一緒に話し合うことができます。
手でノックするのは簡単ではありません.勉強や仕事にインスピレーションがあれば、いいねを残してください,読んでくれてありがとう~~