1. 初探

组件化可以将一个庞大的项目,按功能拆分成独立组件,多组件之间特定方式通讯,从而使模块之间解耦,提高团队协作开发效率

每一个组件都是独立的,可独立运行。一些底层的组件可重复利用,提高可重用性

1.1 使用组件化的优势

  • 模块间解耦

  • 模块复用

  • 提高团队协作开发效率

  • 单元测试

1.2 不建议使用组件化的项目

使用组件化,理论上会使项目变复杂

设计模块的分离和相互之间的通讯,常用控件和功能的封装,对宏定义及分类文件、底层组件的下沉,都会让你的代码变得更庞大

所以,当项目或团队具备以下特性时,不建议使用组件化

  • 项目较小,模块间交互简单,耦合少

  • 模块没有被多个外部模块引用,只是一个单独的小模块

  • 模块不需要重用,代码也很少被修改

  • 团队规模很小

1.3 组件化分层

一般项目的组件化分为业务层、通用层、基础层
image.png

  • 只允许上层对下层的依赖,不允许下层依赖上层
  • 横向模块之间不能依赖,同级模块之间的通讯进行下沉
  • 将通用组件、宏定义、分类文件、公共资源进行下沉,使其具备独立性和复用性
  • 开发中,对于层次的构建,由下至上。避免因下层代码的改动,导致上层代码大量修改

例如:
image.png

2. CocoaPods

CocoaPods是专门为iOS工程提供第三方依赖库的管理工具,通过CocoaPods,我们可以更方便地管理每个第三方库的版本,而且不需要我们做太多的配置,就可以直观、集中和自动化地管理我们项目的第三方库

日常开发中,我们经常使用CocoaPods进行代码提交,也经常会拉取第三方提供的优秀框架使用

使用CocoaPods必须对其进行安装
image.png

导入一个三方框架时,会在本地CocoaPods的索引库中进行查找
image.png

.podspec.json文件中,找到该框架在远程仓库的下载地址,从远程仓库将其导入
image.png

CocoaPods流程图
image.png

3. 创建组件

使用CocoaPods,可分为本地和远程两种方式搭建组件化工程

  • 本地:通过项目中创建模块,利用CocoaPodsworkspec进行本地管理,不需要将项目上传git,而是在项目的Podfile中指定目录

  • 远程:利用CocoaPods进行模块的远程管理,需要将项目上传git。对公司项目而言,一般使用私有库

下面我们以本地方式为例,搭建一个组件化工程

创建LGHomeModule模块

  1. pod lib create LGHomeModule
  2. -------------------------
  3. //对模块进行以下配置:
  4. //工程类型
  5. What platform do you want to use?? [ iOS / macOS ]
  6. > iOS
  7. //开发语言
  8. What language do you want to use?? [ Swift / ObjC ]
  9. > objc
  10. //创建App测试项目
  11. Would you like to include a demo application with your library? [ Yes / No ]
  12. > yes
  13. //提供frameworks的测试
  14. Which testing frameworks will you use? [ Specta / Kiwi / None ]
  15. > none
  16. //提供测试文件
  17. Would you like to do view based testing? [ Yes / No ]
  18. > no
  19. //设置前缀
  20. What is your class prefix?
  21. > LG

配置完成后,生成以下工程:
image.png

进行组件的开发,真正的代码目录在Pods项目的LGHomeModule中,而LGHomeModule是对组件进行测试使用
image.png

在组件中完成Home模块的业务代码
image.png

在测试工程Example目录下,执行pod install
image.png

打开工程,组件成功导入
image.png

4. 三方和本地组件的依赖

4.1 三方框架

日常我们开发的组件,有些功能会依赖于其他三方框架,此时我们需要对其进行额外的配置

创建通用UI组件LGCommonUIModule

  1. pod lib create LGCommonUIModule

完成组件的业务代码,部分功能依赖于AFNetworkingMasonry框架
image.png

配置组件的Pod文件,写入对三方框架的依赖
image.png

在测试工程Example目录下,执行pod install,解决三方框架的依赖问题

4.2 本地组件

除了三方框架的依赖,我们的组件也会对下层的本地组件进行依赖,例如:分类和宏定义等

LGCommonUIModule插件中,对下层的公共组件LGMacroAndCategoryModule进行依赖,并且代码中使用到插件中的分类和宏
image.png

Pod文件中,按照三方库的导入方式

  1. s.dependency 'AFNetworking'
  2. s.dependency 'Masonry'
  3. s.dependency 'LGMacroAndCategoryModule'
  4. s.prefix_header_contents = '#import "Masonry.h"','#import "UIKit+AFNetworking.h"','#import "LGMacros.h"'
  • 如果公共组件在云端,当然不会有任何问题。但案例中,使用本地组件,这种导入方式一定会报错

导入本地组件,除了上述的配置之外,还需要在Pods项目中的Podfile文件中,对公共组件的本地路径进行配置
image.png

以当前的Podfile文件路径为基础,向上两层,找到LGMacroAndCategoryModule本地组件
image.png

在测试工程Example目录下,执行pod install,解决本地组件的依赖问题

5. 资源文件的加载

5.1 图片资源

日常开发中,使用图片资源文件,都会用到UIImageimageNamed方法

  1. self.imageView.image = [UIImage imageNamed:@"share_wechat"];
  • 但使用的图片在组件项目中,使用这种方式是访问不到的

组件内的图片资源存储位置,在组件/Assets目录下
image.png

配置组件的Pod文件,写入资源的Bundle
image.png

在测试工程Example目录下,执行pod install

在测试工程中,通过指定Bundle访问组件内的图片资源

  1. NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/LGModuleTest.bundle"];
  2. NSBundle *resoure_bundle = [NSBundle bundleWithPath:bundlePath];
  3. self.imageView.image = [UIImage imageNamed:@"share_wechat" inBundle:resoure_bundle compatibleWithTraitCollection:nil];

5.2 json文件

组件内封装HomeViewController,读取组件内的json文件

json文件的配置路径,在组件/Assets目录下
image.png

配置组件的Pod文件,写入资源的Bundle
image.png

在测试工程Example目录下,执行pod install

读取方式,指定LGHomeModule.bundle

  1. NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/LGHomeModule.bundle"];
  2. NSString *path = [[NSBundle bundleWithPath:bundlePath] pathForResource:[NSString stringWithFormat:@"Home_TableView_Response_%@", channelId] ofType:@"json"];
  3. NSData *data = [NSData dataWithContentsOfFile:path];

5.3 xib文件

还有一种资源文件,和图片很相似,就是我们开发中经常用到的xib文件

访问组件内的xib文件
image.png

读取方式,指定Bundle

  1. for (NSString *className in HomeTableViewCellIdentifiers.allValues) {
  2. NSString *bundlePath = [NSBundle bundleForClass:[self class]].resourcePath;
  3. [self.tableView registerNib:[UINib nibWithNibName:className bundle:[NSBundle bundleWithPath:bundlePath]] forCellReuseIdentifier:className];
  4. }

6. 通讯解耦

同一层级的模块之间相互通讯,会导致通讯代码错综复杂。你中有我,我中有你。单一模块的修改,很可能牵扯其他模块的报错,不利于项目的维护
image.png

此时应该将模块之间的通讯进行下沉,抽取出一个公用的下层组件,使模块之间解耦,代码相互独立
image.png

针对上述的通讯解耦需求,主流解决方案可分为三种:

  • URL路由

  • target-action

  • protocol

6.1 URL路由

URL路由的方案相对简单,基于URL匹配,双方进行命名约定,使用Runtime方法进行动态调用

方案的代表框架:MGJRouter

最基本的使用

  1. [MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
  2. NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
  3. }];
  4. [MGJRouter openURL:@"mgj://foo/bar"];

当匹配到URL后,routerParameters会自带几个key

  1. extern NSString *const MGJRouterParameterURL;
  2. extern NSString *const MGJRouterParameterCompletion;
  3. extern NSString *const MGJRouterParameterUserInfo;

方案的思路:

  • App启动时实例化各组件模块,这些组件向ModuleManager注册URL。不需要实例化的组件,可使用Class进行注册

  • 组件A调用组件B时,向ModuleManager传递URL,可携带参数。使用封装的openURL方法,由ModuleManager负责组件B的调度

优点:

  • 动态性高,适合页面和参数自由度较高的电商类App

  • 多平台的路由规则可统一管理

  • 适用于URL Scheme

缺点:

  • 使用字符串传递,安全性和稳健性难以保证,被使用的模块不一定存在

  • 对于字符串的管理成本较高

  • 不支持storyboard

  • 一旦使用该方案,很难被替换。对于整个工程来说,重构难度加大

6.2 target-action

基于OCRuntimeCategory特性动态获取模块

  • 通过NSClassFromString获取类并创建实例

  • 通过performSelector + NSInvocation动态调用方法

方案的代表框架:CTMediator

最基本的使用

  1. //1、创建CTMediator的分类,完成对外的接口
  2. @implementation CTMediator (CTMediatorModuleAActions)
  3. - (void)CTMediator_presentImage:(UIImage *)image
  4. {
  5. [self performTarget:@"A"
  6. action:@"nativePresentImage"
  7. params:@{@"image":image}
  8. shouldCacheTarget:NO];
  9. }
  10. @end
  11. //2、添加Action,完成具体的业务
  12. @implementation Target_A
  13. - (id)Action_nativePresentImage:(NSDictionary *)params
  14. {
  15. DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
  16. viewController.valueLabel.text = @"this is image";
  17. viewController.imageView.image = params[@"image"];
  18. [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
  19. return nil;
  20. }
  21. @end
  22. //3、外部调用
  23. [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];

方案的思路:

  • 组件的核心CTMediator类,使用字符串按照指定规则,拿到真实的targetaction的名称

  • 通过方法签名,判断返回值类型,如果是非id类型,使用NSInvocation进行消息转发

    • 传入targetselectorargument

    • 使用getReturnValue方法将其返回

  • 否则,返回值id类型,直接使用performSelector进行方法调用

优点:

  • 利用分类将接口按业务拆分,去中心化

  • 框架核心代码短小精悍,实现方式轻量

缺点:

  • 每一个接口都需要中间方法,有些繁琐

  • 用字符串传递,被使用的模块不一定存在

  • 业务越复杂,创建的分类和target-action中间类就会越多

模块之间的通讯流程:
image.png

6.3 protocol

protocol和对class进行匹配,过用protocol获取class,动态创建实例

方案的代表框架:BeeHive

最基本的使用

  1. //1、创建Protocol
  2. #import <Foundation/Foundation.h>
  3. #import "BHServiceProtocol.h"
  4. @protocol TradeServiceProtocol <NSObject, BHServiceProtocol>
  5. @property(nonatomic, strong) NSString *itemId;
  6. @end
  7. //2、动态创建Module,将protocol和对class进行匹配
  8. @interface TradeModule()<BHModuleProtocol>
  9. @end
  10. @implementation TradeModule
  11. + (void)load
  12. {
  13. [BeeHive registerDynamicModule:[self class]];
  14. }
  15. - (id)init{
  16. if (self = [super init])
  17. {
  18. NSLog(@"TradeModule init");
  19. }
  20. return self;
  21. }
  22. -(void)modInit:(BHContext *)context
  23. {
  24. NSLog(@"模块初始化中");
  25. NSLog(@"%@",context.moduleConfigName);
  26. id<TradeServiceProtocol> service = [[BeeHive shareInstance] createService:@protocol(TradeServiceProtocol)];
  27. service.itemId = @"我是单例";
  28. }
  29. - (void)modSetUp:(BHContext *)context
  30. {
  31. [[BeeHive shareInstance] registerService:@protocol(TradeServiceProtocol) service:[BHTradeViewController class]];
  32. NSLog(@"TradeModule setup");
  33. }
  34. - (void)basicModuleLevel
  35. {
  36. }
  37. @end
  38. //3、外部调用
  39. -(void)click:(UIButton *)btn
  40. {
  41. id<TradeServiceProtocol> obj = [[BeeHive shareInstance] createService:@protocol(TradeServiceProtocol)];
  42. if ([obj isKindOfClass:[UIViewController class]]) {
  43. obj.itemId = @"12313231231";
  44. [self.navigationController pushViewController:(UIViewController *)obj animated:YES];
  45. }
  46. }

方案的思路:
image.png

  • 模块:模块被不同的功能分开,每个模块都可以通过自己的服务与其他模块进行通信
  • 服务:服务是特定模块的接口

6.3.1 系统事件

系统事件通常是应用程序生命周期事件,如DidBecomeActiveWillEnterBackground

系统事件基本工作流程如下:
image.png

使用自定义AppDelegate继承于BHAppDelegate,代替系统AppDelegate

  1. @interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>

6.3.2 通用事件

在系统事件的基础上扩展通用应用程序事件,如modSetupmodInit等,可用于编码每个插件模块的初始化设置

扩展常见事件如下:
image.png

6.3.3 业务自定义事件

如果觉得系统事件,事件还不能满足一般需求,我们将事件简化打包成BHAppdelgate,可以通过继承BHAppdelegate扩展自己的事件,同时BHContext里的modulesByName访问每个模块入口类,增加触发点通过

  1. [[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];

6.3.4 模块注册

模块的注册有两种方式:

  • 静态注册

  • 动态注册


静态注册方式:

AppDelegate应用启动时,初始化moduleservice的plist文件
image.png

打开BHService.plist,以字典方式将protocolclass对应,需要手动维护
image.png


动态注册方式:

使用注解方式,注册protocolclass的对应关系

  1. @BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
  2. #define BeeHiveService(servicename,impl) \
  3. class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
  4. #define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
  • 通过对section的操作,将名字写入到__DATA

对数据段的读取时机

  1. __attribute__((constructor))
  2. void initProphet() {
  3. _dyld_register_func_for_add_image(dyld_callback);
  4. }
  • dyld注册函数添加到image的时候,调用dyld_callback回调函数

进入dyld_callback函数

  1. static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
  2. {
  3. NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
  4. for (NSString *modName in mods) {
  5. Class cls;
  6. if (modName) {
  7. cls = NSClassFromString(modName);
  8. if (cls) {
  9. [[BHModuleManager sharedManager] registerDynamicModule:cls];
  10. }
  11. }
  12. }
  13. //register services
  14. NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
  15. for (NSString *map in services) {
  16. NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
  17. NSError *error = nil;
  18. id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
  19. if (!error) {
  20. if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
  21. NSString *protocol = [json allKeys][0];
  22. NSString *clsName = [json allValues][0];
  23. if (protocol && clsName) {
  24. [[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
  25. }
  26. }
  27. }
  28. }
  29. }
  • 读取该表头下的所有数据,循环注册模块和服务

代码注册服务:

使用registerService方法,注册protocolclass的对应关系:

  1. - (void)modSetUp:(BHContext *)context
  2. {
  3. [[BeeHive shareInstance] registerService:@protocol(TradeServiceProtocol) service:[BHTradeViewController class]];
  4. NSLog(@"TradeModule setup");
  5. }

异步加载:

如果模块设置为export BH_EXPORT_MODULE(YES),会初始化异步执行模块,可以优化后启动前第一屏显示内容启动耗时

6.3.5 服务调用

通过protocol获取class

  1. -(void)click:(UIButton *)btn
  2. {
  3. id<TradeServiceProtocol> obj = [[BeeHive shareInstance] createService:@protocol(TradeServiceProtocol)];
  4. if ([obj isKindOfClass:[UIViewController class]]) {
  5. obj.itemId = @"12313231231";
  6. [self.navigationController pushViewController:(UIViewController *)obj animated:YES];
  7. }
  8. }

进入createService方法

  1. - (id)createService:(Protocol *)proto;
  2. {
  3. return [[BHServiceManager sharedManager] createService:proto];
  4. }
  5. - (id)createService:(Protocol *)service
  6. {
  7. return [self createService:service withServiceName:nil];
  8. }
  9. - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {
  10. return [self createService:service withServiceName:serviceName shouldCache:YES];
  11. }
  12. - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
  13. if (!serviceName.length) {
  14. serviceName = NSStringFromProtocol(service);
  15. }
  16. id implInstance = nil;
  17. if (![self checkValidService:service]) {
  18. if (self.enableException) {
  19. @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
  20. }
  21. }
  22. NSString *serviceStr = serviceName;
  23. if (shouldCache) {
  24. id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
  25. if (protocolImpl) {
  26. return protocolImpl;
  27. }
  28. }
  29. // 问题: app -> VC
  30. // serivceProtocol : 普通 绑定在类
  31. // mudleProtocol -> 一点对多的事件 applegate 生命广播不知道
  32. Class implClass = [self serviceImplClass:service];
  33. if ([[implClass class] respondsToSelector:@selector(singleton)]) {
  34. if ([[implClass class] singleton]) {
  35. if ([[implClass class] respondsToSelector:@selector(shareInstance)])
  36. implInstance = [[implClass class] shareInstance];
  37. else
  38. implInstance = [[implClass alloc] init];
  39. if (shouldCache) {
  40. [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
  41. return implInstance;
  42. } else {
  43. return implInstance;
  44. }
  45. }
  46. }
  47. return [[implClass alloc] init];
  48. }

总结

初探:

  • 组件化可以将一个庞大的项目,按功能拆分成独立组件,多组件之间特定方式通讯,从而使模块之间解耦,提高团队协作开发效率

  • 每一个组件都是独立的,可独立运行。一些底层的组件可重复利用,提高可重用性

使用组件化的优势:

  • 模块间解耦

  • 模块复用

  • 提高团队协作开发效率

  • 单元测试

不建议使用组件化的项目:

  • 项目较小,模块间交互简单,耦合少

  • 模块没有被多个外部模块引用,只是一个单独的小模块

  • 模块不需要重用,代码也很少被修改

  • 团队规模很小

组件化分层:

  • 组件化分为业务层、通用层、基础层

  • 只允许上层对下层的依赖,不允许下层依赖上层

  • 横向模块之间不能依赖,同级模块之间的通讯进行下沉

  • 将通用组件、宏定义、分类文件、公共资源进行下沉,使其具备独立性和复用性

  • 开发中,对于层次的构建,由下至上。避免因下层代码的改动,导致上层代码大量修改