iOS コンポーネント間の通信、もう 1 つの特徴的な実装

この記事は「新人創造式」活動に参加しており、一緒に金塊創造への道を歩み始めます。

コンポーネント間の通信ですが、大規模なプロジェクトではモジュラー開発が行われ、兄弟コンポーネント間の分離と通信の問題が必然的に発生します。

モジュールが互いに依存せずにメッセージを送信するにはどうすればよいでしょうか? オンラインには、次のような多くのソリューションがあります。

  1. URL ルーティング
  2. ターゲット アクション
  3. プロトコル

iOS:コンポーネント化のための 3 つの通信ソリューション

MGJRouter、、、などCTMediatorサードパーティ製コンポーネントの代表も多数ありますBeeHiveZIKRouter

しかし、それらには多かれ少なかれ独自の長所と短所があり、ここでは詳細には触れませんが、基本的にいくつかの問題があります。

  1. 使用するのが面倒で、コストを理解する必要があり、開発のために冗長なコードをたくさん書く必要があります。
  2. 基本的には登録してから実装する必要があります。その場合、コードに実装が必要であるという保証はありません。また、実装が登録と矛盾しているかどうかも保証できません (もちろん、静的検出などの検証方法を追加できます)。これは比較的大規模なプロジェクトでは非常に苦痛であり、負債を蓄積するために履歴コードをあえて削除しないか、過去に無謀であり、テストまたはオンラインに問題がある [手動ドッグ ヘッド] です。
  3. 渡す必要があるモデルがある場合は、パブリック モジュールにシンクするか、または転送しNSDictionaryます。または、パブリック層の負債またはモデルの変更により、実行時の問題が発生します。

銀の弾丸はありますか?これが今回お話しする実装方法で、別の角度から問題を解決します。

さまざまなソリューション

上記の質問を通して、達成したいことを考えてください。

  1. 開発コストを増やす必要はなく、全体的な実装原理を理解する必要もありません。
  2. Provided by the component provider, there is an implementation first and then a definition to ensure that the API is fully available. 実装が変更された場合、呼び出し元はコンパイルしてエラーを報告します (問題が明らかになる前に)。また、他のモジュールはそれに依存しませんが、このメソッドを正確に呼び出すことができます。
  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 脚本做中转即可。

画像.png

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

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

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

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

画像.png

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

画像.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本当にです. また、標準化されていない歴史的なコードが多く、さまざまなスペースとコメント. 一般的な適応を書くことは、比較的時間がかかると考えられています.

また、一部のパーソナライズされた互換性があり、一部のハードコーディングされた状況もあります. たとえば、一部のコンポーネントが関連付けられているモデルはフレームワーク内にあり、対応するテーブルは互換性@classのためにます.

ファローアップ

記事 (jing) と長さ (li) は限られているので、これ以上は説明しません. この実装のアイデアは、著者のその後の開発プロセスの多くに影響を与えました. 興味がある場合は、Flutter に関する著者の記事を読むことができます. codegenも広く使用しています。

質問がある場合は、コメント欄で一緒に話し合うことができます。

手でノックするのは簡単ではありません.勉強や仕事にインスピレーションがあれば、いいねを残してください,読んでくれてありがとう~~

おすすめ

転載: juejin.im/post/7137240001112178702