iOS开发-有赞组件化方案
前言
之前看过蘑菇街, 阿里, CT等组件化方案, 单独拿来使用都不是很好用.
对我们这种中小型公司来说蘑菇街的不够用, 阿里的复杂且并不好用. 所以最终看到有赞的方案觉得还是比较适合学习和摸索的, 更加轻量级, 方便.
主要类介绍
主要的类: Bifrost, Bifrost (Router)
Bifrost
:单例
-
moduleDict
: 可变字典,映射关系如下<serviceProtocol, class>
- 管理每一个模块的对外暴露的服务接口
-
moduleInvokeDict
: 可变字典, 映射执行关系<moduleClass, 类的实例>
-
NSInvocation
调用
Bifrost (Router)
:分类
-
_routes
: 可变字典, 静态变量.初始化一次.<urlStr, BifrostRouteHandler>
- 注册的类的
url
和一个闭包函数的对应, 返回值为id
类型. - 全局的
url
路由, 全都在这里实现.
主要流程
假设现在有一个模块或者组件为HomeModule
要进行加载, 要处理不同模块之间的调用,
要把自己的VC
加载进路由.
-
[Bifrost setupAllModules];
外部调用- 会循环取出
moduleDict
中的模块, 调用setup
方法 - 会循环取出
moduleInvokeDict
中的, 通过[Bifrost checkAllModulesWithSelector:arguments:]
注册一些方法, 如果模块实现了这些方法, 那么也会进行invoke
调用.
- 会循环取出
-
BFRegister(HomeModuleService);
内部注册,写在自身的load
方法中, 在Objc setup
阶段由dyld
调用.
1.创建一个对外的协议
#pragma mark - 需要告诉外部的通知字符串
static NSNotificationName kNotificationHomePageDidAppear = @"kNotificationHomePageDidAppear";
#pragma mark - 需要告诉外部的可以访问的路由
static NSString *const kRouteHomePage = @"//home/home_page";
#pragma mark - Model Protocols
// 这里主要是一些公共model的重用, 一般情况下不会有很多这种情况, 如果重用的很多模块可以重新设计. 这里主要是给出一些get方法
- (NSSTring *)getName;
#pragma mark - 需要注册的外部可调用的函数
@protocol HomeModuleService <NSObject>
- (void)helloOtherModule;
@end
2.将所有模块的协议放置在一个公共依赖的中转模块, 命名为Mediator
这个Mediator
组件相当于中介者模式的中介者
- 外部业务调用的时候, 通过
Mediator
找到对应的组件协议进行调用
- 还会放置一些, 比如每个组件库, 获取
boundle资源, 国际化资源, xib资源
等资源文件需要通过组件的类访问到对应的资源的公共组件.
@interface ModuleBundle : NSObject
+ (NSBundle *)bundleWithName:(NSString *)bundleName;
@end
@implementation ModuleBundle
+ (NSBundle *)bundleWithName:(NSString *)bundleName {
if(bundleName.length == 0) {
return nil;
}
NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
NSAssert([NSBundle bundleWithPath:path], @"not found bundle");
return [NSBundle bundleWithPath:path];
}
@end
//我们组件内部调用资源的时候, 根据这个bundle才可以获取到资源
@interface HomeBundle : ModuleBundle
@end
+ (NSBundle *)bundle{
return [self.class bundleWithName:NSStringFromClass(self.class)];
}
3.组件继承并且实现协议
BifrostModuleProtocol
必须要实现的协议方法如下
- 是把每个模块的
xxxModule
下的必须实现的方法:-
sharedInstance
单例方法 -
setup
初始化需要做什么的方法 -
HomeModuleService
服务下定义的必须实现的方法.
-
- 模块的注册是在
load
方法里, 真实的测评1000次的调用对启动时间的消耗几乎可以忽略不计.但是不可以在load
方法里做任何耗时操作.
所以, 相当于每个组件的xxxModule
这个模块, 都会是个单例.
@interface HomeModule : NSObject<BifrostModuleProtocol, HomeModuleService>
@end
@implementation HomeModule
+ (void)load {
BFRegister(HomeModuleService);
}
#pragma mark - BifrostModuleProtocol
+ (instancetype)sharedInstance {
static HomeModule *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)setup {
}
//优先级, 数字越高, 优先级越高, 在加载的时候, 会重新根据优先级排序,
//所以第一个展现的模块, 可以设置为最高优先级, 以及同步加载.
//主要是根据优先级调用的模块的 setup 方法的同步异步
+ (NSUInteger)priority {
return BifrostModuleDefaultPriority+100; //higher priority than other modules
}
//是否同步, 默认为异步加载
+ (BOOL)setupModuleSynchronously {
return YES;
}
#pragma mark - UIApplicationDelegate, 这里就是checkAll的检查方法,
//moduleInvokeDict里面如果含有这个方法, 则setupAllModules的时候模块内部的也会被调用.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UIWindow *window = application.delegate.window;
UIViewController *homeVC = [Bifrost handleURL:kRouteHomePage];
UINavigationController *rootNavContoller = [[UINavigationController alloc] initWithRootViewController:homeVC];
rootNavContoller.navigationItem.backBarButtonItem.title = @"";
window.rootViewController = rootNavContoller;
[window makeKeyAndVisible];
return YES;
}
#pragma mark - HomeModuleService
- (void)helloOtherModule {
pinrtf("\nhello others\n")
}
@end
4.外部调用组件
至此, 在外部调用组件的时候, 就如下:
//导入协议的头文件
Protocol *cl = @protocol(HomeModuleService);
id cls = [Bifrost moduleByService:cl];
[cls helloOtherModule];
5.组件内部的路由注册
注册操作
+ (void)load {
[Bifrost bindURL:kRouteHomePage
toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
UIViewController *vc = [[HomeBundle storyboardWithName:@"home"] instantiateViewControllerWithIdentifier:@"HomeViewController"];
return vc;
}];
}
获取操作
id cls = [Bifrost handleURL:kRouteHomePage];
这里的所有的组件内部的url
和handle
操作, 都绑定了在了分类的全局静态_routes
字典上. 所以可以直接根据对应的服务的url
去找到handle
操作, 返回值为id, 可以做vc
使用. 跨组件的跳转就是在这里解决的.
6.不同组件间的数据回调
默认homeMoudle
和shopMoudle
中的两个vc进行通讯.
homeVC
代码如下:
//跳转的时候传入一个completion, 然后在跳转的vc需要回调的时候, 会走回调
UIViewController *vc = [Bifrost handleURL:kRouteShopDetail complexParams:nil completion:^(id _Nullable result) {
//result为传回来的参数
NSLog(@"跳转完了");
}];
//vc进行跳转
shopVC
如下:
//绑定的时候, 设置的
[Bifrost bindURL:kRouteShopDetail toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
ShopDetailViewController *vc = [[self alloc] init];
if (parameters[kBifrostRouteCompletion]) {
BifrostRouteCompletion com = parameters[kBifrostRouteCompletion];
//nil就是需要传的参数, 为id类型可以自己定义
//com可以内部保存起来, 在需要操作的时候进行调用.
com(nil);
}
return vc;
}];
Bifrost异常处理
Bifrost中的BifrostExceptionHandler
, 可以看看ModuleExceptionHandler
类
这里使用的是hook, doesNotRecognizeSelector:
函数, 进行异常的处理. 比如方法找不到之类的, 可以防止崩溃.
YZSTableViewModel
这个类主要是对tableView
的代理进行的封装, 将代理转化为函数式编程
-
YZSTableViewModel
:管理sectionModelArray
-
YZSTableViewSectionModel
:管理:cellModelArray
YZSTableViewCellModel
-
我们项目里也有类似的方式, 总体来说还是很方便进行开发和调用.只需要做下面这样一个操作:
- (YZSTableViewModel*)viewModel {
if(!_viewModel) {
_viewModel = [[YZSTableViewModel alloc] init];
self.tableView.dataSource = _viewModel;
self.tableView.delegate = _viewModel;
_viewModel.delegate = self;
}
return _viewModel;
}
- (void)reloadData {
[self.viewModel.sectionModelArray removeAllObjects];
YZSTableViewSectionModel *sectionModel = [[YZSTableViewSectionModel alloc] init];
//1.添加Section
[self.viewModel.sectionModelArray addObject:sectionModel];
//2.循环提添加cell
for (id<GoodsProtocol> goods in list) {
YZSTableViewCellModel *cellModel = [[YZSTableViewCellModel alloc] init];
[sectionModel.cellModelArray addObject:cellModel];
cellModel.renderBlock = ^UITableViewCell * _Nonnull(NSIndexPath * _Nonnull indexPath, UITableView * _Nonnull tableView) {
//相当于cell的渲染, 其他也都是设计成对应的block或者属性
YZStrong(self)
return cell;
};
}
[self.tableView reloadData];
}
上面代码只是大致流程, 这方面相对来说就是对代理做了一层函数式的设计, 不是很难理解.大概如此.
总结
有赞的方案我觉得更简洁, 更易懂, 而且代码不冗余, 非常适合我们这种中小型企业开发.
我们项目里用的也是类似于有赞这种方案, 但是是从阿里和蘑菇街提取出来的写的有点乱, 没有有赞这么的清晰.
根据自己的项目, 在此基础上进行一些修改相对来说会更快捷舒服.
-
Bifrost
处理模块间的加载和互通.- 每个组件设计成单例, 调用
setup
方法做操作 - 调用
invoke
去做一些注册check
的方法调用.
- 每个组件设计成单例, 调用
-
Bifrost (Router)
处理所有模块注册的路由.- 注册
url
对应的block
, 返回值为id
- 可以传参以及传一个
kBifrostRouteCompletion
为key的回调函数在参数里面. - 路由可以跳转到任何一个页面.
- 注册
-
Mediator
组件就是所谓的中介者- 存放所有的服务
- 公共资源的
bundle
的基类设置 - 全局的
ModuleExceptionHandler
的实现
测试加载10000
路由, 启动时间大概是30
ms左右, 所以单纯的加载注册对启动的影响非常小.
以下文章可以做一个学习参考:
GCD面试要点
block面试要点
Runtime面试要点
RunLoop面试要点
内存管理面试要点
MVC、MVVM面试要点
网络性能优化面试要点
网络编程面试要点
KVC&KVO面试要点
数据存储面试要点
混编技术面试要点
设计模式面试要点
UI面试要点