洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

      • iOS组件化系列
        • URL Scheme路由
          • MGJRouter
        • Target - Action
          • CTMediator
        • protocol-class
          • Protocol - Class简单示例
          • BeeHive
        • 组件化架构实践规范和原则
          • 组建拆分原则
          • 基础/业务组件拆分原则
      • MVVM、MVC、MVP
    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • 组件化系列跟架构
洋仔
2023-08-12
目录

iOS组件化系列

# 常⽤的三种组件化通讯方案

组件化通信方案 组件化最重要的是兄弟模块的通讯 常⽤的三种方案

  • URL Scheme
  • Target - Action
  • Protocol - Class 匹配

# URL Scheme路由

  • 使 URL 处理本地的跳转
  • 通过中间层进⾏注册 & 调⽤ (load方法里把被调用者注册到中间层)
  • 注册表⽆需使用反射
  • 非懒加载 / 注册表的维护 / 参数
点击查看
//MTMediator.h --- start
typedef void(^MTMediatorProcessBlock)(NSDictionary *params);

+ (void)registerScheme:(NSString *)scheme processBlock:(MTMediatorProcessBlock)processBlock;

+ (void)openUrl:(NSString *)url params:(NSDictionary *)params;
//MTMediator.h --- end

//MTMediator.m --- start
+ (NSMutableDictionary *)mediatorCache{
    static NSMutableDictionary *cacheScheme;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cacheScheme = @{}.mutableCopy;
    });

    return cacheScheme;
}

+ (void)registerScheme:(NSString *)scheme processBlock:(MTMediatorProcessBlock)processBlock{
    if (scheme.length > 0 && processBlock) {
        [[[self class] mediatorCache] setObject:processBlock forKey:scheme];
    }
}

+ (void)openUrl:(NSString *)url params:(NSDictionary *)params{
    MTMediatorProcessBlock block = [[[self class] mediatorCache] objectForKey:url];
    if (block) {
        block(params);
    }
}
//MTMediator.m --- end

//注册 --- start
+ (void)load {
    [MTMediator registerScheme:@"detail://" processBlock:^(NSDictionary * _Nonnull params) {
        NSString *url = (NSString *)[params objectForKey:@"url"];
        UINavigationController *navigationController = (UINavigationController *)[params objectForKey:@"controller"];
        MTDetailViewController *controller = [[MTDetailViewController alloc] initWithUrlString:url];
//        controller.title = [NSString stringWithFormat:@"%@", @(indexPath.row)];
        [navigationController pushViewController:controller animated:YES];
    }];
}
//注册 --- end

//调用 --- start
//URL Scheme
[MTMediator openUrl:@"detail://" params:@{@"url":item.articleUrl,@"controller":self.navigationController}];
//调用 --- end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

提示

说明:

参考了系统URL Scheme机制 参数传递通过dictionary,对调用者不透明

目前iOS上大部分路由工具都是基于URL匹配的,或者是根据命名约定,用runtime方法进行动态调用

这些动态化的方案的优点是实现简单,缺点是需要维护字符串表,或者依赖于命名约定,无法在编译时暴露出所有问题,需要在运行时才能发现错误。

# MGJRouter

URL路由方式主要是以蘑菇街为代表的的 MGJRouter-swift (opens new window) MGJRoute-oc (opens new window) 其实现思路是:

App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。

// 1、注册某个URL
MGJRouter.registerURLPattern("app://home") { (info) in
    print("info: (info)")
}

//2、调用路由
MGJRouter.openURL("app://home")

1
2
3
4
5
6
7
8

# URL 路由的优点

  • 极高的动态性,适合经常开展运营活动的app,例如电商
  • 方便地统一管理多平台的路由规则
  • 易于适配URL Scheme

# URl 路由的缺点

  • 传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都是通过字符串转换而来
  • 只适用于界面模块,不适用于通用模块
  • 参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。
  • 不支持storyboard
  • 依赖于字符串硬编码,难以管理,蘑菇街做了个后台专门管理。
  • 无法保证所使用的的模块一定存在
  • 解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大

# Target - Action

  • 抽离业务逻辑
  • 通过中间层进行调⽤
  • 中间层使⽤ runtime 反射
  • 中间层代码优化
点击查看
//MTMediator.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface MTMediator : NSObject

//target action
+ ( __kindof UIViewController *)detailViewControllerWithUrl:(NSString *)detailUrl;

@end

NS_ASSUME_NONNULL_END

//MTMediator.m
#import "MTMediator.h"

@implementation MTMediator

+ ( __kindof UIViewController *)detailViewControllerWithUrl:(NSString *)detailUrl{
    Class detailVC = NSClassFromString(@"MTDetailViewController");
    UIViewController *controller = [[detailVC alloc] performSelector:NSSelectorFromString(@"initWithUrlString:") withObject:detailUrl];

    return controller;
}

@end

//调用 
//Target - Action
 UIViewController *vc = [MTMediator detailViewControllerWithUrl:item.articleUrl];
 vc.title = @"详情啊";
 [self.navigationController pushViewController:vc animated:YES];
 
说明:

硬编码方式(直接调用,不利于维护和扩展)
perform 最多能传递2个参数,可以传入字典避免参数过多 
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2; 
initWithUrlString:方法必须实现 否则找不到sel崩溃
业务逻辑柔合在Mediator中,可以各个模块写各自的MTMediator扩展


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# CTMediator

三方框架其主要的代表框架是casatwy的CTMediator (opens new window) 这个方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector + NSInvocation动态调用方法

其实现思路是:

  1. 利用分类为路由添加新接口,在接口中通过字符串获取对应的类
  2. 通过runtime创建实例,动态调用实例的方法

CTMediator简单使用:

点击查看
//******* 1、分类定义新接口
extension CTMediator{
    @objc func A_showHome()->UIViewController?{
    
        //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
        let params = [
            kCTMediatorParamsKeySwiftTargetModuleName: "CJLBase_Example"
        ]
        //CTMediator提供的performTarget:action:params:shouldCacheTarget:方法 通过传入name,找到对应的targer和action
        if let vc = self.performTarget("A", action: "Extension_HomeViewController", params: params, shouldCacheTarget: false) as? UIViewController{
            return vc
        }
        return nil
    }
}

//******* 2、模块提供者提供target-action的调用方式(对外需要加上public关键字)
class Target_A: NSObject {
    
    @objc func Action_Extension_HomeViewController(_ params: [String: Any])->UIViewController{
         
        let home = HomeViewController()
        return home
    }

}

//******* 3、使用
if let vc = CTMediator.sharedInstance().A_showHome() {
            self.navigationController?.pushViewController(vc, animated: true)
        }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

其模块间的引用关系如下图所示:

Screenshot-2023-08-27-at-08

# 优点

  • 利用 分类 可以明确声明接口,进行编译检查
  • 实现方式轻量

# 缺点

  • 需要在mediator 和 target中重新添加每一个接口,模块化时代码较为繁琐
  • 在 category 中仍然引入了字符串硬编码,内部使用字典传参,一定程度上也存在和 URL 路由相同的问题
  • 无法保证使用的模块一定存在,target在修改后,使用者只能在运行时才能发现错误
  • 可能会创建过多的 target 类

# CTMediator源码分析

通过上面CTMediator简单示例的分类所调用的performTarget来到CTMediator中的具体实现,即performTarget:action:params:shouldCacheTarget:,主要是通过传入的name,找到对应的target 和 action

点击查看
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target 生成target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        //swift中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        //OC中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //缓存中查找target
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    //缓存中没有target
    if (target == nil) {
        //通过字符串获取对应的类
        Class targetClass = NSClassFromString(targetClassString);
        //创建实例
        target = [[targetClass alloc] init];
    }

    // generate action 生成action方法名称
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    //通过方法名字符串获取对应的sel
    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];
    }
    //是否响应sel
    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;
        }
    }
}
// 进入safePerformAction:target:params:实现,主要是通过invocation进行参数传递+消息转发
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    //获取方法签名
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    //获取方法签名中的返回类型,然后根据返回值完成参数传递
    const char* retType = [methodSig methodReturnType];
    //void类型
    if (strcmp(retType, @encode(void)) == 0) {
        ...
    }
    //...省略其他类型的判断
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

# protocol-class

  • 增加 Protocol Wrapper层 (中间件先注册Protocol和Class对应关系,将protocol和对应的类进行字典匹配)
  • 中间件返回 Protocol 对应的 Class,然后动态创建实例
  • 解决硬编码的问题

# Protocol - Class简单示例

点击查看
//具体的Protocol
//MTMediator.h --- start
@protocol MTDetailViewControllerProtocol <NSObject>

+ (__kindof UIViewController *)detailViewControllerWithUrl:(NSString *)detailUrl;

@end

@interface MTMediator : NSObject
+ (void)registerProtol:(Protocol *)protocol class:(Class)cls;
+ (Class)classForProtocol:(Protocol *)protocol;
@end
//MTMediator.h --- end

//MTMediator.m --- start
+ (void)registerProtol:(Protocol *)protocol class:(Class)cls{
    if (protocol && cls) {
        [[[self class] mediatorCache] setObject:cls forKey:NSStringFromProtocol(protocol)];
    }
}

+ (Class)classForProtocol:(Protocol *)protocol{
    return [[[self class] mediatorCache] objectForKey:NSStringFromProtocol(protocol)];
}
//MTMediator.m --- end

//被调用
//MTDetailViewController.h --- start
@protocol MTDetailViewControllerProtocol;

@interface MTDetailViewController : UIViewController<MTDetailViewControllerProtocol>
@end
//MTDetailViewController.h --- end

//MTDetailViewController.m --- start
+ (void)load {
    [MTMediator registerProtol: @protocol(MTDetailViewControllerProtocol) class:[self class]];
}

#pragma mark - MTDetailViewControllerProtocol
+ ( __kindof UIViewController *)detailViewControllerWithUrl:(NSString *)detailUrl{
    return [[MTDetailViewController alloc]initWithUrlString:detailUrl];
}
//MTDetailViewController.m --- end

//调用
Class cls = [MTMediator classForProtocol: @protocol(MTDetailViewControllerProtocol)];
if ([cls respondsToSelector: @selector(detailViewControllerWithUrl:)]) {
        [self.navigationController pushViewController:[cls detailViewControllerWithUrl:item.articleUrl] animated:YES];
}

//说明:被调用者先在中间件注册Protocol和Class对应关系,对外只暴漏Protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# BeeHive

protocol比较典型的三方框架就是阿里的BeeHive (opens new window)。BeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能、基础功能模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务。

BeeHive 核心思想

  1. 各个模块间调用从直接调用对应模块,变成调用Service的形式,避免了直接依赖。
  2. App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在。

示例如下:

点击查看
//******** 1、注册
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

//******** 2、使用
#import "BHService.h"

id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

1
2
3
4
5
6
7
8

优点

  1. 利用接口调用,实现了参数传递时的类型安全
  2. 直接使用模块的protocol接口,无需再重复封装

缺点

  1. 用框架来创建所有对象,创建方式不同,即不支持外部传入参数
  2. 用OC runtime创建对象,不支持swift
  3. 只做了protocol 和 class 的匹配,不支持更复杂的创建方式 和依赖注入
  4. 无法保证所使用的protocol 一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块

除了BeeHive,还有Swinject

# BeeHive 模块注册

在BeeHive主要是通过BHModuleManager来管理各个模块的。BHModuleManager中只会管理已经被注册过的模块。 BeeHive提供了三种不同的注册形式,annotation,静态plist,动态注册。Module、Service之间没有关联,每个业务模块可以单独实现Module或者Service的功能。

# Annotation方式注册

这种方式主要是通过BeeHiveMod宏进行Annotation标记

提示

//***** 使用
BeeHiveMod(ShopModule)

//***** BeeHiveMod的宏定义
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

//***** BeeHiveDATA的宏定义 
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

//*****  全部转换出来后为下面的格式 以name是ShopModule为例

char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里针对__attribute需要说明以下几点

第一个参数used:用来修饰函数,被used修饰以后,意味着即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器下会去掉没有被引用的段。 通过使用__attribute__((section("name")))来指明哪个段。数据则用__attribute__((used))来标记,防止链接器会优化删除未被使用的段,然后将模块注入到__DATA中

此时Module已经被存储到Mach-O文件的特殊段中,那么如何取呢?

进入BHReadConfiguration方法,主要是通过Mach-O找到存储的数据段,取出放入数组中

点击查看
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{

    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    // 找到之前存储的数据段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片内存
    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; 
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

注册的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)];
                }

            }
        }
    }

}
__attribute__((constructor))
void initProphet() {
    //_dyld_register_func_for_add_image函数是用来注册dyld加载镜像时的回调函数,在dyld加载镜像时,会执行注册过的回调函数
    _dyld_register_func_for_add_image(dyld_callback);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

读取本地Pilst文件 首先,需要设置好路径

[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist

创建plist文件,Plist文件的格式也是数组中包含多个字典。字典里面有两个Key,一个是@"moduleLevel",另一个是@"moduleClass"。注意根的数组的名字叫@“moduleClasses”。

Screenshot-2023-08-27-at-09

进入loadLocalModules方法,主要是从Plist里面取出数组,然后把数组加入到BHModuleInfos数组里面。

点击查看
//初始化context时,加载Modules和Services
-(void)setContext:(BHContext *)context
{
    _context = context;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self loadStaticServices];
        [self loadStaticModules];
    });
}
👇
//加载modules
- (void)loadStaticModules
{
    // 读取本地plist文件里面的Module,并注册到BHModuleManager的BHModuleInfos数组中
    [[BHModuleManager sharedManager] loadLocalModules];
    //注册所有modules,在内部根据优先级进行排序
    [[BHModuleManager sharedManager] registedAllModules];

}
👇
- (void)loadLocalModules
{
    //plist文件路径
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
    //判断文件是否存在
    if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
        return;
    }
    //读取整个文件[@"moduleClasses" : 数组]
    NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
    //通过moduleClasses key读取 数组 [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]]
    NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
    NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
    //遍历数组
    [self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
    }];
    [modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
            //存储到 BHModuleInfos 中
            [self.BHModuleInfos addObject:obj];
        }
    }];
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# load方法注册

该方法注册Module就是在Load方法里面注册Module的类

点击查看
+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}

//进入registerDynamicModule实现
+ (void)registerDynamicModule:(Class)moduleClass
{
    [[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
👇
- (void)registerDynamicModule:(Class)moduleClass
{
    [self registerDynamicModule:moduleClass shouldTriggerInitEvent:NO];
}
👇
- (void)registerDynamicModule:(Class)moduleClass
       shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
{
    [self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent];
}
// 和Annotation方式注册的dyld_callback回调一样,最终会走到addModuleFromObject:shouldTriggerInitEvent:方法中

  - (void)addModuleFromObject:(id)object
       shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
  {
      Class class;
      NSString *moduleName = nil;

      if (object) {
          class = object;
          moduleName = NSStringFromClass(class);
      } else {
          return ;
      }

      __block BOOL flag = YES;
      [self.BHModules enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
          if ([obj isKindOfClass:class]) {
              flag = NO;
              *stop = YES;
          }
      }];
      if (!flag) {
          return;
      }

      if ([class conformsToProtocol:@protocol(BHModuleProtocol)]) {
          NSMutableDictionary *moduleInfo = [NSMutableDictionary dictionary];

          BOOL responseBasicLevel = [class instancesRespondToSelector:@selector(basicModuleLevel)];

          int levelInt = 1;

          if (responseBasicLevel) {
              levelInt = 0;
          }

          [moduleInfo setObject:@(levelInt) forKey:kModuleInfoLevelKey];
          if (moduleName) {
              [moduleInfo setObject:moduleName forKey:kModuleInfoNameKey];
          }

          [self.BHModuleInfos addObject:moduleInfo];

          id<BHModuleProtocol> moduleInstance = [[class alloc] init];
          [self.BHModules addObject:moduleInstance];
          [moduleInfo setObject:@(YES) forKey:kModuleInfoHasInstantiatedKey];
          [self.BHModules sortUsingComparator:^NSComparisonResult(id<BHModuleProtocol> moduleInstance1, id<BHModuleProtocol> moduleInstance2) {
              NSNumber *module1Level = @(BHModuleNormal);
              NSNumber *module2Level = @(BHModuleNormal);
              if ([moduleInstance1 respondsToSelector:@selector(basicModuleLevel)]) {
                  module1Level = @(BHModuleBasic);
              }
              if ([moduleInstance2 respondsToSelector:@selector(basicModuleLevel)]) {
                  module2Level = @(BHModuleBasic);
              }
              if (module1Level.integerValue != module2Level.integerValue) {
                  return module1Level.integerValue > module2Level.integerValue;
              } else {
                  NSInteger module1Priority = 0;
                  NSInteger module2Priority = 0;
                  if ([moduleInstance1 respondsToSelector:@selector(modulePriority)]) {
                      module1Priority = [moduleInstance1 modulePriority];
                  }
                  if ([moduleInstance2 respondsToSelector:@selector(modulePriority)]) {
                      module2Priority = [moduleInstance2 modulePriority];
                  }
                  return module1Priority < module2Priority;
              }
          }];
          [self registerEventsByModuleInstance:moduleInstance];

          if (shouldTriggerInitEvent) {
              [self handleModuleEvent:BHMSetupEvent forTarget:moduleInstance withSeletorStr:nil andCustomParam:nil];
              [self handleModulesInitEventForTarget:moduleInstance withCustomParam:nil];
              dispatch_async(dispatch_get_main_queue(), ^{
                  [self handleModuleEvent:BHMSplashEvent forTarget:moduleInstance withSeletorStr:nil andCustomParam:nil];
              });
          }
      }
  }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

load方法,还可以使用BH_EXPORT_MODULE宏代替

提示

#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
1
2
3

BH_EXPORT_MODULE宏里面可以传入一个参数,代表是否异步加载Module模块,如果是YES就是异步加载,如果是NO就是同步加载。

# BeeHive Protocol注册

在BeeHive中是通过BHServiceManager来管理各个Protocol的。BHServiceManager中只会管理已经被注册过的Protocol。

注册Protocol的方式总共有三种,和注册Module是一样一一对应的

Annotation方式注册

提示

//****** 1、通过BeeHiveService宏进行Annotation标记
BeeHiveService(HomeServiceProtocol,BHViewController)

//****** 2、宏定义
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ ""#servicename"" : ""#impl""}";

//****** 3、转换后的格式,也是将其存储到特殊的段
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ """HomeServiceProtocol""" : """BHViewController"""}";

1
2
3
4
5
6
7
8
9
10

# 读取本地plist文件

首先同Module一样,需要先设置好路径

提示

[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";

设置plist文件

Screenshot-2023-08-27-at-09

同样也是在setContext时注册services

点击查看
//加载services
-(void)loadStaticServices
{
    [BHServiceManager sharedManager].enableException = self.enableException;

    [[BHServiceManager sharedManager] registerLocalServices];

}
👇
- (void)registerLocalServices
{
    NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
    //获取plist文件路径
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
    if (!plistPath) {
        return;
    }

    NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];

    [self.lock lock];
    //遍历并存储到allServicesDict中
    for (NSDictionary *dict in serviceList) {
        NSString *protocolKey = [dict objectForKey:@"service"];
        NSString *protocolImplClass = [dict objectForKey:@"impl"];
        if (protocolKey.length > 0 && protocolImplClass.length > 0) {
            [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
        }
    }
    [self.lock unlock];
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# load方法注册

在Load方法里面注册Protocol协议,主要是调用BeeHive里面的registerService:service:完成protocol的注册

+ (void)load
{
   [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
👇
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
    [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
1
2
3
4
5
6
7
8
9

到此,三种方式注册就完成了

# Protocol的获取

Protocol与Module的区别在于,Protocol比Module多了一个方法,可以返回Protocol实例对象

点击查看
- (id)createService:(Protocol *)proto;
{
    return [[BHServiceManager sharedManager] createService:proto];
}
👇
- (id)createService:(Protocol *)service
{
    return [self createService:service withServiceName:nil];
}
👇
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {
    return [self createService:service withServiceName:serviceName shouldCache:YES];
}
👇
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
    if (!serviceName.length) {
        serviceName = NSStringFromProtocol(service);
    }
    id implInstance = nil;
    //判断protocol是否已经注册过
    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];
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

createService会先检查Protocol协议是否是注册过的。然后接着取出字典里面对应的Class,如果实现了shareInstance方法,那么就创建一个单例对象,如果没有,那么就创建一个实例对象。如果还实现了singleton,就能进一步的把implInstance和serviceStr对应的加到BHContext的servicesByName字典里面缓存起来。这样就可以随着上下文传递了

进入serviceImplClass实现,从这里可以看出 protocol和类是通过字典绑定的,protocol作为key,serviceImp(类的名字)作为value

点击查看
- (Class)serviceImplClass:(Protocol *)service
{
    //通过字典将 协议 和 类 绑定,其中协议作为key,serviceImp(类的名字)作为value
    NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
    if (serviceImpl.length > 0) {
        return NSClassFromString(serviceImpl);
    }
    return nil;
}
1
2
3
4
5
6
7
8
9

# Module & Protocol

这里简单总结下:

  • 对于Module:数组存储

  • 对于Protocol:通过字典将protocol与类进行绑定,key为protocol,value为 serviceImp即类名

BeeHive辅助类

  • BHContext类:是一个单例,其内部有两个NSMutableDictionary的属性,分别是modulesByName 和 servicesByName。这个类主要用来保存上下文信息的。例如在application:didFinishLaunchingWithOptions:的时候,就可以初始化大量的上下文信息
//保存信息
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
1
2
3
4
5
  • BHConfig类:是一个单例,其内部有一个NSMutableDictionary类型的config属性,该属性维护了一些动态的环境变量,作为BHContext的补充存在

  • BHTimeProfiler类:用来进行计算时间性能方面的Profiler

  • BHWatchDog类:用来开一个线程,监听主线程是否堵塞

# 组件化架构实践规范和原则

基于以上这些组件化架构的问题,需要一些组件化架构相关的规范和原则帮助我们做好组件化架构,后面主要会围绕以下三点进行介绍:

  • 组件拆分原则 - 拆分思想和最佳实践指导组件拆分
  • 组件间依赖 - 优化组件间依赖关系跨组件调用/通信方式的选择
  • 质量保障 - 避免在持续的工程演化过程中工程质量逐渐劣化。主要包含安全卡口和CI检查

接下来以一个典型的电商APP架构案例来介绍一个组件化工程。这个案例架构具备之前所说现有中大型APP架构的一些特点,多组件、多技术栈、业务间需要解耦、复用底层基础组件。基于这个案例来介绍上面的三点原则。 Screenshot-2023-08-27-at-20

# 组建拆分原则

Screenshot-2023-08-27-at-20

组件拆分最重要是帮我们梳理出组件职责以及组件职责的边界。组件划分也会使用很多通用的设计原则和架构思想。

# 使用分层思想拆分

通常我们可以首先使用分层架构的思想将所有组件纵向拆分为多层组件,上面层级的组件只能依赖下面层级的组件。一般至少可以划分为四层组件:

  • 基础层 - 提供核心的与上层业务无关的基础能力。可以被上层组件直接依赖使用。
  • 业务公共层 - 主要包含页面路由、公共UI组件、跨组件通信以及服务接口,可被上层组件直接依赖使用。
  • 业务实现层 - 业务核心实现层,包含原生页面、跨平台容器、业务服务实现。组件间不能直接依赖,只能通过调用页面路由或跨组件通信组件进行使用。
  • APP宿主层 - 主要包含APP主工程、启动流程、页面路由注册、服务注册、SDK参数初始化等组件,用于构建打包生成相应的APP。

划分层级可以很好的指导我们进行组件拆分。在拆分组件时我们需要先识别它应该在哪一层,它应该以哪种调用方式被其他组件使用,新添加的功能是否会产生反向依赖,帮助我们规范组件间的依赖关系。同时按层级拆分组件也有利于底层基础组件的复用。 以下场景使用分层思想就很容易识别:

基础组件依赖业务组件

例子:APP内业务发起网络请求通常需要携带公共参数/Cookie。

  • 没有组件分层约束 - 网络库可能会依赖登录服务获取用户信息、依赖定位服务获取经纬度,引入大量的依赖变成业务组件。

  • 有组件分层约束 - 网络库作为一个基础组件,它不需要关注上层业务需要携带哪些公共业务参数,同时登录/定位服务组件在网络库上层不能被反向依赖。这时候会考虑单独创建一个公共参数管理类,在APP运行时监听各种状态的变更并调用网络库更新公共参数/Cookie。

业务组件间依赖方向是否正确

登录状态切换经常会涉及到很多业务逻辑的触发,例如清空本地用户缓存、地址缓存、清空购物车数据、UI状态变更。

  • 没有组件分层约束 - 可能会在登录服务内当登录状态切换时调用多个业务逻辑的触发,导致登录服务引入多个业务组件依赖。
  • 有组件分层约束 - 登录组件只需要在登录状态切换时发出通知,无需知道登录状态切换会影响哪些业务。业务逻辑应该监听登录状态的变更。

识别基础组件还是业务组件

虽然很多场景下我们很容易能识别处理出来一个功能应该归属于基础组件还是业务组件,例如一个UI控件是基础组件还是业务组件。但是很多时候边界又非常的模糊,例如一个添加购物车按键应该是一个基础组件还是业务组件呢。

  • 基础组件 - 如果不需要依赖业务公共层那应当划分为一个基础组件。
  • 业务组件 - 依赖了业务公共层或者网络库,那就应该划分为一个业务组件。

分层思想可以很好的帮助我们管理组件间的依赖关系,并且明确每个组件的职责边界。

# 基础/业务组件拆分原则

划分基础/业务组件主要是为了强制约束组件间的依赖关系。以上面的组件分层架构为例:

  • 基础组件 - 基础组件可被直接依赖使用,使用方调用基础组件对外暴露API直接使用。基础层、业务公共层都为基础组件。
  • 业务组件 - 业务组件不可被直接依赖使用,只能通过间接通信方式进行使用。APP宿主层和业务实现层都为业务组件。 提示:这里的业务组件并不包含业务UI组件。

# 基础组件拆分

基础组件通常根据职责单一原则进行拆分比较容易拆分,但是会有一些拆分场景需要考虑: 使用插件组件拆分基础组件扩展能力 将核心基础能力和扩展能力拆分到不同的组件。以网络库为例,除了提供最核心的接口请求能力,同时可能还包含一些-扩展能力例如HTTPDNS、网络性能检测、弱网优化等能力。但这些扩展能力放在网络库组件内部可能会导致以下问题:

  • 扩展能力会使组件自身代码变得更加复杂。
  • 使用方不一定会使用所有这些扩展能力违反了最小依赖原则。带来更多的包体积,引入更多的组件依赖,增加模块间的耦合度。
  • 相关的扩展能力不支持灵活的替换/插拔。 所以这种场景我们可以考虑根据实际情况将扩展能力拆分到相应的插件组件,使用方需要时再依赖引入对应插件组件。

# 业务组件拆分

业务页面拆分方式

针对业务页面可以使用技术栈、业务域、页面粒度三种方式进行更细粒度的划分,通常至少要拆分到技术栈、业务域这一层级,页面粒度拆分根据具体页面复杂度和复用诉求。

  • 基于技术栈进行拆分 - 不同的技术栈需要拆分到不同的组件进行管理。
  • 基于业务域进行拆分 - 将同一个业务域的所有页面拆分一个组件,避免不同业务域之间形成强耦合依赖关系,同一个业务域通常会有更多复用和通信的场景也方便开发。例如订单详情和订单列表可放置在一起管理。
  • 基于页面粒度进行拆分 - 单个页面复杂度过高或需要被单独复用时需要拆分到一个单个组件管理。

提示:放置在单一组件内的多个页面之间也应适当降低耦合程度。

# 第三方库

第三方库应拆分单独组件管理 第三方库应使用独立的组件进行管理,一方面有利于组件复用同时避免多个重复第三方库导致符号冲突,另一方面有利于后续升级维护。

编辑 (opens new window)
上次更新: 2023/10/28, 15:22:03
图片相关的问题
MVVM、MVC、MVP

← 图片相关的问题 MVVM、MVC、MVP→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式