Comunicación entre componentes de iOS, otra implementación distintiva

Este artículo ha participado en la actividad "Ceremonia de creación de recién llegados", y juntos iniciaremos el camino hacia la creación de pepitas de oro.

Comunicación entre componentes, pero cualquier proyecto más grande tendrá un desarrollo modular e inevitablemente encontrará problemas de desacoplamiento y comunicación entre componentes hermanos.

¿Cómo pueden los módulos transmitir mensajes entre sí sin depender unos de otros? Hay muchas soluciones en línea, tales como:

  1. Enrutamiento de URL
  2. objetivo-acción
  3. protocolo

iOS: tres soluciones de comunicación para la componentización

También hay muchos representantes de componentes de terceros, MGJRouter, CTMediator, BeeHive, ZIKRouteretc.

Pero tienen más o menos sus propias ventajas y desventajas, y no entraré en detalles aquí, pero básicamente hay varios problemas:

  1. Es engorroso de usar, necesita comprender el costo y necesita escribir una gran cantidad de código redundante para desarrollar.
  2. Básicamente, primero debe registrarse y luego implementarlo. Entonces no hay garantía de que el código deba tener una implementación, ni puede garantizar si la implementación es inconsistente con el registro (por supuesto, puede agregar algunos métodos de verificación, como la detección estática). Esto es muy doloroso en proyectos relativamente grandes, o no te atreves a borrar el código histórico para acumular deudas, o eres temerario en el pasado, y hay problemas en testing o en línea [manual dog head].
  3. Si hay un modelo que debe pasarse, hundirse en el módulo público o transferir NSDictionary. O la deuda de la capa pública o los cambios en el modelo causan problemas de tiempo de ejecución.

¿Hay una bala de plata? Este es el método de implementación del que hablaré esta vez y resolveré el problema desde otro ángulo.

diferentes soluciones

A través de las preguntas anteriores, pensar en lo que queremos lograr:

  1. No hay necesidad de aumentar los costos de desarrollo, y no hay necesidad de comprender el principio de implementación general.
  2. Proporcionado por el proveedor del componente, primero hay una implementación y luego una definición para garantizar que la API esté completamente disponible. Si la implementación cambia, la persona que llama compilará e informará un error (antes de que se exponga el problema). Y otros módulos no dependen de él, pero pueden llamar a este método con precisión.
  3. Todo tipo de modelos se pueden usar normalmente en el módulo, y también se pueden usar normalmente cuando se exponen al mundo exterior, pero no es necesario hundirlos en el módulo público.

¿Se siente como pedir demasiado? Es como un cabrón que no quiere casarse contigo, pero también quiere tener hijos contigo [cabeza de perro manual].

但能不能实现呢,确实是可以的。但解决办法不在 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 脚本做中转即可。

imagen.png

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

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

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

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

imagen.png

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

imagen.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]; 即可。

零零碎碎

También es muy común en algunas bibliotecas de terceros y de terceros usar la regularidad para juzgar y tomar AST, pero es OCrealmente Además, hay muchos códigos históricos que no están estandarizados y hay varios espacios y comentarios La redacción de una adaptación general se considera Relativamente lenta.

También hay alguna compatibilidad personalizada, y también hay algunas situaciones codificadas de forma rígida, por ejemplo, el Modelo al que están asociados algunos componentes está en el marco, y se mantiene una tabla correspondiente @classpara la compatibilidad.

Hacer un seguimiento

El artículo (jing) y la longitud (li) son limitados, por lo que no lo explicaré más. Esta idea de implementación ha afectado muchos de los procesos de desarrollo posteriores del autor. Si está interesado, puede leer el artículo del autor sobre Flutter, que también usa codegen extensivamente.

Si tiene alguna pregunta, puede discutirla juntos en el área de comentarios.

No es fácil tocar a mano. Si tiene algo de inspiración para su estudio y trabajo, deje un me gusta, gracias por leer ~~

Supongo que te gusta

Origin juejin.im/post/7137240001112178702
Recomendado
Clasificación