臧成威的博客

不要让惯性影响你的未来

2014-8-15 周五 多云

巧用Objc的动态特性,会使你的工作变得有趣并且减少很多未来的工作量。以前也在论坛里提及过Objc的一些动态特性,当时就有很多朋友讲到,单纯知道这些特性,但是不知道如何应用。那么,今天就把我可以想到的几个小例子分享一下。

很多时候,你的代码会变得极其相似。但是又有略微不同,这时你就会想,如果我可以把不同的部分分离出来该多好,但是往往事与愿违。你仍会发现很多的透传代码挡在你的面前,就算不停的优化,接口还是要一个一个导过去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类OrdersViewModel
- (BOOL)hasOrder
{
  return internalOrderManager.hasOrder;
}

- (NSInteger)orderCount
{
  return internalOrderManager.orderCount;
}

- (NSString *)orderNameForIndex:(NSInteger)index
{
  return [internalOrderManager orderNameForIndex:index];
}

// 以下略很多的透传

这样的封装很好的接口,却因为很多的透传让代码变得臃肿不堪。还存在一些后续维护的问题,当internalOrderManager的接口变化的时候,还需要记得把外面OrdersViewModel这个类也一起变了。

这时就是动态特性大显身手的时候了,如果我们不实现这些方法,运行时会调用一系列的方法来寻求解决方案,这个例子里,最简单的方法就是- (id)forwardingTargetForSelector:(SEL)aSelector,也被称为快速转发,下面是实现。

1
2
3
4
5
6
7
8
// 类OrdersViewModel
- (id)forwardingTargetForSelector:(SEL)aSelector
{
  if ([self.internalOrderManager respondsToSelector:aSelector]) {
      return self.internalOrderManager;
  }
  return nil;
}

这只是本人的一个日记而已,所以相关技术点请大家去Google一下吧。

这么做之后,发现了一个小问题,就是类OrdersViewModel由于没有实现hasOrder,orderCountorderNameForIndex:这三个方法所以有了Warning。消除的方法就是使用类别

1
2
3
4
5
@interface OrdersViewModel(Extended)
- (BOOL)hasOrder;
- (NSInteger)orderCount;
- (NSString *)orderNameForIndex:(NSInteger)index;
@end

这样就好了。

然而很多时候,没有这么乐观和简单,例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 类OrdersViewModel
- (NSString *)orderNameForIndex:(NSInteger)index
{
    return [self.orders[index] name];
}

- (CGFloat)orderPriceForIndex:(NSInteger)index
{
    return [self.orders[index] price];
}
- (NSString *)orderDescForIndex:(NSInteger)index
{
    return [self.orders[index] desc];
}
- (void)orderIncreasePrice:(NSNumber *)value forIndex:(NSInteger)index
{
    [self.orders[index] increasePrice:value];
}
// 以下略更多的透传

这个看起来,又是如此的相似,但是老办法却搞不定,难道我们注定就要透传和维护下去么?当然不是的。

除了快速转发,还有标准消息转发,不过想要实现,还是需要费一番周章的。

所谓的标准转发,就是实现- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector- (void)forwardInvocation:(NSInvocation *)anInvocation这两个方法进行转发。

NSMethodSignature是方法签名,为什么要有这个腻?那是因为selector其实只是一个字符串,从它并不能知道参数的类型和返回值的类型,而方法签名实际上是用来描述参数的类型和返回值的类型的。也就是说,相同的返回值与参数的所有selector的签名其实是一致的。而Objc运行时要根据对象返回的这个签名来抓取参数,然后才会调用- (void)forwardInvocation:(NSInvocation *)anInvocation这个方法。

NSInvocation这个类,和我们平常用的- (id)performSelector:(SEL)aSelector withObject:(id)object的有相同的作用,不过更为多元化,可以管理参数和返回值。

首先来实现- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature* signature = nil;

    NSString *selectorName = NSStringFromSelector(aSelector);

    if ([self selectorNameCheck:selectorName] ) {
        signature = [self targetMethodSignature:selectorName];
    }

    return signature;
}

- (BOOL)selectorNameCheck:(NSString *)selectorName
{
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF BEGINSWITH 'order' AND (SELF ENDSWITH 'forIndex:' OR SELF ENDSWITH 'ForIndex:')"];
    return [predicate evaluateWithObject:selectorName];
}

- (NSMethodSignature *)targetMethodSignature:(NSString *)selectorName
{
    return [Order instanceMethodSignatureForSelector:[self targetSelector:selectorName]];
}
- (SEL)targetSelector:(NSString *)selectorName
{
    NSMutableString *newSelectorName = [selectorName mutableCopy];
    [newSelectorName deleteCharactersInRange:[selectorName rangeOfString:@"forIndex:" options:NSCaseInsensitiveSearch|NSBackwardsSearch]];
    [newSelectorName deleteCharactersInRange:[@"order" rangeOfString:className options:NSCaseInsensitiveSearch]];
    return NSSelectorFromString(newSelectorName.uncapitalizedString); // uncapitalizedString是我通过类别加的新方法,就是把首字母小写,大家试的时候,随便实现以下就可以了。
}

大功告成,我们先用Predicate匹配了是否以order开头,以forIndex或者ForIndex结尾,然后还把order和forIndex去掉,得到了正确的selector,并取得了新的signature。然后是调用部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL seletor = [anInvocation selector];
    NSString *selectorName = NSStringFromSelector(seletor);
    if ([self selectorNameCheck:selectorName] ) {
        NSUInteger argCount = [anInvocation methodSignature].numberOfArguments;
        NSInteger index = -1;
        [anInvocation getArgument:&index atIndex:argCount - 1];

        id instance = self.orders[index];

        NSMethodSignature *newSignature = [self targetMethodSignature:selectorName];
        NSInvocation *newInvocation = [NSInvocation invocationWithMethodSignature:newSignature];
        newInvocation.selector = [self targetSelector:selectorName];
        for (int i = 2; i < argCount - 1; ++i) {
           NSObject *obj = nil;
           [anInvocation getArgument:&obj atIndex:i];
           [newInvocation setArgument:&obj atIndex:i];
       }
       [newInvocation invokeWithTarget:instance];
       if (strcmp(newSignature.methodReturnType, "@") == 0) {
           NSObject *returnValue = nil;
           [newInvocation getReturnValue:&returnValue];
           [anInvocation setReturnValue:&returnValue];
       }
  
        }
    }
}

调用的时候,先取得参数的个数,然后get最后一个参数,我们知道这个参数就是index,然后取得instance。并生成新的invocation,传递参数,传递返回值。一切看起都那么美好。但是,它是不工作的。

如果你跑一遍就会发现了,argCount是不对的,就算强制改对,你也会发现index取不回来。这是为什么呢?原因就出在- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector的实现上。刚才说了,Objc运行时要根据对象返回的这个签名来抓取参数,我们返回的签名,显然没有最后一个参数,所以invocation生成的时候,就没传过来,自然不会生效。可是要怎么解决呢?

这里有一个难题,就是我们需要找到正确的签名,而参数的个数又是不确定的。我用了一个折中的办法,生成了许多假的方法,涵盖了许多的签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)voidMethodforIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 Param:(id)_9 forIndex:(NSInteger)index {}
- (void)voidMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 Param:(id)_9 Param:(id)_10 forIndex:(NSInteger)index {}

- (id)idMethodforIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 Param:(id)_9 forIndex:(NSInteger)index { return nil;}
- (id)idMethodParam:(id)_0 Param:(id)_1 Param:(id)_2 Param:(id)_3 Param:(id)_4 Param:(id)_5 Param:(id)_6 Param:(id)_7 Param:(id)_8 Param:(id)_9 Param:(id)_10 forIndex:(NSInteger)index { return nil;}

看起来好眼晕的说,哈哈。这种方法在C++的模板里也有类似的应用,就是预生成一些内容,考虑一个假定临界值,把所有的可能写出来。有了这些我们就可以这样修改- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector这个方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature* signature = nil;

    NSString *selectorName = NSStringFromSelector(aSelector);

    if ([self selectorNameCheck:selectorName] ) {
        signature = [self targetMethodSignature:selectorName];
        if (signature != nil) {
            int argCount = signature.numberOfArguments;
            NSMutableString *selfSelectorName = nil;
            NSUInteger insertLoc = 0;
            if (strcmp(signature.methodReturnType, "v") == 0) {
                selfSelectorName = @"voidMethodforIndex:".mutableCopy;
                insertLoc = 10;
            } else if (strcmp(signature.methodReturnType, "@") == 0) {
                selfSelectorName = @"idMethodforIndex:".mutableCopy;
                insertLoc = 8;
            } else {
                NSAssert(NO, @"Class %@ method %@ return a value neither void or id", NSStringFromClass(self.proxyClass), selectorName);
            }
            for (int i = 2; i < argCount; ++i) {
                [selfSelectorName insertString:@"Param:" atIndex:insertLoc];
            }
            signature = [self.class instanceMethodSignatureForSelector:NSSelectorFromString(selfSelectorName)];
        }
    }

    return signature;
}

拿到正确的signature之后,我们根据返回值的类型和参数的个数,生成一个selector的字符串,并根据这个得到新的signature。至此,这个改造总算完成了。我们的新的方法可以不用修改代码而实现了。

这时,可能你就会问了,这样搞起来,不是比原来的代码还多了么?事实确实如此,但是我们用这个应付了以后的种种变化。经过合理的封装,不难得到一个可以复用的例子,我也打算开一个新的开源项目ZFastProxy,把可复用的组件分享出去。

很多时候,用动态特性可以让代码变得“神奇”起来,它更加的智能,也让你的开发变得越来越有趣,因为我们是为了未来而开发,而不是当下。以一种以不变应万变的思维来做事是快乐的,它同时也锻炼我们更高层次的抽象和提高了我们开发的技巧。

希望我的例子可以起到抛砖引玉的作用,让更多的人创造出更好的可复用组件。

Comments