ReactiveCocoa入门教程 - 第一部分
本文翻译自RayWenderlich ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
GitHub:https://github.com/ReactiveCocoa/ReactiveCocoa
作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。
如果你对上面说的还比较疑惑,那还是继续往下看吧。
ReactiveCocoa结合了几种编程风格:
- 函数式编程(Functional Programming):使用高阶函数,例如函数用其他函数作为参数。
- 响应式编程(Reactive Programming):关注于数据流和变化传播。
所以,你可能听说过ReactiveCocoa被描述为函数响应式编程(FRP)框架。
这就是这篇教程要讲的内容。编程范式是个不错的主题,但是本篇教程的其余部分将会通过一个例子来实践。
###Reactive Playground
通过这篇教程,一个简单的范例应用Reactive Playground ,你将会了解到响应式编程。下载初始工程,然后编译运行一下确保你已经把一切都设置正确了。
Reactive Playground是一个非常简单的应用,它为用户展示了一个登录页。在用户名框输入user,在密码框输入password,然后你就能看到有一只可爱小猫咪的欢迎页了。
现在可以花一些时间来看一下初始工程的代码。很简单,用不了多少时间。
打开RWViewController.m看一下。你多快能找到控制登录按钮是否可用的条件?判断显示/隐藏登录失败label的条件是什么?在这个相对简单的例子里,可能只用一两分钟就能回答这些问题。但是对于更复杂的例子,这些所花的时间可能就比较多了。
使用ReactiveCocoa,可以使应用的基本逻辑变得相当简洁。是时候开始啦。
###添加ReactiveCocoa框架
添加ReactiveCocoa框架最简单的方法就是用CocoaPods。如果你从没用过CocoaPods,那还是先去看看CocoaPods简介这篇教程吧。请至少看完教程中初始化的步骤,这样你才能安装框架。
###开动
就像在介绍中提到的,RAC为应用中发生的不同事件流提供了一个标准接口。在ReactiveCocoa术语中这个叫做信号(signal),由RACSignal
类表示。
打开应用的初始view controller,RWViewController.m ,引入ReactiveCocoa的头文件。
#import <ReactiveCocoa/ReactiveCocoa.h>
不要替换已有的代码,将下面的代码添加到viewDidLoad方法的最后:
[self.usernameTextField.rac_textSignal subscribeNext:^(id x){
NSLog(@"%@", x);
}];
编译运行,在用户名输入框中输几个字。注意console的输出应该和下面的类似。
2013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i
2013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t
2013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th
2013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi
2013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is this
2013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is this
2013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is this m
2013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is this ma
2013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is this mag
2013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is this magi
2013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is this magic
2013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is this magic?
可以看到每次改变文本框中的文字,block中的代码都会执行。没有target-action,没有delegate,只有signal和block。令人激动不是吗?
ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。在本教程的第一部分,我们将会关注next事件。在第二部分,将会学习error和completed事件。
RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,subscribeNext:方法提供的block都会执行。
ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。
原理就说这么多,是时候开始让ReactiveCocoa干活了。
ReactiveCocoa有很多操作来控制事件流。假设你只关心超过3个字符长度的用户名,那么你可以使用filter操作来实现这个目的。把之前加在viewDidLoad中的代码更新成下面的:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
编译运行,在text field只能怪输入几个字,你会发现只有当输入超过3个字符时才会有log。
2013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t
2013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th
2013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi
2013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is this
2013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is this
2013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is this m
2013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is this ma
2013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is this mag
2013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is this magi
2013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is this magic
2013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is this magic?
刚才所创建的只是一个很简单的管道。这就是响应式编程的本质,根据数据流来表达应用的功能。
用图形来表达就是下面这样的:
从上面的图中可以看到,rac_textSignal
是起始事件。然后数据通过一个filter,如果这个事件包含一个长度超过3的字符串,那么该事件就可以通过。管道的最后一步就是subscribeNext:
,block在这里打印出事件的值。
filter操作的输出也是RACSignal
,这点先放到一边。你可以像下面那样调整一下代码来展示每一步的操作。
RACSignal *usernameSourceSignal =
self.usernameTextField.rac_textSignal;
RACSignal *filteredUsername =[usernameSourceSignal
filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}];
[filteredUsername subscribeNext:^(id x){
NSLog(@"%@", x);
}];
RACSignal
的每个操作都会返回一个RACsignal
,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。
注意:ReactiveCocoa大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.com。
###类型转换
如果你之前把代码分成了多个步骤,现在再把它改回来吧。。。。。。。。
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value; // implicit cast
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
在上面的代码中,注释部分标记了将id隐式转换为NSString,这看起来不是很好看。幸运的是,传入block的值肯定是个NSString,所以你可以直接修改参数类型,把代码更新成下面的这样的:
[[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString*text){
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
编译运行,确保没什么问题。
###什么是事件呢?
到目前为止,本篇教程已经描述了不同的事件类型,但是还没有说明这些事件的结构。有意思的是(?),事件可以包括任何事情。
下面来展示一下,在管道中添加另一个操作。把添加在viewDidLoad中的代码更新成下面的:
[[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
return @(text.length);
}]
filter:^BOOL(NSNumber*length){
return[length integerValue] > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
编译运行,你会发现log输出变成了文本的长度而不是内容。
2013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4
2013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5
2013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6
2013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7
2013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8
2013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9
2013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10
2013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11
2013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12
新加的map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber。
来看下面的图片:
能看到map
操作之后的步骤收到的都是NSNumber实例。你可以使用map操作来把接收的数据转换成想要的类型,只要它是个对象。
注意:在上面的例子中text.length返回一个NSUInteger,是一个基本类型。为了将它作为事件的内容,NSUInteger必须被封装。幸运的是Objective-C literal syntax提供了一种简单的方法来封装——@ (text.length)。
现在差不多是时候用所学的内容来更新一下ReactivePlayground应用了。你可以把之前的添加代码都删除了。。。。。。
###创建有效状态信号
首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。把下面的代码添加到RWViewController.m中viewDidLoad的最后面:
RACSignal *validUsernameSignal =
[self.usernameTextField.rac_textSignal
map:^id(NSString *text) {
return @([self isValidUsername:text]);
}];
RACSignal *validPasswordSignal =
[self.passwordTextField.rac_textSignal
map:^id(NSString *text) {
return @([self isValidPassword:text]);
}];
可以看到,上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber
封装的布尔值。
下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法:
[[validPasswordSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}]
subscribeNext:^(UIColor *color){
self.passwordTextField.backgroundColor = color;
}];
从概念上来说,就是把之前信号的输出应用到输入框的backgroundColor属性上。但是上面的用法不是很好。
幸运的是,ReactiveCocoa提供了一个宏来更好的完成上面的事情。把下面的代码直接加到viewDidLoad中两个信号的代码后面:
RAC(self.passwordTextField, backgroundColor) =
[validPasswordSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}];
RAC(self.usernameTextField, backgroundColor) =
[validUsernameSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}];
RAC
宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next
事件,传递过来的值都会应用到该属性上。
你不觉得这种方法很好吗?
在编译运行之前,找到updateUIState方法,把头两行删掉。
self.usernameTextField.backgroundColor =
self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor =
self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
这样就把不相关的代码删掉了。
编译运行,可以发现当输入内容无效时,输入框看起来高亮了,有效时又透明了。
现在的逻辑用图形来表示就是下面这样的。能看到有两条简单的管道,两个文本信号,经过一个map转为表示是否有效的布尔值,再经过一个map转为UIColor,而这个UIColor已经和输入框的背景颜色绑定了。
你是否好奇为什么要创建两个分开的validPasswordSignal
和validUsernameSignal
呢,而不是每个输入框一个单独的管道呢?(?)稍安勿躁,答案就在下面。
原文:Are you wondering why you created separate validPasswordSignal and validUsernameSignal signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!
###聚合信号
目前在应用中,登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。
现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了——validUsernameSignal
和validPasswordSignal
了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。
把下面的代码添加到viewDidLoad的末尾:
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue]&&[passwordValid boolValue]);
}];
上面的代码使用combineLatest:reduce:
方法把validUsernameSignal和validPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。
注意:RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。
现在已经有了合适的信号,把下面的代码添加到viewDidLoad的末尾。这会把信号和按钮的enabled属性绑定。
[signUpActiveSignal subscribeNext:^(NSNumber*signupActive){
self.signInButton.enabled =[signupActive boolValue];
}]
在运行之前,把以前的旧实现删掉。把下面这两个属性删掉。
@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;
把viewDidLoad中的这些也删掉:
// handle text changes for both text fields
[self.usernameTextField addTarget:self
action:@selector(usernameTextFieldChanged)
forControlEvents:UIControlEventEditingChanged];
[self.passwordTextField addTarget:self
action:@selector(passwordTextFieldChanged)
forControlEvents:UIControlEventEditingChanged];
同样把updateUIState、usernameTextFieldChanged和passwordTextFieldChanged方法删掉。
最后确保把viewDidLoad中updateUIState的调用删掉。
编译运行,看看登录按钮。当用户名和密码输入有效时,按钮就是可用的,和以前一样。
现在应用的逻辑就是下面这样的:
上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。
- 分割——信号可以有很多
subscriber
,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。 - 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。
这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。
###响应式的登录
应用目前使用上面图中展示的响应式管道来管理输入框和按钮的状态。但是按钮按下的处理用的还是action,所以下一步就是把剩下的逻辑都替换成响应式的。
在storyboard中,登录按钮的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是绑定的。下面会用响应的方法替换,所以首先要做的就是断开当前的storyboard action。
打开Main.storyboard,找到登录按钮,按住ctrl键单击,打开outlet/action连接框,然后点击x来断开连接。如果你找不到的话,下图中红色箭头指示的就是删除按钮。
你已经知道了ReactiveCocoa框架是如何给基本UIKit控件添加属性和方法的了。目前你已经使用了rac_textSignal
,它会在文本发生变化时产生信号。为了处理按钮的事件,现在需要用到ReactiveCocoa为UIKit添加的另一个方法,rac_signalForControlEvents
。
现在回到RWViewController.m,把下面的代码添加到viewDidLoad的末尾:
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
上面的代码从按钮的UIControlEventTouchUpInside事件创建了一个信号,然后添加了一个订阅,在每次事件发生时都会输出log。
编译运行,确保的确有log输出。按钮只在用户名和密码框输入有效时可用,所以在点击按钮前需要在两个文本框中输入一些内容。
可以看到Xcode控制台的输出和下面的类似:
2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked
现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。那么问题就来了,打开RWDummySignInService.h,看一下接口:
typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的subscribeNext: blcok里直接调用这个方法,但是为什么你要这么做?(?)
注意:本教程为了简便使用了一个假的service,所以它不依赖任何外部API。但你现在的确遇到了一个问题,如何使用这些不是用信号表示的API呢?
###创建信号
幸运的是,把已有的异步API用信号的方式来表示相当简单。首先把RWViewController.m中的signInButtonTouched:删掉。你会用响应式的的方法来替换这段逻辑。
还是在RWViewController.m中,添加下面的方法:
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
上面的方法创建了一个信号,使用用户名和密码登录。现在分解来看一下。
上面的代码使用RACSignal的createSignal:
方法来创建信号。方法的入参是一个block,这个block描述了这个信号。当这个信号有subscriber
时,block里的代码就会执行。
block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next
事件,或者用error\complete
事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。
这个block的返回值是一个RACDisposable
对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。
可以看到,把一个异步API用信号封装是多简单!
现在就来使用这个新的信号。把之前添加在viewDidLoad中的代码更新成下面这样的:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];
上面的代码使用map方法,把按钮点击信号转换成了登录信号。subscriber输出log。
编译运行,点击登录按钮,查看Xcode的控制台,等等,输出的这是个什么鬼?
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
<RACDynamicSignal: 0xa068a00> name: +createSignal:
没错,你已经给subscribeNext:的block传入了一个信号,但传入的不是登录结果的信号。
下图展示了到底发生了什么:
当点击按钮时,rac_signalForControlEvents
发送了一个next事件
(事件的data是UIButton)。map操作创建并返回了登录信号,这意味着后续步骤都会收到一个RACSignal。这就是你在subscribeNext:这步看到的。
上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。
###信号中的信号
解决的方法很简单,只需要把map操作改成flattenMap
就可以了:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];
这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。
编译运行,注意控制台,现在应该输出登录是否成功了。
2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0
2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1
还不错。
现在已经完成了大部分的内容,最后就是在subscribeNext步骤里添加登录成功后跳转的逻辑。把代码更新成下面的:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
BOOL success =[signedIn boolValue];
self.signInFailureText.hidden = success;
if(success){
[self performSegueWithIdentifier:@"signInSuccess" sender:self];
}
}];
subscribeNext: block从登录信号中取得结果,相应地更新signInFailureText是否可见。如果登录成功执行导航跳转。
编译运行,应该就能再看到可爱的小猫啦!喵~
你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。
这个逻辑应该怎么添加呢?改变按钮的可用状态并不是转换(map)、过滤(filter)或者其他已经学过的概念。其实这个就叫做“副作用”,换句话说就是在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。
添加附加操作(Adding side-effects
)
把代码更新成下面的:
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x){
self.signInButton.enabled =NO;
self.signInFailureText.hidden =YES;
}]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
self.signInButton.enabled =YES;
BOOL success =[signedIn boolValue];
self.signInFailureText.hidden = success;
if(success){
[self performSegueWithIdentifier:@"signInSuccess" sender:self];
}
}];
你可以看到doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。
上面的doNext: block把按钮置为不可点击,隐藏登录失败提示。然后在subscribeNext: block里重新把按钮置为可点击,并根据登录结果来决定是否显示失败提示。
之前的管道图就更新成下面这样的:
编译运行,确保登录按钮的可点击状态和预期的一样。
现在所有的工作都已经完成了,这个应用已经是响应式的啦。
如果你中途哪里出了问题,可以下载最终的工程(依赖库都有),或者在Github上找到这份代码,教程中的每一次编译运行都有对应的commit。
注意:在异步操作执行的过程中禁用按钮是一个常见的问题,ReactiveCocoa也能很好的解决。
RACCommand
就包含这个概念,它有一个enabled
信号,能让你把按钮的enabled属性和信号绑定起来。你也许想试试这个类。
###总结
希望本教程为你今后在自己的应用中使用ReactiveCocoa打下了一个好的基础。你可能需要一些练习来熟悉这些概念,但就像是语言或者编程,一旦你夯实基础,用起来也就很简单了。ReactiveCocoa的核心就是信号,而它不过就是事件流。还能再更简单点吗?
在使用ReactiveCocoa后,我发现了一个有趣的事情,那就是你可以用很多种不同的方法来解决同一个问题。你可以用教程中的例子试试,调整一下信号,改改信号的分割和聚合。
ReactiveCocoa的主旨是让你的代码更简洁易懂,这值得多想想。我个人认为,如果逻辑可以用清晰的管道、流式语法来表示,那就很好理解这个应用到底干了什么了。
在本系列教程的第二部分,你将会学到诸如错误处理、在不同线程中执行代码等高级用法。
HTTP协议详解
Author :Jeffrey
##引言
HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(Next Generation of HTTP)的建议已经提出。
HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET
、HEAD
、POST
。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type
加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
##一、HTTP协议详解之URL篇
http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。
HTTP URL (URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息)的格式如下:
http://host[“:”port][abs_path]
http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。
eg:
1、输入:www.guet.edu.cn
浏览器自动转换成:http://www.guet.edu.cn/
2、http:192.168.0.116:8080/index.jsp
##二、HTTP协议详解之请求篇
http请求由三部分组成,分别是:请求行、消息报头、请求正文
1、请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:Method Request-URI HTTP-Version CRLF
其中 Method表示请求方法;Request-URI是一个统一资源标识符;HTTP-Version表示请求的HTTP协议版本;CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。
请求方法(所有方法全为大写)有多种,各个方法的解释如下:
GET 请求获取Request-URI所标识的资源
POST 在Request-URI所标识的资源后附加新的数据
HEAD 请求获取由Request-URI所标识的资源的响应消息报头
PUT 请求服务器存储一个资源,并用Request-URI作为其标识
DELETE 请求服务器删除Request-URI所标识的资源
TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT 保留将来使用
OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求
应用举例:
GET方法:在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源,eg:GET /form.html HTTP/1.1 (CRLF)
POST方法要求被请求服务器接受附在请求后面的数据,常用于提交表单。
eg:POST /reg.jsp HTTP/ (CRLF)
Accept:image/gif,image/x-xbit,… (CRLF)
…
HOST:www.guet.edu.cn (CRLF)
Content-Length:22 (CRLF)
Connection:Keep-Alive (CRLF)
Cache-Control:no-cache (CRLF)
(CRLF) //该CRLF表示消息报头已经结束,在此之前为消息报头
user=jeffrey&pwd=1234 //此行以下为提交的数据
HEAD方法与GET方法几乎是一样的,对于HEAD请求的回应部分来说,它的HTTP头部中包含的信息与通过GET请求所得到的信息是相同的。利用这个方法,不必传输整个资源内容,就可以得到Request-URI所标识的资源的信息。该方法常用于测试超链接的有效性,是否可以访问,以及最近是否更新。
2、请求报头后述
3、请求正文(略)
##三、HTTP协议详解之响应篇
在接收和解释请求消息后,服务器返回一个HTTP响应消息。
HTTP响应也是由三个部分组成,分别是:状态行
、消息报头
、响应正文
1、状态行格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。
状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:
1xx:指示信息–表示请求已接收,继续处理
2xx:成功–表示请求已被成功接收、理解、接受
3xx:重定向–要完成请求必须进行更进一步的操作
4xx:客户端错误–请求有语法错误或请求无法实现
5xx:服务器端错误–服务器未能实现合法的请求
常见状态代码、状态描述、说明:
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能 被服务器所理解
401 Unauthorized //请求未经授权,这个状态代 码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供 服务
404 Not Found //请求资源不存在,eg:输入了错 误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
eg:HTTP/1.1 200 OK (CRLF)
2、响应报头后述
3、响应正文就是服务器返回的资源的内容
##四、HTTP协议详解之消息报头篇
HTTP消息由客户端到服务器的请求和服务器到客户端的响应组成。请求消息和响应消息都是由开始行(对于请求消息,开始行就是请求行,对于响应消息,开始行就是状态行),消息报头(可选),空行(只有CRLF的行),消息正文(可选)组成。
HTTP消息报头包括普通报头
、请求报头
、响应报头
、实体报头
。
每一个报头域都是由名字+“:”+空格+值 组成,消息报头域的名字是大小写无关的。
1、普通报头
在普通报头中,有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息。
eg:
Cache-Control
用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制),HTTP1.0使用的类似的报头域为Pragma。
请求时的缓存指令包括:no-cache(用于指示请求或响应消息不能缓存)、no-store、max-age、max-stale、min-fresh、only-if-cached;
响应时的缓存指令包括:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage.
eg:为了指示IE浏览器(客户端)不要缓存页面,服务器端的JSP程序可以编写如下:response.sehHeader(“Cache-Control”,”no-cache”);
//response.setHeader(“Pragma”,”no-cache”);作用相当于上述代码,通常两者//合用
这句代码将在发送的响应消息中设置普通报头域:Cache-Control:no-cache
Date普通报头域表示消息产生的日期和时间
Connection普通报头域允许发送指定连接的选项。例如指定连接是连续,或者指定“close”选项,通知服务器,在响应完成后,关闭连接
2、请求报头
请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。
常用的请求报头
Accept
Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
Accept-Charset
Accept-Charset请求报头域用于指定客户端接受的字符集。eg:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。
Accept-Encoding
Accept-Encoding请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。
Accept-Language
Accept-Language请求报头域类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。
Authorization
Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
Host(发送请求时,该报头域是必需的)
Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg:
我们在浏览器中输入:http://www.guet.edu.cn/index.html
浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.guet.edu.cn
此处使用缺省端口号80,若指定了端口号,则变成:Host:www.guet.edu.cn:指定端口号
User-Agent
我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。
请求报头举例:
GET /form.html HTTP/1.1 (CRLF)
Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,/ (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip,deflate (CRLF)
If-Modified-Since:Wed,05 Jan 2007 11:21:25 GMT (CRLF)
If-None-Match:W/”80b1a4c018f3c41:8317″ (CRLF)
User-Agent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF)
Host:www.guet.edu.cn (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)
3、响应报头
响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。
常用的响应报头
Location
Location响应报头域用于重定向接受者到一个新的位置。Location响应报头域常用在更换域名的时候。
Server
Server响应报头域包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。下面是
Server响应报头域的一个例子:
Server:Apache-Coyote/1.1
WWW-Authenticate
WWW-Authenticate响应报头域必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。
eg:WWW-Authenticate:Basic realm=”Basic Auth Test!” //可以看出服务器对请求资源采用的是基本验证机制。
4、实体报头
请求和响应消息都可以传送一个实体。一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(eg:有无实体正文)和请求所标识的资源的元信息。
常用的实体报头
Content-Encoding
Content-Encoding实体报头域被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding这样用于记录文档的压缩方法,eg:Content-Encoding:gzip
Content-Language
Content-Language实体报头域描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读
者。eg:Content-Language:da
Content-Length
Content-Length实体报头域用于指明实体正文的长度,以字节方式存储的十进制数字来表示。
Content-Type
Content-Type实体报头域用语指明发送给接收者的实体正文的媒体类型。eg:
Content-Type:text/html;charset=ISO-8859-1
Content-Type:text/html;charset=GB2312
Last-Modified
Last-Modified实体报头域用于指示资源的最后修改日期和时间。
Expires
Expires实体报头域给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,我们可以使用Expires实体报头域指定页面过期的时间。eg:Expires:Thu,15 Sep 2006 16:23:12 GMT
HTTP1.1的客户端和缓存必须将其他非法的日期格式(包括0)看作已经过期。eg:为了让浏览器不要缓存页面,我们也可以利用Expires实体报头域,设置为0,jsp中程序如下:response.setDateHeader(“Expires”,”0″);
##五、利用telnet观察http协议的通讯过程
实验目的及原理:
利用MS的telnet工具,通过手动输入http请求信息的方式,向服务器发出请求,服务器接收、解释和接受请求后,会返回一个响应,该响应会在telnet窗口上显示出来,从而从感性上加深对http协议的通讯过程的认识。
实验步骤:
1、打开telnet
1.1 打开telnet
运行–>cmd–>telnet
1.2 打开telnet回显功能
set localecho
2、连接服务器并发送请求
2.1 open www.guet.edu.cn 80 //注意端口号不能省略
HEAD /index.asp HTTP/1.0
Host:www.guet.edu.cn
/*我们可以变换请求方法,请求桂林电子主页内容,输入消息如下*/
open www.guet.edu.cn 80
GET /index.asp HTTP/1.0 //请求资源的内容
Host:www.guet.edu.cn
2.2 open www.sina.com.cn 80 //在命令提示符号下直接输入telnet www.sina.com.cn 80
HEAD /index.asp HTTP/1.0
Host:www.sina.com.cn
3 实验结果:
3.1 请求信息
2.1得到的响应是:
HTTP/1.1 200 OK //请求成功
Server: Microsoft-IIS/5.0 //web服务器
Date: Thu,08 Mar 200707:17:51 GMT
Connection: Keep-Alive
Content-Length: 23330
Content-Type: text/html
Expries: Thu,08 Mar 2007 07:16:51 GMT
Set-Cookie: ASPSESSIONIDQAQBQQQB=BEJCDGKADEDJKLKKAJEOIMMH; path=/
Cache-control: private
//资源内容省略
3.2 请求信息
2.2得到的响应是:
HTTP/1.0 404 Not Found //请求失败
Date: Thu, 08 Mar 2007 07:50:50 GMT
Server: Apache/2.0.54
Last-Modified: Thu, 30 Nov 2006 11:35:41 GMT
ETag: “6277a-415-e7c76980″
Accept-Ranges: bytes
X-Powered-By: mod_xlayout_jh/0.0.1vhs.markII.remix
Vary: Accept-Encoding
Content-Type: text/html
X-Cache: MISS from zjm152-78.sina.com.cn
Via: 1.0 zjm152-78.sina.com.cn:80
X-Cache: MISS from th-143.sina.com.cn
Connection: close
失去了跟主机的连接
按任意键继续…
4 .注意事项:
1、出现输入错误,则请求不会成功。
2、报头域不分大小写。
3、更深一步了解HTTP协议,可以查看RFC2616,在http://www.letf.org/rfc上找到该文件。
4、开发后台程序必须掌握http协议
##六、HTTP协议相关技术补充
1、基础:
高层协议有:文件传输协议FTP、电子邮件传输协议SMTP、域名系统服务DNS、网络新闻传输协议NNTP和HTTP协议等
中介由三种:代理(Proxy)、网关(Gateway)和通道(Tunnel),一个代理根据URI的绝对格式来接受请求,重写全部或部分消息,通过 URI的标识把已格式化过的请求发送到服务器。网关是一个接收代理,作为一些其它服务器的上层,并且如果必须的话,可以把请求翻译给下层的服务器协议。一 个通道作为不改变消息的两个连接之间的中继点。当通讯需要通过一个中介(例如:防火墙等)或者是中介不能识别消息的内容时,通道经常被使用。
代理(Proxy):一个中间程序,它可以充当一个服务器,也可以充当一个客户机,为其它客户机建立请求。请求是通过可能的翻译在内部或经过传递到其它的 服务器中。一个代理在发送请求信息之前,必须解释并且如果可能重写它。代理经常作为通过防火墙的客户机端的门户,代理还可以作为一个帮助应用来通过协议处 理没有被用户代理完成的请求。
网关(Gateway):一个作为其它服务器中间媒介的服务器。与代理不同的是,网关接受请求就好象对被请求的资源来说它就是源服务器;发出请求的客户机并没有意识到它在同网关打交道。
网关经常作为通过防火墙的服务器端的门户,网关还可以作为一个协议翻译器以便存取那些存储在非HTTP系统中的资源。
通道(Tunnel):是作为两个连接中继的中介程序。一旦激活,通道便被认为不属于HTTP通讯,尽管通道可能是被一个HTTP请求初始化的。当被中继 的连接两端关闭时,通道便消失。当一个门户(Portal)必须存在或中介(Intermediary)不能解释中继的通讯时通道被经常使用。
2、协议分析的优势—HTTP分析器检测网络攻击
以模块化的方式对高层协议进行分析处理,将是未来入侵检测的方向。
HTTP及其代理的常用端口80、3128和8080在network部分用port标签进行了规定
3、HTTP协议Content Lenth限制漏洞导致拒绝服务攻击
使用POST方法时,可以设置ContentLenth来定义需要传送的数据长度,例如ContentLenth:999999999,在传送完成前,内 存不会释放,攻击者可以利用这个缺陷,连续向WEB服务器发送垃圾数据直至WEB服务器内存耗尽。这种攻击方法基本不会留下痕迹。
http://www.cnpaf.net/Class/HTTP/0532918532667330.html
4、利用HTTP协议的特性进行拒绝服务攻击的一些构思
服务器端忙于处理攻击者伪造的TCP连接请求而无暇理睬客户的正常请求(毕竟客户端的正常请求比率非常之小),此时从正常客户的角度看来,服务器失去响应,这种情况我们称作:服务器端受到了SYNFlood攻击(SYN洪水攻击)。
而Smurf、TearDrop等是利用ICMP报文来Flood和IP碎片攻击的。本文用“正常连接”的方法来产生拒绝服务攻击。
19端口在早期已经有人用来做Chargen攻击了,即Chargen_Denial_of_Service,但是!他们用的方法是在两台Chargen 服务器之间产生UDP连接,让服务器处理过多信息而DOWN掉,那么,干掉一台WEB服务器的条件就必须有2个:1.有Chargen服务2.有HTTP 服务
方法:攻击者伪造源IP给N台Chargen发送连接请求(Connect),Chargen接收到连接后就会返回每秒72字节的字符流(实际上根据网络实际情况,这个速度更快)给服务器。
5、Http指纹识别技术
Http指纹识别的原理大致上也是相同的:记录不同服务器对Http协议执行中的微小差别进行识别.Http指纹识别比TCP/IP堆栈指纹识别复杂许 多,理由是定制Http服务器的配置文件、增加插件或组件使得更改Http的响应信息变的很容易,这样使得识别变的困难;然而定制TCP/IP堆栈的行为 需要对核心层进行修改,所以就容易识别.
要让服务器返回不同的Banner信息的设置是很简单的,象Apache这样的开放源代码的Http服务器,用户可以在源代码里修改Banner信息,然 后重起Http服务就生效了;对于没有公开源代码的Http服务器比如微软的IIS或者是Netscape,可以在存放Banner信息的Dll文件中修 改,相关的文章有讨论的,这里不再赘述,当然这样的修改的效果还是不错的.另外一种模糊Banner信息的方法是使用插件。
常用测试请求:
1:HEAD/Http/1.0发送基本的Http请求
2:DELETE/Http/1.0发送那些不被允许的请求,比如Delete请求
3:GET/Http/3.0发送一个非法版本的Http协议请求
4:GET/JUNK/1.0发送一个不正确规格的Http协议请求
Http指纹识别工具Httprint,它通过运用统计学原理,组合模糊的逻辑学技术,能很有效的确定Http服务器的类型.它可以被用来收集和分析不同Http服务器产生的签名。
6、其他:为了提高用户使用浏览器时的性能,现代浏览器还支持并发的访问方式,浏览一个网页时同时建立多个连接,以迅速获得一个网页上的多个图标,这样能更快速完成整个网页的传输。
HTTP1.1中提供了这种持续连接的方式,而下一代HTTP协议:HTTP-NG更增加了有关会话控制、丰富的内容协商等方式的支持,来提供
更高效率的连接。
Xcode升级后插件失效的原理与修复办法
Xcode 的插件大大丰富了 Xcode 的功能,而且有了 Alcatraz
,插件的管理也非常容易,像我这种 Vim 党完全离不开 XVim。但是有个非常恼人的问题:一旦升级 Xcode ,插件就失效!
之前 Xcode 升级到6.2的时候遇到过插件失效的问题,Google 之后把一段很长命令复制到 Terminal 后运行一下即可,当时一看解决了,顿时觉得满足感爆棚,自己可以拯救地球了~就没有再深入,结果升级到6.3时又遇到了。“同样的招式对圣斗士是不能使用第二次的!”,同样的坑对有节操的程序员是不能掉进去第二次的!因此这一次一定要搞清楚为什么会这样,以后再次遇到了如何解决。
问题原因
Xcode 的插件放置在 ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins
目录下,为 .xcplugin 格式。通过 Show Content 可以看到 xcplugin 中存在一个 Info.plist,其中有一项为 DVTPlugInCompatibilityUUIDs
,而这就是插件失效的原因。
由于 Apple 没有公开插件开发的相关资料,这里我只能通过命名跟值猜测 DVTPlugInCompatibilityUUIDs 的作用:插件通过 DVTPlugInCompatibilityUUIDs 来指定能够运行此插件的 Xcode 版本。因此,DVTPlugInCompatibilityUUIDs 中存放的是 Xcode 版本对应的 UUID,Xcode 在启动加载控件时,将当前 UUID 同插件 Info.plist 中 DVTPlugInCompatibilityUUIDs 存放的 UUID 数组进行匹配,如果没有匹配项,说明此插件无法在该版本的 Xcode 运行,插件也就失效了。
解决办法
解决办法非常简单:将当前版本的 UUID 加到 DVTPlugInCompatibilityUUIDs 中即可。但是插件比较多(1个及以上)的情况下,一个个的打开修改非常无聊跟低效,作为“懒惰”的程序员,这时候就要用上命令行,让重复劳动自动化。思路为将命令分为两部分:
通过 find 命令在插件目录下找到所有插件的 Info.plist 文件。
通过 xargs 命令对上一步的搜索结果进行“for 循环”(就这样理解吧),针对每一个 Info.plist 文件,利用 defaults write 命令将当前版本的 UUID 加到 DVTPlugInCompatibilityUUIDs 中。
此时问题来了,挖掘机技术。。。不对,是如何获取当前版本 Xcode 的 UUID 呢?首先关掉 Xcode,打开 Terminal,输入 tail -f /var/log/system.log,再次打开 Xcode,就能看到如下 log 信息:
[MT] PluginLoading: Required plug-in compatibility UUID 9F75337B-21B4-4ADC-B558-F9CADF7073A7 for plug-in at path ‘~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/Alcatraz.xcplugin’ not present in DVTPlugInCompatibilityUUIDs
可以看到,log 信息表明 Xcode 加载插件失败的原因,并且能够看到当前版本(6.3)Xcode 的 UUID 为 9F75337B-21B4-4ADC-B558-F9CADF7073A7。经过 @Kyrrr 的提醒,有一种更好的方式来获取当前版本 Xcode 的 UUID:通过 defaults read 命令从 Xcode 的 Info.plist 读取 DVTPlugInCompatibilityUUID。
最终的命令为:
find ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins -name Info.plist -maxdepth 3 | xargs -I{} defaults write {} DVTPlugInCompatibilityUUIDs -array-add `defaults read /Applications/Xcode.app/Contents/Info.plist DVTPlugInCompatibilityUUID`
在 Terminal 中运行上述命令就解决了插件失效的问题,在插件 Info.plist 的 DVTPlugInCompatibilityUUIDs 中也能看到新增的 UUID 了。
from:http://joeshang.github.io/2015/04/10/fix-xcode-upgrade-plugin-invalid/
让工程 Build号自动增加方法
FROM:http://swiftcn.io/topics/31
不知多久以前,iTunesconnect改版后上传APP就每次都要不一样的build号了。所以每次都得改build号很痛快,后面找出了一个办法,贡献给大家
方法很简单,就是利用xcode自带的Build Phases阶增加一段脚本,让每次build都自动更改下build号,自动加一
例如我的版本号是2.0,build号为了可追踪,就设定为2.0.xxx 脚本的作用就是每次把xxx加一,然后替换回去
直接贴代码
#!/bin/bash
#coding utf-8
if [ "$CONFIGURATION" != "Debug" ]
then
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$INFOPLIST_FILE")
appVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$INFOPLIST_FILE")
buildNumber=`echo $buildNumber|sed 's/.*\./''/'`
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $appVersion.$buildNumber" "$INFOPLIST_FILE"
fi
效果如图
PS:由于debug阶段的build是没必要修改build号的,所以增加了一行判断if [ "$CONFIGURATION" != "Debug" ]
好了,以后再也不用手动修改了,如果想更加完善一点,也可以修改build号码为svn版本号或者git的提交号等
Xcode的断点调试技巧
转载:http://supermao.cn/duan-dian-shen-ru-liao-jie/
编码不能没调试,调试不能没断点(Break Point)。XCode的断点功能也是越来越强大。
##基本断点
如下图,这种是最常用的断点,也是最容易设置。左键点击一下就可以设置。
##编辑断点
断点是可以编辑的。
断点有下面几个属性可以设置:
- Condition
- Ignore
- Action
- Options
###Condition
这里可以输入条件表达式,满足条件的时候断点就会生效。例如上面输入a == 50。这个是非常有用的设置,特别在循环体内调试的时候,用着真的是爽。
###Ingore
在这里可以设置忽略断点次数。
###Action
Action是这里最复杂的,最强大的功能了。Action有6中类型。如下图
- AppleScript
- Capture GPU Frame
- Debugger Command
- Log Message
- Shell Command
- Sound
常用的就是Log Message和Debugger Command
##Log Message
在这里填写的东西可以打印到控制台。例如我做了如下设置:
%B会打印断点的名字,%H会打印断点的调用次数,@@中间可以输入表达式。 上面的设置在控制台的输出如下:
##Debugger Command
这里可以输入调试命令,也就是po(打印对象信息),bt(打印函数栈),expression(表达式)这些调试命令。看图就明白了:
image 控制台输出如下:
##Options
勾选Automatically continue after evaluating actions之后程序会在断点产生后继续运行。这个属性是相当有用的,可以输入调试信息至于不暂停程序。
出了上面的基本断点外,XCode还提供了下面四种断点,需要点击断点面板左下角的+号添加。
- Exception Breakpoint
- OpenGL ES Error Breakpoint
- Symbolic Breakpoint
- Test Failure Breakpoint
##Exception Breakpoint
Exception Breakpoint是一个非常有用的断点项。正如名字所示,当程序抛出异常的时候就回产生断点。通常程序崩溃会停在崩溃的地方,但有时候并不能准确停在引起异常的地方。比如数组越界!比如我下图所示,会引起数组越界访问。
程序运行的时候就会崩溃。但是崩溃停在了main函数里面,就算看了栈信息也不能马上定位到到底是那个数组越界访问了。为什么崩溃不能停在数组越界哪里?这是因为数组越界访问不一定会导致程序崩溃的,数组越界访问会导致异常抛出,而抛出的异常没有得到处理才会导致程序崩溃。因此最后会导致崩溃停在CoreFoundation框架里面。这个时候就需要设置Exception Breakpoint产生断点来定位错误了。
##OpenGL ES Error Breakpoint
这个主要是OpenGL ES的断点调试,这个个人没用到过。
##Symbolic Breakpoint
Symbolic Breakpoint,符号断点,真的是调试神器啊。当程序运行到特定符号的时候就会产生断点。通过这种方式添加断点,就不需要在源文件中添加,也不需要知道断点设置在文件的第几行。如图:
比普通断点多了两个属性Symbol和Module。
##Symbol
Symbol的内容,可以有如下几种:
-
方法名称:会对所有具有此方法名称的类方法生效。例如 initWithFrame: 。
-
特定类的方法:OC类和C++类都适用,例如 ,[UIView initWithFrame:]或者 Shap::draw()。
-
函数名称。例如普通C函数。
通过设置Symbol来调试,好用根本停不下来,想怎么断点就怎么断点。
##Test Failure Breakpoint
这个类型的断点会在test assertion 失败的时候暂停程序的执行。
iOS开发者证书-详解/生成/使用
转载:http://nuoerlz.is-programmer.com/posts/47670.html
本文假设你已经有一些基本的Xcode开发经验, 并注册了iOS开发者账号.
#相关基础
##加密算法
现代密码学中, 主要有两种加密算法: 对称密钥加密 和 公开密钥加密.
###对称密钥加密
对称密钥加密(Symmetric-key algorithm)又称为对称加密, 私钥加密, 共享密钥加密.
这类算法在加密和解密时使用相同的密钥.
例如: 最常见的应用场景 - 系统登陆.
要成功登陆系统, 你必须输入正确的密码, 这密码是 唯一的, 是与创建时一样的. 同样 的, 其他人要成 功登陆, 他也要输入这唯一的正确的密码.
###公开密钥加密
公开密钥加密(public-key cryptography, 也称为非对称密钥加密).
这类算法在加密和解密时使用不相同的密钥.
这类加密算法对应有两个密钥: 公钥和私钥. 公钥, 是公开的, 任何人都可以获得; 私钥 , 是私密的, 只由持有者所有.
这类加密算法的特点是: 用公钥加密的内容只能用私钥解密, 用私钥加密的内容只能 用公钥解密.
这类加密算法的特点决定了, 它即可以用于实现数据加密, 又可以用于实现身份认证 (数字签名).
###加密 & 认证
我们需要区分加密和认证这两个基本概念.
-
加密是将数据资料加密, 使得非法用户即使取得加密过的资料, 也无法获取正确的资料 内容, 所以数据加密可以保护数据, 防止监听攻击. 其重点在于数据的安全性.
-
身份认证是用来判断某个身份的真实性, 确认身份后, 系统才可以依其身份给予相应的 权限. 其重点在于用户的真实性.
要实现这两个要求(加密和认证), 都需要用到加密算法, 但并不是所有的算法都可实现身 份认证. 身份认证现在一般使用公开密钥加密算法.
下面将重点说明如何使用公开密钥加密算法实现加密和认证.
###如何实现加密?
对于某一用公钥加密的数据, 只能由对应的私钥解密.
例如, 我要给你发一封加密邮件. 我必须有你的公钥, 我用你的公钥给邮件加密. 这样 就能保证邮件不被别 人看到, 而且不被篡改. 因为只有你可以用你自己的 私钥解密.
###如何实现认证?
对于某一用私钥加密的数据, 只能由对应的公钥解密. (记住, 私钥只由持有人所有)
用我(A1)的私钥给邮件加密, 发给你(B1)之后, 你 能用我的公钥解密. 这样就能保证 邮件是我(A1)发的.
##数字身份证 & 数字证书(Certificates) & 数字证书认证机构(CA) & 根证书
###数字身份证
数字身份证, 是身份标识方式的一种, 是一对"钥匙", 即一对公钥&私钥. 它一般 用本地系统工具生成.
###数字证书认证机构
数字证书认证机构(CA, Certificate Authority), 是负责发放和管理数字证书的权威机构, 并作为交易中受信任的第三方, 承担公钥体系中公钥的合法性 检验的责任.
###数字证书
数字证书, 是一种用于计算机的身分识别机制. 数字证书不是数字身份证 , 而是身份认证机构(数字证书认证机构)在数字身份证上加上数 字签名. 这一行为表示身份认证机构已认定这个持证人. 这里的"认定"是怎么做到 的呢? 参考'如何验证数字证书的有效性?'.
数字证书一般含这样一些信息:
证书发布者
证书持有者
有效期(证书在这个时期之前或之后无效)
证书持有者的公钥()
证书扩展, 包含一些额外信息
所使用的哈希算法
数字签名, 该数字签名是对以上信息的哈希值用CA的 私钥加密生成()
###如何验证数字证书的有效性?
即, 如何确保数字证书是经过CA认证的呢?
注意: 对于某一用私钥加密的数据, 只能由对应的公钥解密.
原理: 计算数据证书的数据信息的哈希值H1, 对证书上的数字签名用CA的公钥解密得 H2, 如果H1等于H2, 则该证书有效, 且是经CA认证的.
CA的公钥如何获得呢? 它包含在根证书里.
###根证书
根证书, 是CA给自己颁发的数字证书, 是信任链的起始点. 它一 般放在CA网站上, 供任何人下载.
###数字签名
数字签名, 是一种类似写在纸上的普通的物理签名, 但是使用了公钥加密领域的 技术实现, 用于鉴别数字信息的方法.
数字签名具有完整性, 不可抵赖性(即不可否认性).
从'如何实现认证?'一节, 我们知道可以利用公开加密算法的特点实现"认证", 也就是签 名. 但是这会有一个问题, 任何人都可以或可能冒充我(A1)! 即, 任何人都可以冒充我 (A1)把他经用他的私钥加密的数据和他的公钥发给你. 也就是说, 你无法确保你接收到的 公钥就是我(A1)的!
所以, 这就需要一个机制来保证. 这机制就是基于前面所说的"数字证书"和"CA". 它可确 保我(A1)是经过CA认证的.
###数字签名原理
1.签名:
发送者先对要发送的"数据"计算一个哈希值, 再用自己的私钥对这个哈希值加密生成一 个"签名(值)", 同时发送者要拥有一个向CA申请得到的"数字证书"(记住, 其记录有公 钥), 最后发送者把"数据", "签名(值)"和"数字证书"一起发送给接收者.
2.验证签名:
接收者接收到发来的"数据", "签名(值)"和"数字证书"后, 会作一系列验证来判断这一数 字签名是否有效:
打开并验证"数字证书"的有效性;
计算"数据"的哈希值H1, 用"数字证书"记录的公钥对"签名(值)"进行解密得到值H2, 如 果H1==H2, 则该"数字签名"有效.
##Xcode代码签名相关
###Keychain
MAC下用于存储和管理密钥等私密信息的工具.
###Identifiers / Bundle ID / App ID
这是应用的唯一标识.
###Device UUID
这是设备的唯一标识.
###Provisioning Profiles
这就是我们最后要生成的 Profiles, 它记录了 App ID, UUID 和其所信任的证书.
当Xcode要把一个应用部署到真机上时, 会作相应检验:
Keychain中是否有相匹配的有效证书? 参考'如何验 证数字证书的有效性?'
Profiles是否有效? 参考'数字签名原理'
要部署的App的App ID是否与Profiles记录的App ID相匹配?
UUID是否相匹配?
只有在所有检验都通过了, 才能部署到真机上.
##Start
###生成密钥
填上Email和Name, 并选择"Saved to disk".
最后在"keys"下生成一对新的密钥, 为了以后分辨方便, 最好对它重新命名, 双击可以重 命名. 同时也会生成一个CRS文件.
###安装根证书
这个证书叫做 Worldwide Developer Relations Certificate Authority, 通过这个链接 可以下载: https://developer.apple.com/certificationauthority/AppleWWDRCA.cer, 一般下载下来的文件名为: AppleWWDRCA.cer.
该证书一般由 Xcode 自动安装.
###向Apple(CA)申请开发者证书
登陆苹果开发者中心, 到 Certificates, Identifiers & Profiles | iOS Apps | Certificates | Development 下, 新增一个Certificates:
点下一步会出现, 要求你先生成一个CRS文件, 这就是我们在"生成密钥"时做的:
直接下一步, 并把刚才生成的CRS文件上传, 最后提交生成证书. 把它下载下来, 双击, 其会自动添加到Keychains中.
###创建 App ID
在Identifiers下创建一个新的App ID.
###把设置的UUID加入Devices注册列表
在Devices下添加.
###生成Profiles
在 Certificates, Identifiers & Profiles | iOS Apps | Provisioning Profiles | Development 下, 点击添加一个新的Profiles.
到"选择证书"页面时, 有一点要注意, 最好只选上刚刚生成的证书, 不要选择所有. 因为 "选择所有"在Keychain中已有别的证书下, 容易出现各种奇葩问题, 后面会有详细说明.
最后, 把它下载下来, 双击添加到Xcode中.
##配置到Xcode
至此, 一切OK!
可能遇到的错
如果你按上面的执行下来, 最后一编译应用发现还是不行, 报类似这种错,
在XcodeOrganizer中也会显示出错:
这错一般是由于证书不匹配, 要检查:
保证生成Profile时, 选择且只选择了一个证书;
保证Keyschain里没有重复的证书.
Swizzle一则代码
Runtime中的一则系统方法替换;转载
https://github.com/12207480/TYSwizzleDemo
#import <objc/objc-runtime.h>
#import <objc/objc-class.h>
#import <Foundation/Foundation.h>
BOOL ty_swizzleInstanceMethod(Class aClass, SEL originalSel, SEL replacementSel)
{
Method origMethod = class_getInstanceMethod(aClass, originalSel);
Method replMethod = class_getInstanceMethod(aClass, replacementSel);
if (!origMethod) {
NSLog(@"original method %@ not found for class %@", NSStringFromSelector(originalSel), aClass);
return NO;
}
if (!replMethod) {
NSLog(@"replace method %@ not found for class %@", NSStringFromSelector(replacementSel), aClass);
return NO;
}
if (class_addMethod(aClass, originalSel, method_getImplementation(replMethod), method_getTypeEncoding(replMethod)))
{
class_replaceMethod(aClass, replacementSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}
else
{
method_exchangeImplementations(origMethod, replMethod);
}
return YES;
}
BOOL ty_swizzleClassMethod(Class aClass, SEL originalSel, SEL replacementSel)
{
return ty_swizzleInstanceMethod(object_getClass((id)aClass), originalSel, replacementSel);
}
IMP ty_swizzleMethodIMP(Class aClass, SEL originalSel, IMP replacementIMP)
{
Method origMethod = class_getInstanceMethod(aClass, originalSel);
if (!origMethod) {
NSLog(@"original method %@ not found for class %@", NSStringFromSelector(originalSel), aClass);
return NULL;
}
IMP origIMP = method_getImplementation(origMethod);
if(!class_addMethod(aClass, originalSel, replacementIMP,
method_getTypeEncoding(origMethod)))
{
method_setImplementation(origMethod, replacementIMP);
}
return origIMP;
}
// other way implement
BOOL ty_swizzleMethodAndStoreIMP(Class aClass, SEL originalSel, IMP replacementIMP,IMP *orignalStoreIMP)
{
IMP imp = NULL;
Method method = class_getInstanceMethod(aClass, originalSel);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(aClass, originalSel, replacementIMP, type);
if (!imp) {
imp = method_getImplementation(method);
}
}else{
NSLog(@"original method %@ not found for class %@", NSStringFromSelector(originalSel), aClass);
}
if (imp && orignalStoreIMP)
{
*orignalStoreIMP = imp;
}
return (imp != NULL);
}
Copyright © 2015 Powered by MWeb, 豫ICP备09002885号-5