iOS - componenteización - esquema de comunicación de componentes

Introducción

En el esquema de componentes, los componentes están en capas y desacoplados. Los componentes comerciales de nivel superior dependen de los componentes básicos de nivel inferior y no se pueden revertir. Sin embargo, los componentes en el mismo nivel deben usar soluciones de comunicación de componentes para evitar la interdependencia.

La solución de comunicación de componentes resuelve el problema de llamadas entre componentes de la misma capa y no genera dependencias de código a nivel de compilación.

  • Un módulo puede entenderse como un conjunto de funciones o negocios de alta cohesión y bajo acoplamiento.
  • Los componentes se inclinan más por la lógica reutilizable

  • Suscribirse, editar, diario pertenecer a la capa empresarial
  • Las funciones de Markdown y las herramientas avanzadas pertenecen a la capa empresarial básica
  • La biblioteca de red pertenece a la capa de componentes básicos.

La capa superior depende directamente de la capa inferior, y la capa inferior no puede depender inversamente de la capa superior. La suscripción, la edición y el registro en diario son llamadas directas al negocio de la misma capa, y las soluciones de comunicación de componentes deben usarse para desacoplar la misma capa.

Dado que el desacoplamiento consiste en desacoplar el acoplamiento de las dependencias de compilación, es necesario encontrar un método de llamada que no sea la llamada directa al código de acuerdo con las características del idioma y la biblioteca del sistema, que se puede considerar como una llamada indirecta. Las ventajas de la llamada directa de código, la inspección de compilación y la eficiencia de ejecución, la desventaja es un fuerte acoplamiento. Desde la perspectiva del desacoplamiento, es fácil pensar en usar el protocolo Protocol para el desacoplamiento.Desde la perspectiva de las capacidades dinámicas, también existe un método de desacoplamiento flexible basado en Runtime.

Hay varios esquemas de implementación.El uso de Protocol-Service para implementar la biblioteca de tres partes incluye principalmente la biblioteca BeeHive, mientras que CTMediator usa el método Target-Action.

  • Servicio de protocolo
  • Acción objetivo

Servicio de protocolo

Existe una relación de uno a uno entre el protocolo y el servicio. El protocolo se utiliza para declarar la interfaz de dependencia entre los componentes, y el servicio implementa las capacidades correspondientes proporcionadas por el protocolo. Entre ellos, diferentes protocolos necesitan una relación de mapeo para ser implementada por qué servicio, que también puede considerarse como una tabla de mapeo de protocolo-servicio.

El proveedor de capacidad implementa el protocolo correspondiente y lo registra en la tabla de mapeo. La persona que llama necesita usar la capacidad del protocolo para encontrar la implementación de servicio correspondiente de la tabla para completar la llamada.

Colmena

Además de proporcionar la función de comunicación entre componentes, BeeHive también proporciona funciones como el registro de módulos, la aplicación y la distribución de eventos del ciclo de vida del módulo. Aquí se analizan principalmente las capacidades de comunicación de los componentes.

usar

Declare el protocolo de interfaz que debe proporcionarse, heredado de BHServiceProtocol

@protocol HomeServiceProtocol <NSObject, BHServiceProtocol>

-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName;

@end

servicio de registro

registro dinámico

[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

registro estático

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
        <dict>
                <key>HomeServiceProtocol</key>
                <string>BHViewController</string>
        </dict>
</plist>

transferir

获取到对应的Service,调用协议中声明的接口

#import "BHService.h"
id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

// use homeVc do invocation

原理

Protocol-Service表

  • key是protocol,value是protocolImplClass
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
    [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
注册

写入到Mach-O中DATA段

  • Mach-O的DATA段是可读可写的
  • Service信息,写入到Mach-O中DATA段
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ ""#servicename"" : ""#impl""}";

从Mach-O中DATA段读取存储的信息

NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp);
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
    
    unsigned long counter = size/sizeof(void*);
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;
        
        BHLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }
    
    return configs;    
}

attribute((constructor))中添加_dyld_register_func_for_add_image回调

  • attribute((constructor))在main函数之前被调用

  • 虽然此时dyld相关的操作比如add_image的已经完成,_dyld_register_func_for_add_image的回调有2种情况

    • 一个是在add_image的时机
    • 如果注册回调的时候add_image已经完成,也会直接回调
__attribute__((constructor))
void init() {
    _dyld_register_func_for_add_image(dyld_callback);
}

dyld_callback回调函数中,完成动态注册

static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
    NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
    for (NSString *modName in mods) {
        Class cls;
        if (modName) {
            cls = NSClassFromString(modName);
            
            if (cls) {
                [[BHModuleManager sharedManager] registerDynamicModule:cls];
            }
        }
    }
    
    //register services
    NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
    for (NSString *map in services) {
        NSData *jsonData =  [map dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error = nil;
        id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
        if (!error) {
            if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
                
                NSString *protocol = [json allKeys][0];
                NSString *clsName  = [json allValues][0];
                
                if (protocol && clsName) {
                    [[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
                }
                
            }
        }
    }
}
注册时机

BeeHive中也使用静态注册的方式,使用plist文件。动态注册是使用了Annotation注解的方式,结合在Mach-O文件的__DATA段存储,之后在_dyld_register_func_for_add_image的回调中,读取对应的数据再注册到映射表中

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <dict>
        <key>service</key>
        <string>UserTrackServiceProtocol</string>
        <key>impl</key>
        <string>BHUserTrackViewController</string>
    </dict>
</array>
</plist>

纯Swift的项目主要有几个问题,一个是难以对Mach-O直接操作,还有一个是无法灵活获得反射关系,实例,类,protocol与父protocol的关系。对于无法直接使用Mach-O的DATA段实现Annotation的问题,我们只能把注册时机后移到Application -didFinishLaunchingWithOptions阶段。只要service与protocol的绑定,在service调用之前完成。

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Codes
        ServiceManager.shared.register(service: "(HomeServiceProtocol.self)", implClass: HomeServiceImpelementor.self)
        // Codes
    }
获取
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
    if (!serviceName.length) {
        serviceName = NSStringFromProtocol(service);
    }
    id implInstance = nil;
    
    if (![self checkValidService:service]) {
        if (self.enableException) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
        }
        
    }
    
    NSString *serviceStr = serviceName;
    if (shouldCache) {
        id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
        if (protocolImpl) {
            return protocolImpl;
        }
    }
    
    Class implClass = [self serviceImplClass:service];
    if ([[implClass class] respondsToSelector:@selector(singleton)]) {
        if ([[implClass class] singleton]) {
            if ([[implClass class] respondsToSelector:@selector(shareInstance)])
                implInstance = [[implClass class] shareInstance];
            else
                implInstance = [[implClass alloc] init];
            if (shouldCache) {
                [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
                return implInstance;
            } else {
                return implInstance;
            }
        }
    }
    return [[implClass alloc] init];
}

Target-Action

Target指的是需要交互的对象,Action指的是Target中被调用的方法,Target-Action模式在iOS开发中使用广泛。得益于OC的动态特性,使用和实现都很方便。

使用

创建提供给外部依赖的module,在module中给CTMediator类添加分类

@interface CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail;

@end

NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativeFetchDetailViewController = @"nativeFetchDetailViewController";

@implementation CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativeFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界选择是push还是present
        return viewController;
    } else {
        // 这里处理异常场景,具体如何处理取决于产品
        return [[UIViewController alloc] init];
    }
}
@end

在内部实现的module中,提供对应的Target,实现对应的Action

#import "Target_A.h"
@implementation Target_A
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}
@end

调用的模块,引入刚才对外提供的module,不用引入实际实现Target_Action的module,直接调用

#import "CTMediator+CTMediatorModuleAActions.h"
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
    [self.navigationController pushViewController:viewController animated:YES];
}
@end

原理

CTMediator中的中间件代码非常简短,主要是对Target_Action的解析调用

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // generate action
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}

结构关系

适配层

从调用者的角度来说,由于模块外部依赖的不稳定性,通常会设计适配层(adapter)来隔离外部可能的api变更导致的内部多处调用都需要修改的情况。

模块中适配层要做的事情,举例来说无论是依赖SDWebImage还是Kingfisher实现图片加载,对业务模块内部都适配成loadImage一类的接口,外部接口不会对内部造成侵入。

调停者

原本各个module的Protocol都集中在ServiceLib库中,或者以文件夹的方式,或者以subspec的方式做区分。所以所有的module都只需要依赖ServiceLib即可,但是对于ServiceLib这个基础组件来说,它对应的指责和代码权限都不应该开放给所有业务线,Protocol作为各个module对外提供的能力是跟业务相关的能力和代码,并不适合放在Lib这样的基础组件中。

ServiceLib作为一个基础组件,不应该有业务代码的侵入,从而演变为以下结构。对于Target-Action方案,从调用规范的角度,把CTMediator扩展的分类设计成单独的module,提供给外部依赖。

Protocol都放在每个Extension的库中,由对应的业务域负责模块维护。由于Service需要实现Protocol,这边的业务库会对Extension有依赖关系。

引用

BeeHive Github

CTMediator Github

iOS 组件化方案探索

Supongo que te gusta

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