Jul 10

在oc中实现注解一样的东西

Lrdcq , 2017/07/10 00:22 , 程序 , 閱讀(6707) , Via 本站原創
大家都知道,在java等一些语言中我们可以通过一个@xxxx(aaaa)的形式的语法来装饰我们的类属性方法等一切物体,而oc里面并没有这样的东西。而某一些场景下,为了方便的提供微服务,比如反转注入相关的功能实现,注解是最直接方便的内容注册方式。比如这次我想模仿spring的controller对路由进行内容分发,注解当然也是不二之选。因此,我们尝试在oc中实现注解一样的东西。

首先帖一段最终实现的效果(路由注册和分发):
@implementation KLMIncomingURLDispatcher

@path(@"/home")
- (void)dispatchHome:(KLMURL *)url {
    NSLog(@"KLMIncomingURLDispatcher dispatchHome");
    //
}

@path(@"/detail/{csuCode}")
@or
@path(@"/detail")
@or
@scheme_http
@param(@"csuCode")
- (void)dispatchDetail:(KLMURL *)url csu:(NSInteger)csuCode {
    NSLog(@"KLMIncomingURLDispatcher dispatchDetail");
    //
}

@path(@"/webview")
@or
@scheme_http
- (void)dispatchWebiew:(KLMURL *)url url:(NSString *)openUrl {
    NSLog(@"KLMIncomingURLDispatcher dispatchWebiew");
    //
}

@end

分析

1. 首先不用思考,在oc中实现注解一样的东西肯定是通过宏来实现的。

2. 在oc中实现注解主要有4个方向:代码中注解,类注解,属性注解,方法注解。

其中代码中注解的形式应该是最常见的。你说没见过?libextobjc中那一套@weakify和@strongify你们总用得很爽吧。拆开看看这就是做代码中注解的标准形式了。而类注解,考虑了很久暂时还是误解状态,因此也没法讲了。因此实现属性注解和方法注解是关键。

3. 既然是宏实现的,因此注解宏展开后应该是实际能够在对应的段落实际有效的语法才对。另外为了和面向对象的oc类型进行关联,因此在oc中可以随便乱写的c代码当然很难办了。因此我们可以做的宏很快就限定下来了,在属性中宏展开新的属性,在方法中宏展开新的方法。

实现

首先实现一下方法注解。由于我们知道我们需要展开方法,因此我们很快就能写出这样的宏:
#define path(x) \
- (id)__klmurl_path { \
    return x ;\
}

先无视缺少前面那个@符号,实际写到代码中去,很快就发现问题了——如果一个类的多个方法需要标注这个注解,那么我们声明的__klmurl_path方法就重复了,显然不合理的。因此,每次使用宏新建一个方法时,方法名最好加入一些参数使得名字不一样。参看c的各种内置宏之后,我们注意到一个很常用的:__COUNTER__,每次使用自动+1。这就是我们想要的吧,因此这个宏改成了:
#define __klmurl_macro_concat(A, B) A##B
#define path(x) \
- (id)__klmurl_macro_concat( __klmurl_path_ , __COUNTER__ ) { \
    return x ;\
}

其中##是宏的直接串接,因此我们希望这样可以生成出方法名为__klmurl_path_123这样的方法。然而尴尬了,事与愿违的事这样的宏产生的方法是:__klmurl_path___COUNTER__。WTF?想象也就知道正常情况下宏是先展开再带入,因此把__klmurl_macro_concat展开后就成了那样,无法识别为新的宏。

那有没有什么办法让我们的__COUNTER__先带入再展开呢,有的,仔细阅读宏的展开规则之后,多层展开在没用#和##运算的情况下,会被先带入再展开。这也是常见的多层宏展开解决方法,仔细看看libextobjc中很多这样的写法,因此写成如下这样:
#define __klmurl_macro_concat_inner(A, B) A##B
#define __klmurl_macro_concat(a,b) __klmurl_macro_concat_inner(a, b)
#define path(x) \
- (id)__klmurl_macro_concat( __klmurl_path_ , __COUNTER__ ) { \
    return x ;\
}

这个宏已经完全可以使用了,最后还有一个问题,那个@是哪儿来的,毕竟宏里面本来是不允许有这样的符号的。我们先看看@weakify的@哪儿来的。展开后就发现,原来强项在展开的内容前加了无用的带@的表达式:
#define weakify(...) \
    rac_keywordify \
    metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define rac_keywordify autoreleasepool {}
#define rac_keywordify try {} @catch (...) {}

这样才能很自然的串一个@。当然,这两个是在方法内容里的,那在类声明部分即方法外面,有什么带@的而且没有什么卵用的标示呢。百度了一半天,我们找到一个@compatibility_alias,作用是把一个类映射到另一个名字上以在编译前方便实用。由于这个东西应该在编译后就被干掉了,所以完完全全不影响性能,因此用它就合适了。写出的效果是:
#define path(x) \
compatibility_alias __klmurl_macro_concat(_KLMURL_ , __COUNTER__) KLMURL; \
- (id)__klmurl_macro_concat( __klmurl_path_ , __COUNTER__ ) { \
return x ;\

在实际使用的的时候,还遇到一个比较常见的毛病,就是如果宏输入的x包含逗号的话,宏参数解析会出现错误,比如:
#define path(x) x
//
path(@[@"x",@"y"])

这种情况,中间的逗号会使得输入参数会变为“@[@"x"”,而第二个参数没使用,导致解析出错。因此这种情况下,如果这里正好只有一个参数的话,用范参数和__VA_ARGS__组合正好可以解决这个问题。因此最后写出来的是:
#define path(...) \
compatibility_alias __klmurl_macro_concat(_KLMURL_ , __COUNTER__) KLMURL; \
- (id)__klmurl_macro_concat( __klmurl_path_ , __COUNTER__ ) { \
return __VA_ARGS__ ;\

这一段展开后的代码是:
@path(@"/home")

//展开后如下

@compatibility_alias _KLMURL_0 KLMURL;
- (id) __klmurl_path_1 {
  return @"/home";
}

另外,方法展开ok了,属性展开应该问题也不大了,道理是相通的,而且注册属性自带@,不用我们写无用展开来适应@了。

注解解析

注解写好了,我们当然还要想办法把它读取出来。这里利用到一个很重要的特性是,属性和方法在代码中是有序的,因此代码中的哪儿有一个方法,我在运行时通过runtime取到的方法列表也是相通的顺序。因此,我们直接遍历一个类的方法列表,就可以通过注解展开的方法名的前缀(比如这里是“__klmurl_”)就可以判断出哪些是注解哪些是真实方法。而注解方法后面紧跟的第一个真实方法,就是前面的注解应用的实际方法。处理属性同理。

这里是处理路由注册和分发的注解解析的代码段:
- (void)installDispatcher {
    Class clz = [_delegate class];
    _dispatchs = [NSMutableArray array];
    NSMutableArray<KLMURLDispatcherRule *> *tempRules = [NSMutableArray array];
    KLMURLDispatcherRule *tempRule = nil;
    
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(clz, &methodCount);
    
    for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        
        NSString *name = [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSASCIIStringEncoding];
        if ([name hasPrefix:@"__klmurl_"]) {//如果是一条注解
            name = [name substringFromIndex:9];
            if (!tempRule) tempRule = [KLMURLDispatcherRule create];
            tempRule.path = [_delegate performSelector:method_getName(method)];
            //.......
        } else {//如果是一个真正的方法
            if (tempRule) {
                [tempRules addObject:tempRule];
                tempRule = nil;//清空temp
            }
            if (tempRules.count) {
                KLMURLDispatcherSelector *sel = [[KLMURLDispatcherSelector alloc] init];
                sel.methods = method;
                sel.rules = tempRules;
                [_dispatchs addObject:sel];//绑定方法和temp内容
                
                tempRules = [NSMutableArray array];//清空temp
            }
        }
    }
    free(methods);
    
    NSLog(@"%@",_dispatchs);
}

其中tempRule和tempRules就像一个堆栈一样,存储我们遇到但是还没有归属的注解内容,一旦我们遇到真实方法,就把临时栈的内容丢给它,再把栈清空就ok了。

处理属性注解也是同理咯。

其它疑问

虽然简单的快速实现了,不过还是有一些疑问有待解决:

1. 类注解有没有方法解决。
2. 属性注解现在依赖属性名称来储存信息,有没有更好的方法。
3. 方法注解的@符号解决方案到底是否是最优的。

4. oc中这样设计注解真的合适么?
关键词:objective-c , oc , 注解
logo
lili
2017/07/29 15:26
不错不错