臧成威的博客

不要让惯性影响你的未来

聊一聊RAC

今天来聊一聊RAC,这个在Github上很火热的开源框架,相信很多关注iOS前沿开发的人都或多或少的知道。

关于RAC的介绍和一些概念,我这里就不再啰嗦了,大家可以看 limboy的几篇关于RAC的介绍,很不错。

ReactiveCocoa与Functional Reactive Programming
说说ReactiveCocoa 2
基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client
ReactiveCocoa2实战

此外还有Cocoachina的一系列教程Reactive Cocoa详解

写Blog,在我看来和开发一样,也要讲究重用,引用列位的信息即可,何必黏贴和表达类似的观点。这里我也仅说一下我自己的理解和看法。

相信很多人都尝试过RAC用到一些小的DEMO或者项目中的一个小部分然后就浅尝辄止了。为什么呢?我觉得主要在于,RAC实际推行的一种新的概念大家还没有习惯,那就是函数响应式编程FRP。staltz在一个Gist上倒是给了一个比较全面的解释The introduction to Reactive Programming you’ve been missing,可惜是英文的。

这种FRP的核心就在于数据流,FRP把整个的程序认为是数据流的转换,既不是过程、也不是对象。一个系统先产生信号,转换信号,然后业务和界面再接收和响应信号的变化。所以,这是一个新的概念。如果使用别的概念套用,自然用起来就没那么顺手了。那么我们来看看,这样做的好处是什么呢?

  • 统一的流处理模式,使得不同的组件可以很好的结合起来,例如iOS的UserDefaultNSNotificationCenterKVO这些,在RAC库下面,都是相同的封装,这样就使得上层的业务逻辑实现了大同,进而一切的信号转换合并都可以有效的结合在一起。
  • 处理异步,很多时候,我们对于异步再同步是比较头大的。而RAC中,一个信号的终止,是不局限在一个函数中的。这样我们可以把不同线程、不同时期的状态绑到一个信号上,使得使用者达到一种内聚。和这种内聚,在转换和迭代的过程中是很必要的。
  • 统一的错误处理,从古老的C时代的int DoSomeThine(int input1, int *output1)这种以返回值返回错误,到后来SetErrorStatus(int ErrorCode, const char *message)的线程栈内全局报错机制,还有现在try-catch机制,都有一个很要命的问题,就是错误处理,或者是可以被忽略,或者是让开发变得很烦恼。Java的try-catch机制,相信Java的开发者们一定深有感触。而RAC把错误变得简单了,它对于错误的处理,会随着变化一起传递到顶层,既不会忘记,也不用在中间环节中手动传递来传递去。
  • 逻辑的拆分,在FRP中,逻辑变得相对独立,通常是一个模块,根据一定的变化产生一个信号,亦或是一个模块,根据一个传入的信号,产生一定的转换。这就使得,我们可以只返回我们的直接结果,后期的加工和变更是分离的。对于上层模块,也只关注信号的类型,不关注处于那个线程还是何种手法。

总而言之,RAC给与我们以数据的变化作为出发点,界面与之响应的一套框架。详细的一些技巧,我会在后续的blog中为大家慢慢介绍。

2014-8-18 周一 晴

今天就提一提升级的问题,仅是个人想法。

国内做app的公司,大都有一个特点,就是iOS的版本支持,都比较全面,支持iOS4以上的应用颇为普遍。广大的开发商认为这样可以让目前所有的用户都可以享用到自己的产品,广大的用户认为这样就可以用自己老的产品既不升级硬件,也不升级软件。其实在我看来,长久下去,是对大家都没有好处的。

大家都对某些东西的升级很感兴趣,而且有不给升级就恼怒的情况。这些升级是什么呢?比如职位的升级,工资的升级,职称的升级,家庭地位的升级。可见升级一直都是一个大家比较喜欢和向往的词儿。但是唯独到了软件的升级,就变成了一种痛苦。我们来听一听:

iOS 8要来了,我们又得适配新的分辨率了
API又变了这么多,不晓得又有多少BUG
什么?这个应用要iOS7以上才可以装,真是个垃圾应用,老子不鸟它
大家都用上iOS7了,怎么升级啊,不会呢
为什么要升级,现在用的不是挺好的么
苹果开始审核不兼容iPhone5的分辨率就不让通过了

其实,就单纯iOS来说,用户升级的软件成本已经越来越低了,OTA升级几乎让绝大多数的小白用户都可以在不明觉厉的情况下体验到新的版本。(当然OTA升级后,系统实际上由于垃圾过多速度会稍稍拖慢),那为什么还有那么多人不升级呢?原因有这几个

  1. 很多程序都是兼容的,不升级也可以用新的
  2. 新的系统有BUG,还是等一等稳定的吧(这一等也许就等到你的设备退休)
  3. 喜欢老的界面,不喜欢变化
  4. 从来不关注这些,有升级提示也不看

我们来一个一个分析一下,首先是兼容的问题。这是头等大事,因为软件可以用,当然不需要升级。试想一下,极端情况,如果所有的软件都是只支持最新版,你的iOS6毛应用都装不上,除了把它卖了,就只好升级了。当然这有点太激进,但是也是一种现象。软件的兼容反而是阻碍大家升级的主要原因。反过来看软件的兼容带来什么其他的麻烦了呢?作为开发者,你会发现这些现象:

  1. 有新的API和开发方法了,不过这个不支持iOS 4,还是别看了
  2. 哦,这个API已经不能用了,那我们加个if(response)-else吧
  3. XCode新版已经不支持编译了?我们得想办法装上旧版

对于老的公司来说,有旧的版本的程序倒还好做兼容,只是在老的代码上缝缝补补,又可以穿个几年。但是对于新的公司来说,明明一个新的产品,却套上了旧的衣衫,拖慢了开发效率和执行效率不说,软件的“逼格”似乎也拉低了?什么?开发效率?是的,其实苹果每次发布新版iOS,都从API上做了很大的改进,目的当然不是为了让大家更麻烦,而是让大家更灵活的支持定制和拥有更简单的实现方式。Storyboard、Autolayout、包括最新的Size Classes。无一不是为了开发者而搞出来的,大家之所以那么抵触,实际上是因为浮躁和懒惰。浮躁在不愿意仔细了解,一旦发现问题和难点,就马上批判。懒惰在不想学习和研究新的变化,而懒惰的人更喜欢找借口。

往往见到的就是很多人,揪着新技术的一个小小的缺点,然后把它贬低的一无是处。如果是另外一个开发者因为你的一个bug,把你贬低的一无是处,你非气炸了不可。但是很多人就是这样对待新的API和工具的。一张漂漂亮亮的画作上,滴了墨水,很多人都会关注那瑕疵,遍不会好好看那画作了。其实大家静下心来,好好学习一下新的知识,你会发现,它真的可以帮助你好多。

关于新的系统的bug的问题,和上段刚提到的,是一样的,大家缺少包容,通常会对瑕疵有着格外的在意。大家在生活上或多或少都有着这样的感觉,就是你明明做得很辛苦,但是因为一件小事,被人抓到,然后仿佛你所有的努力都没有了。一份90分的卷纸,很少人在乎你做对了90分的题,掌握了90分的知识,只是看到你做错了10分的题,没有掌握或者马虎了10分的知识点。己所不欲,勿施于人。我们都希望世界可以更公平的对待每一个人,也都希望自己的努力可以得到重视,那么我们就不应该这样对待别人,对待新的,很多人努力的成果,不是批判它的瑕疵,而是体验它带来的便利。

喜欢老的界面这个问题,到是可以理解,萝卜白菜各有所爱,很多人在iOS6升7的时候,都有着不适应,似乎在Win7升8的时候更为强烈。主要还是一个习惯问题,我不觉得喜欢一种风格是值得抨击的。但是为了可以提供更好,更优质的软件,我还是喜欢用新的东西来创造。而且,我觉得,如果新的东西做得好起来,还是会让很多人有所改观,相信现在,很多人也已经接受扁平化的iOS7的风格了。

至于不关注的升级的人,其实很多也不关注软件的,所以开发者为了这个理由不升级,就更没有必要了不是么。

再来说说硬件成本,苹果的软件升级,对于较新的设备,是免费的,这点比微软要好,我们总是可以在不花钱的情况下领略新系统带来的便利。然后这总会在你的设备足够老的时候还回去,一旦你开始喜欢上新的系统,发现你不恩能再升级了,就会敦促你买一个新的设备了。苹果也可以那样长久下去。而苹果的长青,也关乎着千千万万的iOS开发者的腰包。所以,顺应潮流,大家都用新的系统来支持,广大的用户就会踊跃升级,也踊跃换新机,大家的开发也就又简单(因为用新的系统和API,减少兼容的代码)又赚得多了。

总之,面向新的时代,我们越能顺应潮流,就越快乐。

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,把可复用的组件分享出去。

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

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

2014-8-14 周四 晴

每天思绪万千,不停涌入的想法,总需要收集起来,所以,打算从今天开始,写日记。没错,程序员日记,记录每天思绪的点点滴滴,说不定某一天就能用到了,呵呵。

设计模式在程序员的耳中也算耳熟能详了,今天一早刷新Github的时候,发现onevcat start了一个叫做ochococo/Design-Patterns-In-Swift的项目,勾起了我对设计模式的感慨。

虽说如此的如雷贯耳,但是真正用的好的,并且不滥用的还是少数。绝大多说的程序员们还是处于邯郸学步的状态,在未领会设计原则的状态下,把设计模式生搬硬套上的不是少数。

昨天看的CSDN咨询上《假如李小龙是程序员……》也提到过

“所有固定的模式都是缺乏适应性和塑造性的。这是固定模式的死穴。”
注解:没有所谓的秘诀银弹。没有绝对的设计模式。每样事物都有其自身特点。

所以以无法为有法,以无形为有形,才能让设计模式在你的设计中起到重要的作用。

使用jenkins+calabash+cocoapods搭建ios持续集成环境

持续集成

持续集成究竟是什么呢?根据敏捷大师Martin Fowler的定义:

持续集成是一种软件开发实践。在持续集成中,团队成员频繁集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包括自动测试)的检验,以尽快发现集成错误。许多团队发现这种方法可以显著减少集成引起的问题,并可以加快团队合作软件开发的速度。

只要是开发就有分工,哪怕是自己一个写也要分成多个模块。随着项目越来越大,模块也越来越多,各个模块是否可以征程协作就成了问题,有了持续集成,可以有如下好处:

  1. 持续集成中的任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复过程以节省时间、费用和工作量;
  2. 持续集成保障了每个时间点上团队成员提交的代码是能成功集成的。换言之,任何时间点都能第一时间发现软件的集成问题,使任意时间发布可部署的软件成为了可能;
  3. 持续集成还能利于软件本身的发展趋势,这点在需求不明确或是频繁性变更的情景中尤其重要,持续集成的质量能帮助团队进行有效决策,同时建立团队对开发产品的信心。

下面就给大家介绍,如何使用Jenkins+Calabash搭建持续集成开发环境。

环境

XCode 5.0

Mac OS X 10.9.2

Cocoapods

CocoaPods简介

CocoaPods是一个负责管理iOS项目中第三方开源代码的工具。CocoaPods项目的源码在Github上管理。该项目开始于2011年8月12日,经过一年多的发展,现在已经超过1000次提交,并且持续保持活跃更新。开发iOS项目不可避免地要使用第三方开源库,CocoaPods的出现使得我们可以节省设置和更新第三方开源库的时间。

安装Cocoapods

安装Homebrew

Homebrew是Mac下著名的包管理工具,RVM和以后用到xctool都需要用这个来安装,相当于Ubuntu的Apt-get。

安装方法是在命令行中键入

ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"

之后执行环境检查

brew doctor

检查没有错误就可以使用了,如果出现错误,请参考提示进行修正。

确认无误后,可以安装第一个应用curl,一个用来下载的工具。使用命令

brew install curl

安装RVM

虽然Mac默认都带有Ruby,但是有些时候使用起来很麻烦(例如必须使用sudo来安装gem)并且只有一个版本,所以我们使用RVM来管理ruby的版本,ruby是自动化测试工具calabash的运行环境,所以必须要有。

安装方法是命令行中键入

\curl -sSL https://get.rvm.io | bash -s stable

过程中可能需要输入sudo密码。

使用淘宝源替换

sed -i .bak 's!cache.ruby-lang.org/pub/ruby!ruby.taobao.org/mirrors/ruby!' $rvm_path/config/db

安装Ruby

使用rvm下载ruby2.0版本

rvm install 2.0.0

选用2.0.0版本的ruby,并设置为默认

rvm use 2.0.0 --default

使用淘宝源替换gem源

rvm source --add http://ruby.taobao.org/
rvm source --remove https://rubygems.org/

安装Cocoapods

CocoaPods是一个用来帮助我们管理第三方依赖库的工具。它可以解决库与库之间的依赖关系,下载库的源代码,同时通过创建一个Xcode的workspace来将这些第三方库和我们的工程连接起来,供我们开发使用。

通过Gem安装Cocoapods

gem install cocoapods

执行cocoapods的初始化

pod setup

该过程需要到github上拉取specs,速度很慢,可以喝杯咖啡慢慢等

使用Cocoapods

首先创建一个普通项目来演示下如何使用Cocoapods。

创建项目1
创建项目2

之后在命令行里面,进入到你的项目路径

cd /path/to/your/project
pod init

之后会在项目根目录下创建好Podfile,修改下Podfile的内容

1
2
3
4
 # #为Podfile的注释行,Podfile实际上是一个ruby代码段
 platform :ios, "6.0" # platform后面跟平台和版本号,这里是ios6平台
 
 # pod 'MKNetworkKit' 像这样写就可以引入第三方库了,为了简化,这里没有引入任何库

在目录执行pod插件install命令

pod install 

每次使用pod install,它都会到github上更新spec库,耗费了不少时间,可以使用下面的命令跳过这个过程

pod install --no-repo-update

执行之后,会提示没有引入任何的第三方库,不要担心(因为我们真的没有引入)。你会发现目录上多了integration_test.xcworkspace这个工作区文件,以后我们就都使用这个打开项目了。

打开后如图所示
引入Pod后的工程

恭喜您,已经可以正常使用Cocoapods了。下一步就是使用Calabash进行自动化测试了。

Calabash

Calabash是一款开源的跨平台UI测试工具,目前支持iOS和Android。它使用Cucumber作为测试核心,Cucumber是一个在敏捷团队十分流行的自动化的功能测试工具,它使用接近于自然语言的特性文档进行用例的书写和测试,支持多语言和多平台。

安装Calabash

gem install calabash-cucumber

安装Calabash中文支持包

gem install calabash-cucumber-ios-cn

新建集成测试的Target

重新打开工作区,然后选择integration_test这个工程,打开配置,targets中integration_test上右键进行复制。
新建集成测试目标
如果出现Duplicate iPhone Target对话框,选择Duplicate Only就可以,另外一个选项是复制并转换成iPad程序。

之后修改目标的名称
修改目标名称

修改项目配置 修改项目配置

修改scheme
修改scheme1
修改scheme2

共享scheme,目的是在版本管理中,让其他用户也可以获取到这些scheme
共享scheme

这样新的测试目标就创建好了,为什么要创建新的目标呢?

  1. 不希望在发布的产品中包含测试代码
  2. calabash默认启动-cal结尾的目标

引入Calabash包

修改Podfile文件,加入新的pod

1
2
3
target 'integration_test-cal', exclusive: false do
  pod 'Calabash'
end

到命令行里,进入到自己的目录,执行

pod install --no-repo-update

执行成功后,创建用例模板

calabash-ios gen

屏幕会出现

1
2
3
4
5
----------Question----------
I'm about to create a subdirectory called features.
features will contain all your calabash tests.
Please hit return to confirm that's what you want.
---------------------------

按回车确认,就生成了features文件夹,我们的用例和测试配置都在这里了。你可以把features这个文件夹拖到项目中,方便使用xcode直接编辑,注意不要选择任何目标,以为这些文件根本没有必要编译和存到app中。

编写用例

Cucumber是使用gherkin来进行用例描述的,这是一种近乎自然语言的脚本,并且对多语言有很好的支持。具体的语法可以查阅它的官方wiki

这里我们先写一个简单用例,修改features/my_first.feature

1
2
3
4
5
6
7
8
9
# language: zh-CN  
功能: 运行基准测试
  做为一个iOS开发者
  我希望有一个简单的基准测试
  使我可以快速的开启测试

场景: 基准测试
  假如 应用正在运行
  那么 我把应用切到后台3秒

是的,就是这样的用例!你可以书写自然语言来描述一个功能,calabash就使用cucumber帮您测试了,神奇吧。

接下来还需要修改features/step_definitions/calabash_steps.rb,在这里包含中文解析,在最下面加上

require 'calabash-cucumber-ios-cn/calabash_steps.rb'

这个包里面带有中文的功能说明,具体可以看文档

执行用例

激动人心的时刻终于到了,首先编译integration_test-cal这个scheme,然后使用模拟器运行一下,在模拟器打开Accessibility Inspector。模拟器->设置(Settings)->通用(General)->辅助功能(Accessibity)->Accessibility Inspector开启。

打开命令行,进到目录中执行命令

cucumber

可能需要输入密码,之后就看到模拟器重新加载,并按照我们的用例开始自动执行了。

执行结束后,会有下图的结果。

结果截图

恭喜我们的2个步骤都成功了。快点用更多的功能和用例来测试吧^_^。

到这里,每个开发人员都可以通过cucumber命令来对自己写的内容进行测试了,这和我们的持续集成还有一段距离,那么接下来,我们介绍Jenkins这个持续集成web工具,实现真正的持续集成。

Jenkins

Jenkins 是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。

XCTool

使用Jenkins进行持续集成之前,还有一个前提,就是编译这个过程需要自动化,中途用xcode手动点的不行,所以我们需要有命令可以一次编译我们的工程,这里我们使用xctool这个工具,是facebook写的一个集成工具,用来编译和打包程序的。

安装方法是使用homebrew,在命令行执行

brew install xctool

安装好在程序目录下测试一下是否可以编译

 xctool -workspace integration_test.xcworkspace -scheme integration_test-cal -sdk iphonesimulator7.1 clean build

注意这里的sdk每个人可能不同,要根据本机安装的sdk来写 , 查看的方法是执行命令

xcodebuild -showsdks

如果显示** BUILD SUCCEEDED **那么可以进入下一步了。

Jenkins

安装jenkins还是使用brew

brew install jenkins

安装好之后,可以通过使用命令行启动

java -jar /usr/local/opt/jenkins/libexec/jenkins.war

如果想自动启动,需要先执行以下命令,创建启动项

ln -sfv /usr/local/opt/jenkins/*.plist ~/Library/LaunchAgents

可以编辑一下~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist这个文件

open ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist

想要让局域网都可以访问,需要把–httpListenAddress=127.0.0.1改成自己的局域网IP

手动启动启动项可以执行

launchctl load ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist

之后用浏览器就可以访问http://localhost:8080/来登录jenkins了

Jenkins网页截图

Jenkins启动之后,可以配置用户权限,但是我们为了简单,先不配置用户。

Jenkins Plugin

Jenkins有一个很方便的功能,就是可以通过插件形式进行扩展,为了支持我们的持续集成,我们需要先安装必要的插件。

进入Jenkins网页的系统管理->插件管理->高级,找到右下角的“立即获取”就可以获得所有的插件信息了。

Jenkins获取最新插件

更新好之后,在可选插件里面,安装如下插件

Git Server Plugin               #Git的支持,如果用svn就不需要了
Git Client Plugin               #Git的支持,如果用svn就补需要了
Rvm                             #加载RVM环境变量以实用ruby的cucumber命令
Cucumber Test Result Plugin     #解析Cucumber的测试报告

记得安装时勾选更新完自动重启

至此,我们持续集成的所有环境应该都满足了。

托管你的项目

Jenkins一定要从一个地方获得一份软件副本的,所以,要想使用持续集成,必须要有一个版本管理工具,在Jenkins中成为scm,我们的例子使用git,并且我已经将测试工程上传到CODE服务器上,地址在这里:https://code.csdn.net/zangcw/integration_test

创建一个项目

当你的源代码已经在代码托管服务器上之后,现在就可以在jenkins创建一个项目了。
我们创建一个自由风格的软件项目
创建Jenkins项目

并且对其配置
配置Jenkins项目

主要配置如下内容:

  • 源码管理,示例中配置为https://code.csdn.net/zangcw/integration_test.git
  • 构建环境,要勾选RVM,否则没有办法在脚本中执行cucumber这个命令
  • 构建脚本,选择Execute shell,内容如下,请根据需要自行修改
1
2
3
4
cd $WORKSPACE
/usr/local/bin/xctool -workspace integration_test.xcworkspace -scheme integration_test-cal -sdk iphonesimulator7.1 clean build
mkdir -p test-reports
cucumber --format json -o test-reports/cucumber.json
  • 构建后的操作,选择Publish Cucumber test result report,指定报告的目录test-reports/cucumber.json

之后点击应用,即完成了配置
项目创建完毕

立即构建

还在等什么?马上点击立即构建吧。。。
立即构建

等待构建的过程中,我们可以查看控制台输出
控制台输出

模拟器也会在中途弹出,然后自动关闭

构建结束后,我们可以看到构建结果
构建结果

结果展示了变更、由谁触发的构建和测试报告,更多的信息大家可以自行挖掘。总之构建是完成了。

想要进行持续构建,需要设置成每个一段时间自动构建,在Build periodically中配置即可。

下一步该做什么?

在淌通了这一整套流程之后,其实还是有很多事情等着我们来做的,下面是几个例子:

  1. 为Jenkins创建用户管理
  2. 修改脚本,自动存放ipa并上传到特定服务器
  3. 配置构建策略,每日1次,或者多次,或者监听git变化,有上传就构建
  4. 配置邮件策略,使大家及时获得反馈

总之,拥抱集成测试吧。

介绍一下开源项目FastAnimationWithPOP

这是一个很简单的动画框架,基于Facebook的POP库。使用它你就可以在故事版中以0行代码的代价来添加动画了。

Github上地址是 这里.

你可以从这里下载DEMO查看效果.

如果你觉得不错,欢迎在到这里点个赞,方便让更多人注意到它

Demo

功能

  • 使用属性来添加一个动画到任意的View。
  • 在nib或者故事版唤醒时自动执行动画。
  • 也可以随时手动执行动画。
  • 控制动画的细节。
  • 给control绑定一些动画,例如按下松开等状态。
  • 轻松的扩展新的动画,只需要实现FastAnimationProtocolControlFastAnimationProtocolFastAnimationReverseProtocol这几个协议.

环境要求

iOS SDK: iOS 6.0+

XCode版本: 5.0+

如何安装

最好的办法是使用CocoaPods:

  1. 添加这行到你的podfile文件 pod 'FastAnimation'

  2. 安装更新 pod install

如果想要尝试最新的版本,你可以添加这个pod 'FastAnimation', :head.

使用指导

1. 在故事板里使用

你可以通过设置用户自定义运行时属性(user defined runtime attributes)给View添加一个动画。

StroyBoard1

StroyBoard2

下面是一些属性的含义:

UIView的属性

  • animationType

    通过这个属性来指定动画的类型,可以是完整的类名,也可以省略FAAnimation前缀.

  • delay

    执行动画的延时,以秒为单位。

  • animationParams

    这个是各个动画的灵活参数,你可以从动画类的头文件中找到信息,例如下面:

1
2
3
4
5
#define kSpringBounciness   (@"animationParams.springBounciness")
#define kSpringSpeed        (@"animationParams.springSpeed")
#define kDynamicsTension    (@"animationParams.dynamicsTension")
#define kDynamicsFriction   (@"animationParams.dynamicsFriction")
#define kDynamicsMass       (@"animationParams.dynamicsMass")
  • startAnimationWhenAwakeFromNib

    定义是否需要在故事板唤醒的时候就执行动画,默认是YES

UIControl的属性

  • bindingAnimationType

    通过这个属性来指定控件动画的类型,可以是完整的类名,也可以省略FAAnimation前缀.

2. 代码写View的应用

在代码写View中使用FastAnimation同样方便。

你可以设置动画类型等属性,然后执行- (void)startFAAnimation即可。就像这样:

1
2
3
4
5
6
7
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor redColor];
view.animationType = @"Shake";
view.animationParams[@"velocity"] = @-7000;
// You can also set params like this
// [view setValue:@-7000 forKeyPath:kShakeVelocity];
[view startFAAnimation];

还有这些扩展的用法:

1
2
3
4
5
6
// In UIView instance.
- (void)startFAAnimation;
- (void)reverseFAAnimation;
// In UIControl instance.
- (void)bindingFAAnimation;
- (void)unbindingFAAnimation;

3. 定义一个新的动画扩展

轻松的扩展新的动画,只需要实现FastAnimationProtocolControlFastAnimationProtocolFastAnimationReverseProtocol这几个协议.

就像这样:

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
// new_animation.h
@interface FAAnimationNewAnimation : NSObject<FastAnimationProtocol,
FastAnimationReverseProtocol> // Maybe only FastAnimationProtocol

@end
// new_animation.m
@implementation FAAnimationBounceRight

+ (void)performAnimation:(UIView *)view
{
    // some thing you like.
}

+ (void)stopAnimation:(UIView *)view
{
    // some thing you like.
}

+ (void)reverseAnimation:(UIView *)view
{
     // some thing you like.
}

+ (void)stopReverse:(UIView *)view
{
     // some thing you like.
}
@end

4. 一些控制动画的操作

  • 停止动画:

如果想要手动体制,使用下面的方法:

1
2
- (void)stopFAAnimation;
- (void)stopReverseFAAnimation;
  • 嵌套动画:

使用如下方法处理嵌套:

1
2
3
4
- (void)startFAAnimationNested;
- (void)stopFAAnimationNested;
- (void)reverseFAAnimationNested;
- (void)stopReverseFAAnimationNested;

目前已经拥有的动画:

  • 反弹动画(4方向): BounceLeft,BounceRight,BounceUp,BounceDown
  • 放大动画(2方向):ZoomInX,ZoomInY
  • 颤动动画
  • 组动画
  • 放大动画
  • Button的放大效果绑定
  • 更多的动画等着大家的贡献哟!

下一步要做的事

  • 把DEMO和库项目和到同一个Workspace里。
  • 制作更多更好看的DEMO。
  • 假如便捷的转场动画,目前先设法支持iOS7+
  • 确保所有的功能都含有单元测试。
  • 更多更好的动画。
  • 把核心部分和效果部分分离,效果按照iOS5 6 7+来打成不同的包.
  • 支持Swift写扩展.