组件化工具BeeHive(二):组件化实践

前言

使用BeeHive来进行项目组件化,其实是使用BeeHive来构建一个中间层,通过中间层来解耦各个模块。在文章iOS组件化通用工具浅析有简单介绍过BeeHive的一些组件化思路,本文将更多的从使用者的角度来分析BeeHive。

1. 用法

通过构建中间层来组件化项目,共需要三步:

  1. 创建protocol
  2. 创建impClass
  3. 存储protocol-impClass映射关系

以BeeHive-demo2为例

1.1. 创建protocol

protocol表示模块对外暴露的接口,调用模块时只需要依赖模块对应的protocol,就可以实现对模块的调用。

下列代码表示,模块A对应的协议BHServiceProtocol的定义,调用者可以通过-[getModuleAMainViewController]-[pushToModuleAOneViewController]这两个方法来调用模块A。

// BHServiceProtocol.m

#import "BHServiceProtocol.h"
#import 

@protocol ModuleAServiceProtocol 

- (UIViewController *)getModuleAMainViewController;

- (void)pushToModuleAOneViewController;

@end

这个协议需要继承BeeHive中的协议BHServiceProtocol,协议BHServiceProtocol中定义如下两个可选方法+[singleton:]+[shareInstance]
如果协议对应的响应者impClass实现了这两个方法,并且+[singleton:]方法返回YES,则调用响应类的+[shareInstance]方法来创建响应者对象。否则,直接调用[[implClass alloc] init]来创建对象。

#import 
#import "BHAnnotation.h"
@protocol BHServiceProtocol 

@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:]方法,将传入的protocolimpClass添加到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(二):组件化实践_第1张图片

需要注意的是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 

@protocol ModuleAServiceProtocol 

- (void)pushToModuleAViewController;

@end

模块A的响应类

//ModuleAService.m

#import "ModuleAViewController.h"
#import "ModuleAService.h"

@BeeHiveService(ModuleAServiceProtocol, ModuleAService)
@implementation ModuleAService

- (void )pushToModuleAViewController{
    
    id 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 

@protocol LoginServiceProtocol 

- (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 注册时异常

注册方式共有三种:

  1. 使用BeeHive类的-[registerService:service:]
  2. 使用宏BeeHiveService
  3. 使用plist文件

注册时,可能存在下列三种情况:

  1. protocol和impClass对应的协议或类不存在
  2. protocol和impClass存在,但impClass没有遵循对应的protocol
  3. 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必须实现被调用的方法。另外,将注册方法写在本模块中。

你可能感兴趣的:(组件化工具BeeHive(二):组件化实践)