内容简介:ReactiveCocoa教程 - 入门:1/2
作为一名iOS开发者, 你写的每一行代码几乎都是对某些事件的反馈:点击button、接收到网络信息、 一个属性的改变(通过KVO监测) 或者 通过CoreLocation监听用户所在位置的变化 以上等等都是很好的例子。然而,这些事件都有不同的编码方式,如: action、delegate、KVO、回调 等。 ReactiveCocoa 为事件定义了标准的接口,从而可以使用一些基本 工具 来更容易的连接、过滤和组合。
听起来很困惑?奇妙?…令人兴奋?接着往下看:
ReactiveCocoa结合了几个编程风格:
- Functional Programming : 函数式编程, 使用了高阶函数,即函数采用了多种函数为它们的参数
- Reactive Programming : 响应式编程,侧重数据流和变化传递
出于这个原因,你可能会听到ReactiveCocoa被描述为一个 函数响应式编程 ( FRP )框架。
放心,这个技术会在本教程中Get到!编程范式是一个不错的讨论主题,但这个ReactiveCocoa教程是一个实际的例子,而不是学术理论。
The Reactive Playground
在这个Reactive教程中,你将在一个非常简单的事例应用中引入响应式编程。下载 示例项目 ,然后编译运行以保证已经拥有基本设置。( 根据示例项目我也模仿编写一个Demo )
ReactivePlayground是一个非常简单的应用程序,提供了一个登录界面。当用户输入正确的用户名和密码时,一只可爱的小猫就会映入眼帘。
现在来花时间看看这个简单Demo的代码。很简单,浪费多长时间。
打开 ViewController.m ,全局的翻阅一下。你能否快速定位到 SignIn 的enabled状态? 能否快速找到 signInFailureLabel 什么时候显示/隐藏? 在这个相对简单的例子中可能只需要2-3分钟来回答这个问题。对于复杂的例子,如果是相同类型的,则需要花费相当长的时间。
使用ReactiveCocoa会让应用程序的的逻辑编的简洁。接下来开始使用。
添加ReactiveCocoa框架依赖
添加ReactiveCocoa最简单的方式就是使用 CocoaPods 如果你没用过CocoaPods,请先去看看 CocoaPods简介 这篇文章(或参照 CocoaPods的安装和使用(一) )。 至少通过文章中的步骤将CocoaPods初始化完成,你才能安装ReactiveCocoa。
如果由于某种原因你不想使用CocoaPods你仍然可以使用ReactiveCocoa,只需按照GitHub上的文档中的 导入ReactiveCocoa步骤 。
ReactiveCocoa手动导入的教程
。
对于RAC,个人建议还是使用CocoaPods导入。
打开 Terminal(终端) 进到工程目录
$ pod init
$ open -a Xcode Podfile
在Podfile中添加 pod ‘ReactiveCocoa’, ‘~> 4.0.4-alpha-4’ 框架
注意: Swift文件使用CocoaPods导入, 需要加 use_frameworks!
因此将Podfile中的 use_frameworks! 取消注释
然后执行下载过程
$ pod install –verbose –no-repo-update
Time To Play
正如介绍中提到的,ReactiveCocoa为处理您的应用程序中发生的事件的不同流的标准接口。在ReactiveCocoa术语,这些被称为信号,并且由RACSignal类表示。
打开应用的初始的视图控制器, ViewController.m,并在头部引入ReactiveCocoa的头文件。
#import
暂时不要修改ViewDidLoad中其他的代码,在ViewDidLoad的末尾添加一些方法,随便做一些测试:
[self.userNameText.rac_textSignal subscribeNext:^(id x) { NSLog(@"%@", x); }];
编译并运行程序, 在用户名的输入框中,输入内容,查看控制台打印结果:
从上面的打印结果,你可以看出每次修改textfield中的文本时, 该Block块中的代码都会执行。没有target-action, 没有delegate, 只有信号和blocks。
ReactiveCocoa信号(由 RACSignal 表示)发送事件流给他们的用户。有三种类型的事件知道:next,error和completed。在出现 error 或者信号 completed 之前, 一个信号能送任意数量的事件。在本节中,你会专注于 next event 。请务必阅读第二部分时,它可用来了解error和completed的事件。
RACSignal包含很多方法,用于订阅这些不同的事件类型。每个方法都需要一个或多个Block,当事件发生时,在你Block中的逻辑则会执行。在这种情况下,可以看出 subscribeNext: 方法被用于提供一个Block给每一个将要执行的 next event 。
ReactiveCocoa 框架使用了许多Category给UIKit框架中的一些基本控件添加了许多信号的方法,因此你可以订阅他们的事件。这就是textfield属性中rac_textSingal的来源。
原理就介绍到这里,接下来开始使用ReactiveCocoa去为你做一些事情。
ReactiveCocoa有许多操作,你可以用它们来操纵事件流。例如,你只让3个字符以上的用户名有效。你可以实现 filter (过滤) 这个操作。在之前的ViewDidLoad添加如下代码:
[[self.userNameText.rac_textSignal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
运行之后,然后键入一些文本到textField,你会发现。他只会打印textField长度大于3的字符串。
你在这里创建了一个非常简单的事件流。它就是反应式编程的本质,通过数据流来表达应用程序的功能。
下面这张图片可以帮助你更好的理解数据流向:
从上面的图中可以看出,rac_textSignal是事件的最初来源。数据流通过过滤器时,仅允许字符串的长度是大于三的事件进传递。在事件流的最后一步是 subscribeNext: 在这里你可以打印事件的值。
值得一提的一点是,该filter过滤器的返回值也是一个RACSignal(即返回值为一个信号)。你可以通过如下分步代码来理解数据流向的具体步骤:
RACSignal *signal = self.userNameText.rac_textSignal; RACSignal *filterSignal = [signal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }]; [filterSignal subscribeNext:^(id x) { NSLog(@"%@", x); }];
一个RACSignal的每个操作的返回值也是RACSignal,因此,它被称为 流式接口 ( fluent interface )。这样的特征允许你构建事件流,而不需要考虑每一步都使用局部变量。
注意: ReactiveCocoa使用了大量的Block。如果你新学习Block,你可能需要阅读苹果官方的 Blocks Programming Topics 。而如果像我一样,已经熟悉了Blocks,但却发现很难记住,你可以去看看很有趣的一个网站 How Do I Declare A Block in Objective-C? (经测试该链接是正常运作的)
类型变换
刚刚我们将之前的代码分割成了多个RACSignal,那现在将其改回之前的流式语法:
[[self.userNameText.rac_textSignal filter:^BOOL(id value) { NSString *text = value;// 隐式转换 return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
在上面的代码中, 注释部分从id隐式转换成NSString,这样看起来不是很优雅。幸运的是,传递给该Block中的值就是一个NSString,你可以更改参数类型本身。更新你的代码如下:
[[self.userNameText.rac_textSignal filter:^BOOL(NSString *text) { return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
编译并运行,确保没有任何问题。
什么是一个事件?
到目前为止,本教程描述了不同的事件类型,但并没有详细说明这些事件的结构。有趣的是,一个事件绝对可以包含任何事情!
通过下面这个例子,你可以将另一个操作添加到事件流。添加如下代码到你的ViewDidLoad:
[[[self.userNameText.rac_textSignal map:^id(NSString *text) { return @(text.length); }] filter:^BOOL(NSNumber *value) { return [value integerValue] > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
如果你编译运行,你会发现打印的是文本的长度:
新添加的map(映射)操作为改变事件数据提供了Block块。它将接收到的事件,通过执行Block块所得的返回值提供给下一个事件。在上面的代码中,map的Block返回了取出的NSString文字的长度, 这使得下一个事件的值则为NSNumber类型。
对于它如何工作的请看下面这张图片:
正如你所看到的一样,所有这一切跟着map的操作进行改变, 现在接收到的是NSNumber类型的对象。你可以使用 map 操作去将你接收到的数据转换成你喜欢的类型, 只要他是一个对象类型。
注意: 在上面的例子中text.length属性的类型是NSUInteger。为了用它作为事件的内容,它必须被装箱。幸运的是Objective-C的文字语法中提供了字面量 - @(text.length)。
这些足够开始编写代码了! 现在是时候使用目前学到的概念更新ReactivePlayground应用程序。现在你可以删除所有已经添加的代码了。
创建有效状态的Signals(信号)
首先,先创建俩个信号, 表示用户名密码是否有效。添加以下内容到ViewController.m中:
RACSignal *validUsernameSignal = [self.userNameText.rac_textSignal map:^id(NSString *text) { return @([self isValidUsername:text]); }]; RACSignal *validPasswordSignal = [self.passWordText.rac_textSignal map:^id(NSString *text) { return @([self isValidPassword:text]); }];
正如你所看到的, 上述代码通过map函数将 rac_textSignal 所生成的字符串类型的值转换为了 NSNumber 类型的 BOOL 值。
接下来, 转变信号时,可以给textField提供相应的背景颜色。简单来说,就是你订阅这个信号并使用信号的结果来更新textFiled的背景颜色。你可以像下面这样写:
[[validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }] subscribeNext:^(UIColor *color) { self.passWordText.backgroundColor = color; }];
(请不要添加这段代码到你的工程中, 因为还有更优雅的方案!)
理论上指派的信号输出的值会改变textField的backgroundColor属性。然而,这段代码的方案是比较low的。
幸运的是, ReactiveCocoa有一个宏, 可以将这段代码表现的更优雅。在viewDidLoad中添加俩个信号, 代码如下:
RAC(self.userNameText, backgroundColor) = [validUsernameSignal map:^id(NSNumber *usernameValid) { return [usernameValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }]; RAC(self.passWordText, backgroundColor) = [validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }];
RAC的宏会将信号输出的值赋给对象的属性。它包含俩个参数,参数一 : 需要设置的属性值的对象, 参数二 : 要赋值的属性名称。每个信号发送下一个事件时, 传递的值就会被赋给指定的属性。
这是一个非常优雅的解决方案,你认为呢?
在你编译和运行之前,做最后一件事。找到 updateUIState 方法并删除掉以下俩行内容:
self.userNameText.backgroundColor = self.usernameIsValid ? [UIColor whiteColor] : [UIColor yellowColor]; self.passWordText.backgroundColor = self.passwordIsValid ? [UIColor whiteColor] : [UIColor yellowColor];
这样就清除掉了非RAC的代码。
编译并运行应用程序。你会发现textField的内容在无效时,是高亮的;在有效时,是透明的。
看起来效果不错,如果当前的逻辑以图形化来表示的话,如下图。在下面你可以看到把俩个信号形容成俩个通道,俩个通道做了相同的事情。首先,信号通过map方法映射成判断是否有效的BOOL值,再通过map方法通过二次映射将BOOL值转成UIColor,通过UIColor决定textField的背景颜色。
看到这里,你是否有疑问为何要创建俩个独立的validPasswordSignal和validUsernameSignal信号,而不是俩个输入框公用一个信号呢?想知道答案,接着往下看!
信号结合
在当前这个App中,登录按钮在用户名输入框和密码输入框都有效时, 才被显示。现在是时候改成 响应式 了!
当前的代码已经具有判断用户名和密码字段是否有效的功能,并且是一个可以返回BOOL类型值的信号; 分别是 validUsernameSignal 和 validPasswordSignal 。接下来的任务就是将这俩个信号结合在一起来决定button是否可以点击。
在viewDidLoad的末尾处添加如下代码:
RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) { return @([usernameValid boolValue] && [passwordValid boolValue]); }];
上面的代码使用了 combineLatest: reduce: 方法, 通过结合validUsernameSignal 和 validPasswordSignal所发出的结果,从而生成一个新的信号。每次当这俩个源信号中任意一个值改变时, reduce block 会执行, 并将俩个源信号的值 组合 作为新的信号的返回值。
注意: RACSignal的 combinLatest 方法能结合任意数量的信号, 并且 reduce block 中的每一个参数都是对应源信号的(combinLatest中的信号顺序, 与参数顺序相同)。
ReactiveCocoa中包含一个实用的工具类, RACBlockTrampoline 可以在内部处理 reduce block 内部变量参数列表。事实上, 有很多隐藏在ReactiveCocoa实现中的实用的小技巧,非常值得你去学习研究。
现在你有一个非常合适的信号添加到viewDidLoad结尾处。将这个信号与Button的enabled属性绑定在一起:
[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) { self.signIn.enabled = [signupActive boolValue]; }];
在运行代码之前,我们需要花点时间来删除一些没用的代码。删除文件顶部的下面俩个属性:
@property (nonatomic, assign) BOOL passwordIsValid; @property (nonatomic, assign) BOOL usernameIsValid;
从最接近viewDidLoad顶部的位置, 移除掉以下代码:
[self.userNameText addTarget:self action:@selector(usernameTextFieldChanged) forControlEvents:UIControlEventEditingChanged]; [self.passWordText addTarget:self action:@selector(passwordTextFieldChanged) forControlEvents:UIControlEventEditingChanged];
还需要删除 updateUIState, usernameTextFieldChanged和passwordTextFieldChanged方法。
最后,确保从viewDidLoad中删除updateUIState方法的调用。
如果你编译运行,检查登录按钮的状态。如果按钮是可用的,说明userNameText和passWordText都是有效状态。和以前的效果一样。
更新应用程序逻辑后,执行流程如下图:
以上说明了几个非常重要的概念,可以使用ReactiveCocoa去执行一些非常重量级的任务
- 拆分 - 信号可以有多个订阅者,也可以作为多个后续事件流步骤的源。另外,在上图中,请注意指示userNameText和passWordText是否有效的信号,被分别用于了不同的地方。
- 聚合 - 多个信号可以被组合,以用来创建新的信号。在这种情况下,俩个BOOL信号用做合并。然而,你可以通过结合信号发出任意值类型的信号。
这些改变的结果是应用程序不用再写私有属性用来表明俩个textField的当前有效状态。你会发现,这是你采用响应式编程与以往模式主要不同的地方之一 — 你不需要使用实例变量来追踪瞬时的状态。
响应式登录
该应用目前使用如上图所示的响应式事件流来管理文本框和按钮的状态。不过,按下按钮操作仍然在使用action做响应,所以下一步是使用 响应式 更换剩下的应用程序的逻辑。
在ViewController.m中, SignIn按钮的Touch Up Inside事件通过StoryBoard与signInTouchUp方法进行关联。如果过你想用响应式替代,首先你需要先断开当前故事版与action的连线。
打开Main.storyboard, 找到Sign In按钮, 按住Ctrl键单击按钮,会弹出outlet/action连接的界面,找到对应的action连接,点击x删除掉对应的连接。下图很直观的显示出来在哪里可以删除按钮的action:
你已经看到了ReactiveCocoa框架为标准的 UIKit 框架的 controls 添加了属性和方法。到现在为止,我们已经使用了rac_textSignal, 发射事件时,文本的变化。为了更好的处理事件,你需要使用ReactiveCocoa中其他的给UIKit添加的controls中的方法— rac_signalForControlEvents。
回到ViewController.m中, 在viewDidLoad结尾处添加如下代码:
[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) { NSLog(@"button clicked"); }];
上述代码通过创建一个按钮的 UIControlEventTouchUpInside 事件信号, 并对该信号进行订阅,使每个这个事件被触发生时,都有Log。
编译运行,验证是否与我们所想象的那样一致,会出现消息Log。注意,确保用户名和密码都是有效时, 按钮才可以点击。所以在点击按钮之前,先输入一些文字到俩个textField中。
输入完毕,按钮有效后。点击几次按钮,你可以看到Xcode控制台中会出现如下打印信息:
现在,这个按钮已经具有点击触发事件的信号。下一步就是把该事件与登录流程组合起来。这就出现了问题-但这是好的。打开ReactiveManager.h,看看内部的代码:
// 实际项目可根据请求结果决定 typedef void(^SignInResponse)(BOOL); @interface ReactiveManager : NSObject /** * 登录的方法 * * @param username 用户名 * @param password 密码 * @param completeBlock 登录成功后的回调 */ + (void)signInWithUsername:(NSString *)username password:(NSString *)password complete:(SignInResponse)completeBlock; @end
该服务需要用户名,密码和完成的 Block 作为参数。当登录成功或失败时, Block 会执行。你可以在按钮点击事件 subscribeNext: 的 Blcok 里直接调用这个方法,但是这么做会有些不合适。你可以直接使用 ReactiveCocoa 来重写这些代码。
注意: 本教程依赖的是一个虚拟的服务,这样并没有对外界的API产生依赖。但是,遇到了新的问题,如何在信号中表示不是用信号编写的API。
创建信号
幸运的是,把一个现有的异步API表示为一个信号还是很简单的。首先,从ViewController.m中删除当前signInButtonTouched方法。已经不需要这个逻辑了,因为会有与之等价的方法来替换它。
然后在ViewController.m中添加以下方法
- (RACSignal *)signInSignal { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [ReactiveManager signInWithUsername:self.userNameText.text password:self.passWordText.text complete:^(BOOL success) { [subscriber sendNext:@(success)]; [subscriber sendCompleted]; }]; return nil; }]; }
上述方法,创建了一个使用用户名和密码登录的信号。现在我们拆分来看看。
上面的代码使用了 RACSignal 信号的 createSignal: 的方法。描述这个信号模块的是一个 Block 。当这个信号有订阅者时,会执行此 Block 内的代码。
该Block的参数是一个遵循 RACSubscriber 协议的 subscriber ,协议中有很多方法可以产生事件。你还可以发送任意数量的 next 事件,当调用 error 或是 complete 时则会被终止。在本案例中,首先发送了一个 next 事件来指示登录是否成功,然后发送了一个 complete 的事件。
这个Block的返回类型是 RACDisposable 对象, 它可以让你执行你所需要的清理工作,例如取消订阅或丢弃。这个信号并不需要清理任何内容,因此,返回 nil 。
正如你所看到的,这是多么简单的一个封装异步API的信号。
接下来,让我们利用这一信号。更新您添加到ViewDidLoad中末尾的代码,如下所示例:
[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) { return [self signInSignal]; }] subscribeNext:^(id x) { NSLog(@"Sign in result: %@", x); }];
上述代码使用先前的按钮触摸信号并通过 map(映射) 转换成登录信号。用户只需要登录的结果。
编译运行后,点击登录按钮,查看Xcode的控制台,你会看到上面代码的结果……
……并且结果和你预期的并不一样。
2016-03-28 21:44:06.713 ReactivePlayground[2956:83740] Sign in result: <RACDynamicSignal: 0x7fa7fbcb8a80> name:
该信号已经执行了 subscribeNext: Block, 但是登录信号的输出结果与我们想要的并不一样!
通过下图可以告诉你,究竟哪里错了:
当你点击按钮时, rac_signalForControlEvents 会发出 next 事件。 map(映射) 会创建并返回登录信号, 这意味着事件流的下一步会接收到 RACSignal 信号。这就是为什么后面打印出来的是信号,而不是我们想要的结果。
以上情况也有时会被称为信号的信号;换句话说,一个外信号内包含了一个内信号。你可以中外部信号 subscribeNext: 的Block中再订阅信号,但是这样会造成混乱。幸运的是,ReactiveCocoa为这种常见的情况准备了应对方案。
信号的信号
要解决这个问题很简单,只要修改 map(映射) 函数改为 flattenMap函数, 如下图所示:
[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^RACStream *(id value) { return [self signInSignal]; }] subscribeNext:^(id x) { NSLog(@"Sign in result: %@", x); }];
此按钮触摸事件转换为了登录信号,并且通过内部信号向外部信号发送了正确的结果。
map 映射出的值是 信号本身
flattenMap 映射出的值是 信号的值
编译运行,看控制台结果,现在应该输出登录成功或者失败了。
2016-03-29 15:53:25.639 ReactivePlayground[5861:233703] Sign in result: 0 2016-03-29 15:53:33.791 ReactivePlayground[5861:233703] Sign in result: 1
现在可以做你想做的事了,最后一步就是将登录是否成功逻辑添加到 subscribeNext 中,用以下代码把刚刚的代码替换掉。
[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id(id x) { return [self signInSignal]; }] subscribeNext:^(NSNumber *signedIn) { BOOL success = [signedIn boolValue]; self.signInFailureLabel.hidden = success; if (success) { [self performSegueWithIdentifier:@"signInSuccess" sender:self]; } }];
通过 subscribeNext: Block取到结果,通过该结果更新signInFailureLabel的可见性,并根据结果来决定是否要执行 segue 的跳转。
编译运行,我们再次看到了那只可爱的小猫。
就现在的应用体验而言,你有没有发什么什么不好的用户体验?当登录服务正在进行时, 应该禁用登录按钮。这可以避免用户重复登录。此外,如果登录失败了一次,显示了错误提示,应当再用户视图登录时隐藏掉。
但是,问题来了,应该如何添加这个逻辑到当前代码中呢?更改按钮的启用状态,无法使用 map filter 或其他已知的概念。这就可以称之为 附加操作 了,因为这个逻辑应该在next事件发生时执行,并不是改变事件本身。
附加操作
用以下代码替换之前所写代码:
[[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) { self.signIn.enabled = NO; self.signInFailureLabel.hidden = YES; }] flattenMap:^id(id x) { return [self signInSignal]; }] subscribeNext:^(NSNumber *signedIn) { self.signIn.enabled = YES; BOOL success = [signedIn boolValue]; self.signInFailureLabel.hidden = success; if (success) { [self performSegueWithIdentifier:@"signInSuccess" sender:self]; } }];
你可以看到上面代码添加了一个 doNext: , 在触摸事件后添加的。请注意, doNext:并没有返回值, 因为它只是一个附加操作,并且事件本身不变。
在doNext Block中设置登录按钮不可点击,并隐藏了 signInFailureLabel。并在 subscribeNext: Block中重新启用 登录按钮,并根据结果决定是否显示失败文本。
现在是时候更新程序执行示意图,包括附加操作:
编译运行,并确认按钮启用是否与预期相同。
如果和你想的一样,那么这个应用就大功告成了。
如果你在之前没有跟上,可以通过这个地址下载 最终的项目
注意: 在异步执行的过程中禁用按钮是一个常见问题,ReactiveCocoa也能作出很好的解决。RACCommand就封装了这个概念,它有个enabled信号,使您可以将信号和enabled连接起来,你也可以试试这个类。
总结
希望本教程可以在你自己的应用程序中使用ReactiveCocoa时给你一个良好的基础。你可以采取一些练习来熟悉这些概念,就像学习一门编程语言或编程一样,一旦你有了良好的基础,它就变得很简单。ReactiveCocoa中的核心就是信号,无非就是一堆事件流。还有什么能比这个更简单呢?
随着逐渐学习ReactiveCocoa,我发现了其中有可以解决很多疑难问题的方法。你可以使用本教程案例试试,调整信号的组合和拆分。
这里最值得注意的是ReactiveCocoa的主要目标是使你的代码更加简洁,更加容易理解。如果应用程序的逻辑都清晰的表示成事件流、流式语法,那这个应用具体做了什么就很好理解了。
在本教程系列的第二部分,你将学习更先进的语法。比如错误处理以及如何管理不同线程中执行的代码等高级语法。
以上所述就是小编给大家介绍的《ReactiveCocoa教程 - 入门:1/2》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。