iOS使用“注解”实现“微服务”路由

galenlin· 2019-11-22
本文来自 jianshu ,作者 galenlin

前言

大家知道 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方法的实现很简单:根据已注册的分发器做转发。

那么如何以“微服务”的形式做注册呢?

我们注意到了 NSObjectload 方法,这个方法会在类被加载的时候执行,显然用来做服务注册是再合适不过了——类被加载了意味着这个类可用,与此同时注册上服务意味着服务也是可用的。这也符合“微服务”启动自注册的理念。

终上,要完成对 "main" 的路由,只需要以下 3 个步骤:

  1. 继承 RouterDispatcher 实现 MainRouterDispatcher

  2. 使用 load 来注册 "main" 服务

  3. 实现 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 神器,两步即可搞定:

  1. 修改 PBRequest 为 _PBRequest (.h 跟 .m文件)

  2. 在修改后的 _PBRequest.h 文件中加上一句:
    @compatibility_alias PBRequest _PBRequest;

于是,其他的 所有 引用到 PBRequest 的地方都不需要改动,甚至接入这个库的使用者依然可以直接使用 PBRequest,因为大家都处于同一个编译环境下。

OK,回到我们的话题上来,我们来使用这个神奇的 @compatibility_alias 完成我们的“注解”:

#define page(_pageID_) \ncompatibility_alias _RouterDispatcher RouterDispatcher; \n+ (void)load { \n    [self registerPage:_pageID_]; \n}

通过上述定义,使用 @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 的实现。