组件化工具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 <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:]方法,将传入的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.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 注册时异常

注册方式共有三种:

  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必须实现被调用的方法。另外,将注册方法写在本模块中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容