阅读本文大概需要 4.55 分钟
前言
大家知道 Objective-C 本身是没有支持注解功能的,但有时使用注解将大幅提高效率,同时让代码更简单易懂。特别是今天要介绍的一个关于“微服务”注册的场景。
什么是“微服务”
微服务是目前后端提的比较多的一个东西,从广义上来说就是一个去中心化的开发模式,通过各个组件的自注册,达到服务分发的效果。
那么跟客户端有什么关系呢?有一个很具体的例子就是“界面路由”。这也是近期大家谈的比较多的一个事(包括我最近也在做),相信很多同学并不陌生。具体的做法就是,我们会对各个界面定义一个ID,比如首页-main、详情页-detail、关于页-about等等,然后通过路由管理器来分发,从而展示指定的Controller。
路由方案一
一个比较简单的做法是建立ID到Controller的映射表,然后根据Controller类名创建对象,进行push操作:
NSString *controllerClazz = [routeConfig clazzForID:@"main"];
Class controllerClass = NSClassFromString(controllerClazz);
// check the controller class...
UIViewController *controller = [[controllerClass alloc] init];
// initialize parameters...
[topVC pushViewController:controller animated:YES];
这种做法的好处是足够简单,但是规则太过于死板,无法根据业务做定制。
一种改进的方案是做一个分发器,建立ID到展示界面方法的映射。
路由方案二
@implement RouterDispatcher
- (void)dispatchMain {
// 定制
[topVC pushViewController:mainViewController animated:YES];
}
- (void)dispatchDetail {
// 定制
[topVC pushViewController:detailViewController animated:YES];
}
@end
这个方案增加了一层中转,方便业务的定制,不过缺点也是明显的,大量的代码都堆砌在了 RouterDispatcher
类里。这意味着每次新增界面或者修改业务都需要改动到这个类,显然作为一个底层核心库,我们希望最大限度地剥离业务以避免改动与保障稳定性。如何去掉这个中心化,就利用到了我们前文所提的“微服务”思想。
微服务路由
作为底层框架,我们不想关心一个ID具体是如何被路由的,我们只提供这种分发能力,具体的业务通过上层注册来实现。以下面的代码为例:
@implement RouterDispatcher
static NSMutableDictionary<NSString */*page*/, NSString */*dispatcherClazz*/> kPages;
+ (void)registerPage:(NSString *)pageID {
if (kPages == nil) {
kPages = [[NSMutableDictionary alloc] init];
}
kPages[page] = NSStringFromClass([self class]);
}
+ (void)dispatchPage:(NSString *)pageID {
NSString *dispatcherClazz = kPages[pageID];
if (dispatcherClazz == nil) {
return;
}
Class dispatcherClass = NSClassFromString(dispatcherClazz);
RouterDispatcher dispatcher = [[dispatcherClass alloc] init];
[dispatcher dispatch];
}
@end
我们只提供了 registerPage
注册方法以及 dispatchPage
分发方法。dispatchPage
方法的实现很简单:根据已注册的分发器做转发。
那么如何以“微服务”的形式做注册呢?
我们注意到了 NSObject
的 load
方法,这个方法会在类被加载的时候执行,显然用来做服务注册是再合适不过了——类被加载了意味着这个类可用,与此同时注册上服务意味着服务也是可用的。这也符合“微服务”启动自注册的理念。
终上,要完成对 "main" 的路由,只需要以下 3 个步骤:
- 继承 RouterDispatcher 实现 MainRouterDispatcher
- 使用
load
来注册 "main" 服务 - 实现
dispatch
完成路由分发
@implement MainRouterDispatcher
+ (void)load {
[self registerPage:@"main"];
}
- (void)dispatch {
// 业务逻辑
[topVC pushViewController:mainViewController animated:YES];
}
@end
好了,现在底层框架基本OK了,但是对于一线开发来说,重复的写 + (void) load
显然是件很啰嗦的事,而且看上去不够醒目,容易被忽略。那么这个时候就是“注解”一展身手的时候了。
使用注解
最终的效果是:
@page(@"main")
- (void)dispatch {
// Do stuff...
}
首先,注解的基本格式为 @annotation(attr)
,官方常见的一些 @
打头的关键字包括:
- @property
- @synthesize
- @dynamic
- @interface
- @implement
我们可以使用宏来做替换,比如定义了:
#define my_property property
就可以使用 @my_property
来替代 @property
,达到一样的效果。
但是这里有一个前提,我们选用的 @xx
需要在 @implement
@end
区间内部来使用,比较符合的是 @synthesize
、@dynamic
,但缺点是其后面必须带上一个属性,比如 @synthesize title;
,如果当前类没有属性就无法定义。
这个时候,我们注意到了一个不常用的 @compatibility_alias
。这个注解是用于类名兼容的,一般开发不会用到。不过在框架开发中可能派上用场。
讲到这里,顺便提下我在开发 Pbind 过程中的一个小插曲。当时Pbind内部实现了一个类 PBRequest 用来统一封装API请求,突然有一天发现 Apple 的私有库 ProtocolBuffer.framework 也有这么一个同名的类,控制台输出警告:“类名冲突,系统会选择其中一个而忽略另一个”。这就尴尬了,谁知道你哪天选哪个呢?保险起见,只能自己换掉,初步的想法是用 _ PBRequest 替换 PBRequest,但是回头想想这个类是面向开发者的,我一大波的 PB 打头类,突然碰上一个 _ PB 打头的不是很奇怪么?偶然间发现了 @compatibility_alias
神器,两步即可搞定:
- 修改 PBRequest 为 _PBRequest (.h 跟 .m文件)
- 在修改后的 _PBRequest.h 文件中加上一句:
@compatibility_alias PBRequest _PBRequest;
于是,其他的 所有 引用到 PBRequest 的地方都不需要改动,甚至接入这个库的使用者依然可以直接使用 PBRequest,因为大家都处于同一个编译环境下。
OK,回到我们的话题上来,我们来使用这个神奇的 @compatibility_alias
完成我们的“注解”:
#define page(_pageID_) \
compatibility_alias _RouterDispatcher RouterDispatcher; \
+ (void)load { \
[self registerPage:_pageID_]; \
}
通过上述定义,使用 @page(@"main")
时将会被展开:
@compatibility_alias _RouterDispatcher RouterDispatcher;
+ (void)load {
[self registerPage:@"main"];
}
相比我们最初给出的代码,这里唯一添加的一句“废话”就是:@compatibility_alias _RouterDispatcher RouterDispatcher;
即允许你在当前环境下使用 _“RouterDispatcher” 类,显然你不会用到它。不过我们也不需要过多地关注它,这句代码是在编译时做的,并不会影响到运行时。
OK,现在我们可以很方便地使用这个注解在任何地方,完成各个界面的路由。更重要的是 load
方法是系统加载类时自动触发的,这意味着你可以把分发器实现在各个地方,包括你所实施的组件化的某一个 Library 或者 Framework 里。
总结
本文介绍了“微服务”的基本思想,通过 load
方法实现了 iOS 微服务组件的自注册。再结合宏与 @compatibility_alias
完成了 iOS 的“注解”功能。达到“注解”实现“微服务”路由的效果:
@page(@"main")
- (void)dispatch {
// Do stuff...
}
Pbind 是一个支持 LiveLoad 的高度可配置化框架,以上代码实践均源自 Pbind 的开发过程,关于注解注册服务的部分还可以参考 Pbind 源码中的 PBAction 以及 PBClient 的实现。