前言
使用BeeHive来进行项目组件化,其实是使用BeeHive来构建一个中间层,通过中间层来解耦各个模块。在文章iOS组件化通用工具浅析有简单介绍过BeeHive的一些组件化思路,本文将更多的从使用者的角度来分析BeeHive。
1. 用法
通过构建中间层来组件化项目,共需要三步:
- 创建protocol
- 创建impClass
- 存储protocol-impClass映射关系
1.1. 创建protocol
protocol表示模块对外暴露的接口,调用模块时只需要依赖模块对应的protocol,就可以实现对模块的调用。
下列代码表示,模块A对应的协议BHServiceProtocol
的定义,调用者可以通过-[getModuleAMainViewController]
和-[pushToModuleAOneViewController]
这两个方法来调用模块A。
// BHServiceProtocol.m
#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>
@protocol ModuleAServiceProtocol <NSObject, BHServiceProtocol>
- (UIViewController *)getModuleAMainViewController;
- (void)pushToModuleAOneViewController;
@end
这个协议需要继承BeeHive中的协议BHServiceProtocol
,协议BHServiceProtocol
中定义如下两个可选方法+[singleton:]
和+[shareInstance]
。
如果协议对应的响应者impClass实现了这两个方法,并且+[singleton:]
方法返回YES
,则调用响应类的+[shareInstance]
方法来创建响应者对象。否则,直接调用[[implClass alloc] init]
来创建对象。
#import <Foundation/Foundation.h>
#import "BHAnnotation.h"
@protocol BHServiceProtocol <NSObject>
@optional
+ (BOOL)singleton;
+ (id)shareInstance;
@end
1.2. 创建impClass
impClass是protocol对应的响应类,它需要遵守这个protocol协议,它可以是模块中一个已经存在的业务类,也可以是这个模块的一个封装类。
如果模块对外暴露的方法全部来自于同一个业务类,则可以将这个业务类设置成impClass;
如果模块对外暴露的方法全部来自于多个不同的业务类,则需要给这个模块创建一个封装类,通过这个封装类来实现对模块的调用,impClass指向这个封装类。(这种方式也叫做target-action)
第一种方式比较常用,BeeHive的官方demo基本上是使用的这种方法。
模块A的impClass是ModuleAService
类,它是一个封装类,内部实现了对模块A中两个不同类的调用。
//ModuleAService.m
#import "ModuleAOneViewController.h"
#import "ModuleAViewController.h"
#import "ModuleAService.h"
@implementation ModuleAService
- (UIViewController *)getModuleAMainViewController{
return [ModuleAViewController new];
}
- (void )pushToModuleAOneViewController{
UITabBarController *tab = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;
UINavigationController *nav = tab.selectedViewController;
ModuleAOneViewController *one = [ModuleAOneViewController new];
[nav pushViewController:one animated:YES];
}
另外,模块C对外暴露的方法只有一个,所以模块C使用的是第一种方式,它的impClass直接指向ModuleCViewController
这个业务类。
1.3. 设置protocol-impClass映射关系
在BeeHive中,所有protocol-impClass的映射关系都由BHServiceManager
管理,BHServiceManager
主要提供了两个方法:
- (void)registerService:(Protocol *)service implClass:(Class)implClass;
- (id)createService:(Protocol *)service;
方法名中的service指的就是上文中所说的protocol,所以方法一的作用是注册protocol-impClass的映射关系,方法二的作用是通过protocol获取对应的响应类。
在BHServiceManager
类中,有一个叫做allServicesDict
的属性,它保存了所有的protocol-impClass的映射关系,上述方法一和方法二就是根据这个属性来执行的。
allServicesDict
是一个可变字典,其中key是protocol的字符串名称,value是impClass的字符串名称。
具体注册方式有下列三种
1. 使用BeeHive
类的-[registerService:service:]
方法-[registerService:service:]
的实现
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
[[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
这个方法内部就是调用了BHServiceManager
的-[registerService:implClass:]
方法,将传入的protocol
和impClass
添加到BHServiceManager
类的属性allServicesDict
中。
2. 使用宏BeeHiveService
上文中,定义了ModuleA模块的协议ModuleAServiceProtocol
和响应类ModuleAService
,可以使用如下代码来注册它们之间的关系:
BeeHiveService(ModuleAServiceProtocol, ModuleAService)
使用宏来注册时,务必在本模块中调用宏。如果在主工程中调用,且主工程没有导入这个模块(更准确的说是impClass对应的类没有导入),会导致程序crash。
在上一篇文章第四节中已经讲过了注册Module
类的宏BeeHiveMod
,这两个宏的实现原理是一样的,都是在mach-o文件中增加一个section来存储数据,然后在启动项目时取出数据,最终也是调用BHServiceManager
的-[registerService:implClass:]
方法来注册,详细过程这里就不在赘述。
mach-o文件的section:__DATA:BeehiveServices
中存储的是一个json格式的字符串:
"{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}"
3. 使用plist文件
使用plist文件注册,需要在初始化BeeHive时指定plist文件的路径
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
plist文件的格式:
需要注意的是BeeHive.bundle必须添加到项目的主工程的target上,因为BeeHive内部是在[NSBundle mainBundle]
的目录下寻找BeeHive.bundle。
当使用cocoapods来加载BeeHive时,默认情况下,BeeHive.bundle是存在于BeeHive.framework中,这个时候使用[NSBundle mainBundle]
时获取不到BeeHive.bundle的,解决办法是改用[NSBundle bundleForClass:self.class]
或将BeeHive.bundle添加到项目的主工程的target上。
2. 使用场景
一个典型的场景,当调用模块A时,如果当前还没有登录,则调用登录模块,登录成功之后,再调用模块A;如果已经登录了,则直接调用模块A。
以项目BeeHive-demo3为例
模块A对外的协议ModuleAServiceProtocol
//BHServiceProtocol.h
#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>
@protocol ModuleAServiceProtocol <NSObject, BHServiceProtocol>
- (void)pushToModuleAViewController;
@end
模块A的响应类
//ModuleAService.m
#import "ModuleAViewController.h"
#import "ModuleAService.h"
@BeeHiveService(ModuleAServiceProtocol, ModuleAService)
@implementation ModuleAService
- (void )pushToModuleAViewController{
id<LoginServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(LoginServiceProtocol)];
[moduleAService loginIfNeedWithCompleteBlock:^(BOOL succeed) {
if (succeed) {
UINavigationController *root = (UINavigationController *)[UIApplication sharedApplication].delegate.window.rootViewController;
ModuleAViewController *moduleA = [ModuleAViewController new];
[root pushViewController:moduleA animated:YES];
}
}];
}
@end
不管有没有登录,首先调用登录模块,具体的跳转逻辑被保存在block中,然后传给登录模块,登录完成之后,执行这个block。
登录模块的协议LoginServiceProtocol
//LoginServiceProtocol.h
#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>
@protocol LoginServiceProtocol <NSObject, BHServiceProtocol>
- (void)loginIfNeedWithCompleteBlock:(void (^)(BOOL))completeBlock;
@end
登录模块的响应类
//LoginService.m
#import "LoginViewController.h"
#import "LoginService.h"
@BeeHiveService(LoginServiceProtocol, LoginService)
@implementation LoginService
- (void)loginIfNeedWithCompleteBlock:(void (^)(BOOL))completeBlock{
if ([LoginViewController isLogined]) {
completeBlock(YES);
}else{
LoginViewController *login = [LoginViewController new];
login.completeBlock = completeBlock;
UIViewController *root = [UIApplication sharedApplication].delegate.window.rootViewController;
[root presentViewController:login animated:YES completion:nil];
}
}
@end
如果已经登录,直接执行传入的block;如果没有登录,则弹出登录界面,登录成功之后,执行block。
3. impClass的生命周期
通过上文可知,impClass的对象是最终是由BHServiceManager
类创建的,但是BHServiceManager
类并没有持有impClass的对象,本质上,BHServiceManager
相当于是一个对象工厂。
如果impClass是一个模块的封装类,impClass的对象只在当前作用域有效,超过了这个作用域,这个对象会被释放掉。
如果impClass是一个模块的业务类,则impClass对象的生命周期依赖于模块内部的具体实现了。
如果想长期持有这个impClass对象,通常有两种方式:
1.在模块调用处,强引用被创建的impClass对象。
2.实现BeeHive中BHServiceProtocol
协议的+[singleton]
方法,并返回YES。这样,被创建的impClass对象会被保存在单例[BHContext shareInstance]
中。(如果同时实现了+[shareInstance]
方法,则使用这个方法来创建impClass的对象)
可以使用下列BHContext
的方法来移除保存的impClass对象
- (void)removeServiceWithServiceName:(NSString *)serviceName;
4. 异常处理
BeeHive可以通过下列设置来开启异常模式,在这个模式下,如果遇到BeeHive内部的一些错误,会直接抛出异常。一般在调试模式下,应该开启。生产模式下,应该关闭。
[BeeHive shareInstance].enableException = YES;
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
4.1 注册时异常
注册方式共有三种:
- 使用
BeeHive
类的-[registerService:service:]
- 使用宏
BeeHiveService
- 使用plist文件
注册时,可能存在下列三种情况:
- protocol和impClass对应的协议或类不存在
- protocol和impClass存在,但impClass没有遵循对应的protocol
- protocol和impClass存在,且impClass遵循对应的protocol
方式一 | 方式二 | 方式三 | |
---|---|---|---|
情况一 | 编译时报错 | 启动时crash | 注册成功 |
情况二 | 注册不成功,如果是异常模式,则crash | 注册不成功,如果是异常模式,则crash | 注册成功 |
情况二 | 注册成功 | 注册成功 | 注册成功 |
当注册方法和被注册的模块没有写在一起时,删除了模块,而它的注册方法没有被删除,这个时候就会出现情况一,比如在pod中解除了对模块的依赖。
要避免情况一中的两个报错,最好是将注册方法写在本模块中,比如Module类的-[modInit:]
方法中,这样删除模块的时候,也删除了对应的注册方法。
不管plist文件中protocol和impClass是否存在,是否匹配,只要它们的key符合格式,就会被注册成功。
4.2. 调用时异常
在调用模块时,首先需要创建impClass,一般是通过BeeHive
类的-[createService:]
方法,这个方法需要一个protocol
- (id)createService:(Protocol *)proto;
创建好impClass的对象之后,然后这个对象调用protocol中声明的方法。
在这个调用过程中,可能会遇到下列三种情况:
protocol未注册 | protocol已注册,但对应impClass的类不存在 | protocol已注册,且对应impClass的类存在,但执行的方法没实现 | |
---|---|---|---|
处理结果 | 将impClass的值设置为nil,如果是异常模式,则crash。 | 将impClass的值设置为nil | 抛出异常 |
4.3. 小结
在调试阶段时,可以开启异常模式,这样就能检测一些潜在的问题出来,比如impClass没有遵循protocol、使用未注册的protocol来创建impClass。
关于异常处理,需要注意的是,impClass必须实现被调用的方法。另外,将注册方法写在本模块中。