Communication between iOS components, another distinctive implementation

This article has participated in the "Newcomer Creation Ceremony" activity, and together we will start the road to gold nugget creation.

Communication between components, but any larger project will do modular development, and it will inevitably encounter problems of decoupling and communication between sibling components.

How can modules transmit messages to each other without relying on each other? There are many solutions online, such as:

  1. URL routing
  2. target-action
  3. protocol

iOS: three communication solutions for componentization

There are also many representatives of third-party components, MGJRouter, CTMediator, BeeHive, ZIKRouteretc.

But they more or less have their own advantages and disadvantages, and I won’t go into details here, but basically there are several problems:

  1. It is cumbersome to use, needs to understand the cost, and needs to write a lot of redundant code to develop.
  2. Basically, you need to register first and then implement it. Then there is no guarantee that the code must have an implementation, nor can it guarantee whether the implementation is inconsistent with the registration (of course you can add some verification methods, such as static detection). This is very painful in relatively large projects. Either you dare not delete the historical code to accumulate debts, or you are reckless in the past, and there are problems in testing or online [manual dog head].
  3. If there is a Model that needs to be passed, either sink to the public module, or transfer NSDictionary. Or public layer debt or model changes cause runtime problems.

Is there a silver bullet? This is the implementation method I will talk about this time, and solve the problem from another angle.

different solutions

Through the above questions, think about what we want to achieve:

  1. There is no need to increase development costs, and there is no need to understand the overall implementation principle.
  2. Provided by the component provider, there is an implementation first and then a definition to ensure that the API is fully available. If the implementation changes, the caller will compile and report an error (before the problem is exposed). And other modules do not depend on it but can call this method accurately.
  3. All kinds of models can be used normally in the module, and can also be used normally when exposed to the outside world, but there is no need to sink them into the public module.

Does it feel like asking too much? It's like a scumbag who doesn't want to marry you, but also wants to have children with you [manual dog head].

但能不能实现呢,确实是可以的。但解决办法不在 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]; 即可。

零零碎碎

It is also very common in some second-party and third-party libraries to use regularity to judge and grab AST, but it is OCreally . In addition, there are many historical codes that are not standardized, and there are various spaces and comments. Writing a general adaptation is considered Relatively time-consuming.

There are also some personalized compatibility, and there are also some hard-coded situations. For example, the Model to which some components are associated is in the framework, and a corresponding table is maintained @classfor compatibility.

Follow up

The article (jing) and the length (li) are limited, so I won’t explain it anymore. This implementation idea has affected many of the author’s subsequent development processes. If you are interested, you can read the author’s article on Flutter, which also uses codegen extensively.

If you have any questions, you can discuss them together in the comment area.

It's not easy to knock by hand. If you have some inspiration for your study and work, please leave a like, thank you for reading~~

Guess you like

Origin juejin.im/post/7137240001112178702