1. 组件化的由来
1.1 为什么要做组件化
早期项目建立之初工程一般较为简单,但是随着项目的慢慢的扩展、功能逐渐的增多,界面VC等之间的通讯满天飞,还有最主要的就是,大的项目一般都不会是一个人做的,如何做到组员高效率的开发、减少沟通成本等一系列的问题。这时开发人员就会意识到组件化的重要性;
1.2 组件化的好处
- 模块间解耦
- 模块重用
- 提高团队协作开发效率
- 单元测试
1.3 每个项目都需要组件化吗
由于做组件化也是需要时间和成本的,如果你的项目满足下面几点,那就暂时把组件化放一放。
- 项目较小,模块间交互简单,耦合少
- 模块没有被多个外部模块引用,只是一个单独的小模块
- 模块不需要重用,代码也很少被修改
- 团队规模很小
2. 组件化划分
要做组件化,那么如何划分组件就是开发人员首先需要考虑的问题,组件划分的颗粒度和解耦合程度,会直接影响你组件化的开发,以及项目的以后的走势。
2.1 划分依据

- 业务模块
在业务模块这层,一般用业务去划分组件,比如首页、我的、关注。。。
- 通用模块
在业务模块这层,一般会从业务模块下沉一些通用的常用控件、数据管理。。。
- 基础模块
在基础模块这层,一般会把一些宏定义、分类等不怎么变化的基础内容放到这层
- 管理模块
而以上这些个组件又是通过管理模块中个管理工具去控制,一般用CoCoaPods/Cathage
2.2 划分注意点
上面的划分依据只是满足通用的工程架构,你也可以多分几层,但是在划分时,你需要注意以下几点
- 依赖下沉:如果同级模块中出现相互依赖部分,那么要这部分抽出来,下沉到下一个模块
- 依赖不可倒置:要上层依赖下层进行开发,下层不能依赖上层
3. Cocoapods 管理组件
3.1 Cocoapods下载组件原理
Cocoapods 如何下载对应的组件?在我们pod install的时候,Cocoapods 根据组件名字比如AFNetworking在本地Specs文件中找到相应的文件夹,之后找到对应的版本josn文件

josn文件里面包含了AFNetworking在github上的下载地址,最后根据这个下载地址下载到本地项目中


3.2 Cocoapods 管理私有组件
既然Cocoapods可以管理第三方的组件,那当然可以管理自己本地划分出来的私有组件,我们可以将项目根据上面所说的划分依据,拆分成若干个组件,这几个组件都通过Cocoapods来管理,需要使用的时候,就通过Cocoapods来组合,项目就像搭积木一样方便快捷,至于私有组件如何去具体管理,您可以参考这篇文章使用Cocoapods创建私有库。下面是我对步骤的简单总结
- 创建模板库
pod lib create KTestBaseModle - 复制私有组件文件到
replaceme.m所属文件夹 pod installExample文件夹- 将这个私有库上传到远程git库中,其他程序员就只要使用
pod install就能使用你写的组件了
3.2.1 Cocoapods 管理私有组件注意点
- 先从基础模块开始创建私有组件,防止后期的上层模块又要拆分出来
- 如果组件有下层依赖,那就需要在 pod ->
xxx.podspec文件中添加依赖,以及头文件
s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'Masonry'
s.dependency 'KMacro'
prefix_header_contents = '#import "AfNetworking.h",#import "Masonry.h",#import "KMacro.h"'
像AfNetworking,Masonry这种三方库cocoapods能够被识别从而下载下来,而KMacro这种是你自己的私有组件,那就需要在Podfile文件中添加索引路径
use_frameworks!
platform :ios, '8.0'
target 'KTestBaseModle_Example' do
pod 'KTestBaseModle', :path => '../'
//私有组件添加索引路径,我这里是本地路径 如果是网上的 就需要换成网上的路径
Pod 'KMacro', :path => '../../KMacro'
target 'KTestBaseModle_Tests' do
inherit! :search_paths
end
end
- 如果私有库使用还涉及到一些资源文件:图片,json,xib...,那么在工程里使用的时候需要新的代码路径,下面是加载一张图片的示例代码,当然每个资源这么写显得很麻烦,你可以自己写个宏做个封装
NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/KTestBaseModle.bundle"];
NSBundle *resurce_bundle = [NSBundle bundleWithPath:bundlePath];
self.imageView.image = [UIImage imageNamed:@"testImage.png" inBundle:resurce_bundle compatibleWithTraitCollection:nil];
并且在xxx.podspec文件文件中还需要给到图片路径(资源路径)
s.resource_bundles = {
'KTestBaseModle' => ['KTestBaseModle/Assets/*.png']
}
4. 组件之间的通讯
4.1 模块耦合
在上面的内容中我们聊了组件划分以及具体的管理操作,但是组件化工作里面还有一个重要的内容:组件间的通讯。日常开发中,模块之间的必然需要相互通讯,但是这样会带来不同模块之间的耦合,就像下图所示,最起码,。模块之间你得导入头文件吧


4.1.1 中间件注意点
- 使用字符串去去确定一个类,因为字符串可以解耦合,中间件不需要导入对应的头文件;
- 模块和中间件之间使用中间件的分类去管理和调用组件
5 组件化业内方案
目前业内有三种主流的组件方案:MGJRouter、CTMediator、BeeHive。
5.1 MGJRouter
这个组件是蘑菇接开发团队提出的,基于路由和协议思想设计的,但是由于其内存常驻、字符串硬编码、解耦能力有限等局限性,这里不再讨论。
5.2 CTMediator
该方案基于Mediator(中间件)和Target-Action模式组件化,通过运行时完成调用。简单来说:Mediator维护着若干个category,一个category对应一个target,一个target可以包含多个action。 可以这么理解:一个A业务组件包含一个category组件,这个category组件中有个Mediator的category,这个category中有一个target,这个target属于对应的A业务组件,target中会有若干接口方法,用来其他业务组件通过这个category获取到A业务组件中的业务。 所有组件都通过组件自带的Target-Action来响应,也就是说,模块与模块之间的接口被固化在了Target-Action这一层。

5.2.1 核心方法
基于runtime
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
- (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];
NSLog(@"返回值的类型 %s",retType);
if (strcmp(retType, @encode(void)) == 0) {
// 通过NSMethodSignature对象创建NSInvocation对象,NSMethodSignature为方法签名类
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
// 设置消息参数
[invocation setArgument:¶ms atIndex:2];
// 设置要调用的消息
[invocation setSelector:action];
// 设置消息调用者
[invocation setTarget:target];
// 发送消息,即执行方法
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
// 当你确定编译器的警告对你来说没有什么用处的时候,为了避免心烦,
// 你可以使用#pragma clang diagnostic ignored “xxx” 这样的语句来忽略掉相应的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
5.3 BeeHive
CTMediator更多的是一对一的调用模式,但是如果换成一对多的调用方式,比如在用户登录成功的时候,好多的页面需要调整,这时CTMediator就会显得不那么方便了。对于这种情况,阿里的组件化方案 :BeeHive 可能会更加适合。
5.3.1 问题
我们知道在appdelegate中,包含了下面这些时机方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
- (void)applicationDidEnterBackground:(UIApplication *)application;
- (void)applicationWillEnterForeground:(UIApplication *)application;
...
但是这些时机需要开发人员去做主动获取,缺少主动下发能力。eg:某个vc需要监测applicationDidEnterBackground时机,就要在改方法中去做一个消息的通知,无法做到自动下发;
5.3.2 骚气的BeeHive
根据源码,我们看看BeeHive是怎么做的
- 用自定义的BHAppDelegate去替换原有的AppDelegate。这样BHAppDelegate就拥有了系统事件收集能力;然后后将这些事件用全局单利对象BHModuleManager去存储起来
//BHAppDelegate 遵循<UIApplicationDelegate>协议,所以拥有协议能力
@interface BHAppDelegate : UIResponder <UIApplicationDelegate>
@end
@implementation BHAppDelegate
@synthesize window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//储存事件
[[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];
[[BHModuleManager sharedManager] triggerEvent:BHMInitEvent];
dispatch_async(dispatch_get_main_queue(), ^{
[[BHModuleManager sharedManager] triggerEvent:BHMSplashEvent];
});
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
if ([UIDevice currentDevice].systemVersion.floatValue >= 10.0f) {
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
}
#endif
#ifdef DEBUG
[[BHTimeProfiler sharedTimeProfiler] saveTimeProfileDataIntoFile:@"BeeHiveTimeProfiler"];
#endif
return YES;
}
-(void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler {
[[BeeHive shareInstance].context.touchShortcutItem setShortcutItem: shortcutItem];
[[BeeHive shareInstance].context.touchShortcutItem setScompletionHandler: completionHandler];
[[BHModuleManager sharedManager] triggerEvent:BHMQuickActionEvent];
}
...
...
- 单利对象BeeHive内部保存一个全局的
BHContext对象,BHContext用来保存application、launchOptions、openURLItem、、、application原来的属性和参数(包括方法里的参数)
@interface BeeHive : NSObject
//save application global context
@property(nonatomic, strong) BHContext *context;
...
@end
@interface BHContext : NSObject <NSCopying>
//global env
@property(nonatomic, assign) BHEnvironmentType env;
//global config
@property(nonatomic, strong) BHConfig *config;
//application appkey
@property(nonatomic, strong) NSString *appkey;
//customEvent>=1000
@property(nonatomic, assign) NSInteger customEvent;
//全局的application
@property(nonatomic, strong) UIApplication *application;
//launchOptions
@property(nonatomic, strong) NSDictionary *launchOptions;
//3D-Touch 包含内部属性会被封装成BHShortcutItem
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80400
@property (nonatomic, strong) BHShortcutItem *touchShortcutItem;
#endif
//OpenURL model
@property (nonatomic, strong) BHOpenURLItem *openURLItem;
...
- 使用
BHModuleProtocol协议去包含个原来application中的方法,比如只要某vc遵循了BHModuleProtocol,他就能接收到里面BHModuleProtocol的方法;
@protocol BHModuleProtocol <NSObject>
@optional
//如果不去设置Level默认是Normal
//basicModuleLevel不去实现默认Normal
- (void)basicModuleLevel;
//越大越优先
- (NSInteger)modulePriority;
- (BOOL)async;
:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
//这个类似于didFinishLaunchingWithOptions方法,而原方法里的(UIApplication *)application和(NSDictionary *)launchOptions两个参数会被储存在(BHContext *)context里
- (void)modSetUp:(BHContext *)context;
- (void)modInit:(BHContext *)context;
- (void)modSplash:(BHContext *)context;
- (void)modQuickAction:(BHContext *)context;
- (void)modTearDown:(BHContext *)context;
- (void)modWillResignActive:(BHContext *)context;
- (void)modDidEnterBackground:(BHContext *)context;
- (void)modWillEnterForeground:(BHContext *)context;
....
- 最后当
BHApplication在收到系统事件后(eg:WillEnterForeground),会通过BHModuleManager,找到事先实现对应BHModuleProtocol协议的模块集合,然后遍历调用,调用的同时会把全局的context参数一并给模块使用
- (void)handleModuleEvent:(NSInteger)eventType
forTarget:(id<BHModuleProtocol>)target
withSeletorStr:(NSString *)selectorStr
andCustomParam:(NSDictionary *)customParam
{
//区全局BHContext的对象
BHContext *context = [BHContext shareInstance].copy;
context.customParam = customParam;
context.customEvent = eventType;
//根据eventType枚举取到对应的方法
if (!selectorStr.length) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
}
SEL seletor = NSSelectorFromString(selectorStr);
if (!seletor) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
seletor = NSSelectorFromString(selectorStr);
}
//id<BHModuleProtocol> moduleInstance 是都是遵守协议的,所以能够执行协议中的方法
NSArray<id<BHModuleProtocol>> *moduleInstances;
if (target) {
moduleInstances = @[target];
} else {
moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
}
//最后遍历调用
[moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//把参数context 给 moduleInstance
[moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
整个过程如下图,注意:图中的通知就是遍历调用

5.3.3 BeeHive 如何建立协议和控件(eg:vc)联系的?
上面说的是appdelegate事件如何被主动下发,即一对多的操作,那一对一的操作又是如何实现的呢?
下面以在官方提供的样例代码中BHUserTrackViewController交易vc说明
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
在BHViewController的init()方法中有这么一段代码,只通过UserTrackServiceProtocol就能取到v4(BHUserTrackViewController)的vc对象,那UserTrackServiceProtoco 和 BHUserTrackViewController是如何绑定了呢?继续看源码
5.3.3.1 存
#import "BHUserTrackViewController.h"
#import "BeeHive.h"
#import "BHService.h"
//这个宏是关键
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
//BHUserTrackViewController 遵循 UserTrackServiceProtocol
@interface BHUserTrackViewController()<UserTrackServiceProtocol>
@end
@implementation BHUserTrackViewController
+(BOOL)singleton {
return NO;
}
@end
我们看到有这一句@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)看上去就是绑定操作,那就继续深究,查看这个宏
//使用__attribute() 函数可以在编译其就能将数据写入到mach-o data段
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
//建立 servicename 和 impl联系
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
源码中使用了一个 __attribute()C函数宏在项目编译期就能将protocol和vc对应关系写入mach-o的data段
5.3.3.2 取
//C函数 会在dyld - doModinitFunctions函数后自动调用
__attribute__((constructor))
void initProphet() {
//在编译期对_dyld_register_func_for_add_image()进行监听,用dyld_callback这个函数保存监听后的动作
_dyld_register_func_for_add_image(dyld_callback);
}
#### dyld_callback
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
//从Mach-o DATA段取出之前写入的集合BHReadConfiguration()这个方法是关键
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
//这个是从
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)];
}
}
}
}
}
##### BHReadConfiguration()
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
//又从DATA 取出来
#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;
}
又通过监听_dyld_register_func_for_add_image()方法在回调方法dyld_callback()里从之前的mach-o DATA段取出对应集合,让后保存在本地内存中。预编译时期的存,和dyld链接时期的取--内存动态注入,真是太骚了!
写在最后
如果文章对您有帮助,烦请点个赞,小弟在此谢过了!