iOS - componentization - component communication scheme

Introduction

In the componentization scheme, the components are layered and decoupled. The upper-level business components depend on the lower-level basic components and cannot be reversed. However, components on the same level need to use component communication solutions to avoid interdependence.

The component communication solution solves the problem of calling each other between components of the same layer, and does not generate code dependencies at the compilation level.

  • A module can be understood as a high cohesion, low coupling business or function set
  • Components are more inclined to reusable logic

  • Subscribe, edit, journal belong to the business layer
  • Markdown functions and advanced tools belong to the basic business layer
  • The network library belongs to the basic component layer

The upper layer is directly dependent on the lower layer, and the lower layer cannot be reversely dependent on the upper layer. Subscription, editing, and journaling are direct calls to the same-layer business, and component communication solutions need to be used to decouple the same layer.

Since decoupling is to uncouple the coupling of compilation dependencies, it is necessary to find a calling method other than direct code calling according to the characteristics of the language and system library, which can be regarded as an indirect call. The advantages of direct code calling, compilation inspection, and execution efficiency, the disadvantage is strong coupling. From the perspective of decoupling, it is easy to think of using the Protocol protocol for decoupling. From the perspective of dynamic capabilities, there is also a flexible decoupling method based on Runtime.

There are several implementation schemes. Using Protocol-Service to implement the three-party library mainly includes the BeeHive library, while CTMediator uses the Target-Action method

  • Protocol-Service
  • Target-Action

Protocol-Service

There is a one-to-one relationship between protocol and service. Protocol is used to declare the dependency interface between components, and service implements the corresponding capabilities provided by protocol. Among them, different protocols need a mapping relationship to be implemented by which service, which can also be regarded as a protocol-service mapping table.

The capability provider implements the corresponding protocol and registers it in the mapping table. The caller needs to use the ability of the protocol to find the corresponding service implementation from the table to complete the call

BeeHive

In addition to providing the communication function between components, BeeHive also provides functions such as module registration, application and module life cycle event distribution. Here the component communication capabilities are mainly analyzed.

use

Declare the interface Protocol that needs to be provided, inherited from BHServiceProtocol

@protocol HomeServiceProtocol <NSObject, BHServiceProtocol>

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

@end

registration service

dynamic registration

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

static registration

<?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>

transfer

获取到对应的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 组件化方案探索

Guess you like

Origin juejin.im/post/7238185960059748411