ReactiveObjC使用简介

文是对ReactiveObjC部分使用介绍,原理及流程简介,见文章结尾

目录:

1、简单使用

2、UIKit (基于UIView控件)

3、Foundation (Foundation对象)

4、KVO (关于监听)

5、事件信号

6、结合网络请求使用

一、简单使用

RACSignal 信号相当于一个电视塔 ,只要将电视机调到跟电视塔的赫兹相同的频道,就可以收到信息。

subscribeNext 相当于订阅频道。当RACSignal信号发出sendNext消息时,subscribeNext就可以接收到信息。

//1、创建信号RACSignal*signal=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){//任何时候,都可以发送信号,可以异步[subscriber sendNext:@"发送信号"];//数据传递完,最好调用sendCompleted,这时命令才执行完毕。[subscriber sendCompleted];returnnil;}];//2、订阅信号RACDisposable*disposable=[signal subscribeNext:^(id  _Nullable x){//收到信号时NSLog(@"信号内容:%@",x);}];//取消订阅[disposable dispose];

1、调用 createSignal 创建一个信号

二、UIKit (基于UIView控件)

1、rac_textSignal 文本监听信号,可以减少对代理方法的依赖

//UITextField创建了一个 `textSignal`的信号,并订阅了该信号//当UITextField的内容发生改变时,就会回调subscribeNext[[self.textField rac_textSignal]subscribeNext:^(NSString*_Nullable x){NSLog(@"text changed = %@",x);}];

2、filter 对订阅的信号进行筛选

//当UITextField内输入的内容长度大于5时,才会回调subscribeNext[[[self.textField rac_textSignal]filter:^BOOL(NSString*_Nullable value){returnvalue.length>5;}]subscribeNext:^(NSString*_Nullable x){NSLog(@"filter result = %@",x);}];

3、ignore 对订阅的信号进行过滤

[[[self.textField rac_textSignal]ignore:@"666"]subscribeNext:^(NSString*_Nullable x){//当输入的内容 equalTo @"666" 时,这里不执行//其他内容,均会执行subscribeNextNSLog(@"ignore = %@",x);}];

4、rac_signalForControlEvents 创建事件监听信号

//当UIButton点击时,会调用subscribeNext[[self.button rac_signalForControlEvents:(UIControlEventTouchUpInside)]subscribeNext:^(__kindof UIControl*_Nullable x){NSLog(@"button clicked");}];

三、Foundation (Foundation对象)

1、NSNotificationCenter 通知

//@property (nonatomic, strong) RACDisposable *keyboardDisposable;self.keyboardDisposable=[[[NSNotificationCenter defaultCenter]rac_addObserverForName:UIKeyboardDidShowNotification object:nil]subscribeNext:^(NSNotification*_Nullable x){NSLog(@"%@ 键盘弹起",x);// x 是通知对象}];

注意:rac_addObserverForName同样需要移除监听。RAC通知监听会返回一个RACDisposable清洁工的对象,在dealloc中销毁信号,信号销毁时,RAC在销毁的block中移除了监听

-(void)dealloc{[_keyboardDisposable dispose];}

2、 interval定时器 (程序进入后台,再重新进入前台时,仍然有效,内部是用GCD实现的)

//创建一个定时器,间隔1s,在主线程中运行RACSignal*timerSignal=[RACSignal interval:1.0fonScheduler:[RACScheduler mainThreadScheduler]];//定时器总时间3秒timerSignal=[timerSignal take:3];//定义一个倒计时的NSInteger变量self.counter=3;@weakify(self)[timerSignal subscribeNext:^(id  _Nullable x){@strongify(self)self.counter--;NSLog(@"count = %ld",(long)self.counter);}completed:^{//计时完成NSLog(@"Timer completed");}];

3、delay延迟

//创建一个信号,2秒后订阅者收到消息[[[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];returnnil;}]delay:2]subscribeNext:^(id  _Nullable x){NSLog(@"delay : %@",x);}];

4、NSArray 数组遍历

NSArray*array=@[@"1",@"2",@"3",@"4",@"5"];[array.rac_sequence.signal subscribeNext:^(id  _Nullable x){NSLog(@"数组内容:%@",x);}];

5、NSDictionary字典遍历

NSDictionary*dictionary=@{@"key1":@"value1",@"key2":@"value2",@"key3":@"value3"};[dictionary.rac_sequence.signal subscribeNext:^(RACTuple*_Nullable x){// x 是一个元祖,这个宏能够将 key 和 value 拆开  乱序RACTupleUnpack(NSString*key,NSString*value)=x;NSLog(@"字典内容:%@ : %@",key,value);}];

6、RACSubject代理

定义一个DelegateView视图,并且声明一个RACSubject的信号属性,在touchesBegan方法中,给信号发送消息

@interfaceDelegateView:UIView//定义了一个RACSubject信号@property(nonatomic,strong)RACSubject*delegateSignal;@end@implementationDelegateView-(void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event{// 判断代理信号是否有值if(self.delegateSignal){// 有值,给信号发送消息[self.delegateSignal sendNext:@666];}}@end

在UIViewController中声明DelegateView作为属性

@interfaceViewController()@property(nonatomic,strong)DelegateView*bView;@end//使用前,记得初始化self.bView.delegateSignal=[RACSubject subject];[self.bView.delegateSignal subscribeNext:^(id  _Nullable x){//订阅到 666 的消息NSLog(@"RACSubject result = %@",x);}];

四、KVO (关于监听)

1、rac_valuesForKeyPath 通过keyPath监听

[[self.bView rac_valuesForKeyPath:@"frame"observer:self]subscribeNext:^(id  _Nullable x){//当self.bView的frame变化时,会收到消息NSLog(@"kvo = %@",x);}];

2、RACObserve 属性监听

//counter是一个NSInteger类型的属性[[RACObserve(self,counter)filter:^BOOL(id  _Nullable value){return[value integerValue]>=2;}]subscribeNext:^(id  _Nullable x){NSLog(@"RACObserve : value = %@",x);}];

在进行监听时,同样可以使用filter信号,对值进行筛选

3、RAC 事件绑定

//当UITextField输入的内容为@"666"时,bView视图的背景颜色变为grayColorRAC(self.bView,backgroundColor)=[self.textField.rac_textSignal map:^id_Nullable(NSString*_Nullable value){return[value isEqualToString:@"666"]?[UIColor grayColor]:[UIColor orangeColor];}];

#define RAC(TARGET, ...)这个宏定义是将对象的属性变化信号与其他信号关联,比如:登录时,当手机号码输入框的文本内容长度为11位时,"发送验证码" 的按钮才可以点击

五、事件信号

名词描述说明

RACTuple元祖只能存储OC对象 可以用于解包或者存储对象

bind包装获取到信号返回的值,包装成新值, 

再次通过信号返回给订阅者

concat合并按一定顺序拼接信号,当多个信号发出的时候,

有顺序的接收信号

then下一个用于连接两个信号,当第一个信号完成,

才会连接then返回的信号

merge合并把多个信号合并为一个信号,

任何一个信号有新值的时候就会调用

zipWith压缩把两个信号压缩成一个信号,

只有当两个信号都发出一次信号内容后,

并且把两个信号的内容合并成一个元组,

才会触发压缩流的next事件(组合的数据都是一一对应的)

combineLatest结合将多个信号合并起来,并且拿到各个信号的最新的值,

必须每个合并的signal至少都有过一次sendNext,

才会触发合并的信号

(combineLatest 与 zipWith不同的是,每次只拿各个信号最新的值)

reduce聚合用于信号发出的内容是元组,

把信号发出元组的值聚合成一个值,

一般都是先组合在聚合

map数据筛选map 的底层实现是通过 flattenMap 实现的

flattenMap信号筛选flattenMap 的底层实现是通过bind实现的

filter过滤过滤信号,获取满足条件的信号

ps:表格排版加上<br>换行之后,才不至于列的内容挤到一起,累累累...

1、RACTuple 元祖

只能存储OC对象 可以用于解包或者存储对象

//解包数据RACTupleUnpack(NSNumber*a,NSNumber*b)=x;

2、bind 包装

获取到信号返回的值,包装成新值, 再次通过信号返回给订阅者

[[self.textField.rac_textSignal bind:^RACSignalBindBlock _Nonnull{return^RACSignal*(id value,BOOL*stop){// 处理完成之后,包装成信号返回出去return[RACSignalreturn:[NSString stringWithFormat:@"hello: %@",value]];};}]subscribeNext:^(id  _Nullable x){NSLog(@"bind : %@",x);// hello: "x"}];

3、concat 合并

按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号

RACSignal*signalA=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@"signalA"];[subscriber sendCompleted];returnnil;}];RACSignal*signalB=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@"signalB"];[subscriber sendCompleted];returnnil;}];// 把signalA拼接到signalB后,signalA发送完成,signalB才会被激活 顺序执行[[signalA concat:signalB]subscribeNext:^(id  _Nullable x){//先拿到 signalA 的结果 , 再拿到 signalB 的结果 , 执行两次NSLog(@"concat result = %@",x);}];

4、then 下一个

用于连接两个信号,当第一个信号完成,才会连接then返回的信号

// 底层实现  1.使用concat连接then返回的信号  2.先过滤掉之前的信号发出的值[[[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];[subscriber sendCompleted];returnnil;}]then:^RACSignal*{return[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){//可以对第一个信号的数据进行过滤处理 , 不能直接获得第一个信号的数据返回值[subscriber sendNext:@2];returnnil;}];}]subscribeNext:^(id x){// 只能接收到第二个信号的值,也就是then返回信号的值NSLog(@"then : %@",x);// 2}];

5、merge 合并

把多个信号合并为一个信号,任何一个信号有新值的时候就会调用

//创建多个信号RACSignal*mergeSignalA=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];returnnil;}];RACSignal*mergeSignalB=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@2];returnnil;}];// 合并信号,只要有信号发送数据,都能监听到.RACSignal*mergeSignal=[mergeSignalA merge:mergeSignalB];[mergeSignal subscribeNext:^(id x){//每次获取单个信号的值NSLog(@"merge : %@",x);}];

6、zipWith 压缩

把两个信号压缩成一个信号,只有当两个信号都发出一次信号内容后,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件(组合的数据都是一一对应的)

RACSignal*zipSignalA=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];[subscriber sendNext:@2];returnnil;}];RACSignal*zipSignalB=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){//3秒后执行dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3*NSEC_PER_SEC)),dispatch_get_main_queue(),^{[subscriber sendNext:@3];});//5秒后执行dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5*NSEC_PER_SEC)),dispatch_get_main_queue(),^{[subscriber sendNext:@5];});returnnil;}];RACSignal*zipSignal=[zipSignalA zipWith:zipSignalB];[zipSignal subscribeNext:^(id  _Nullable x){// x 是一个元祖RACTupleUnpack(NSNumber*a,NSNumber*b)=x;NSLog(@"zip with : %@  %@",a,b);//第一次输出  1  3//第二次输出  2  5}];

7、combineLatest 结合

将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号 (combineLatest 与 zipWith不同的是,每次只拿各个信号最新的值)

RACSignal*combineSignalA=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];[subscriber sendNext:@2];returnnil;}];RACSignal*combineSignalB=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3*NSEC_PER_SEC)),dispatch_get_main_queue(),^{[subscriber sendNext:@3];});dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5*NSEC_PER_SEC)),dispatch_get_main_queue(),^{[subscriber sendNext:@5];});returnnil;}];RACSignal*combineSignal=[combineSignalA combineLatestWith:combineSignalB];[combineSignal subscribeNext:^(id  _Nullable x){// x 是一个元祖RACTupleUnpack(NSNumber*a,NSNumber*b)=x;NSLog(@"combineLatest : %@  %@",a,b);//第一次输出 2 3//第二次输出 2 5//因为combineSignalA中的2是最新数据,所以,combineSignalA每次获取到的都是2}];

8、reduce 聚合

用于信号发出的内容是元组,把信号发出元组的值聚合成一个值,一般都是先组合在聚合

RACSignal*reduceSignalA=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@1];returnnil;}];RACSignal*reduceSignalB=[RACSignal createSignal:^RACDisposable*_Nullable(id<RACSubscriber>_Nonnull subscriber){[subscriber sendNext:@3];returnnil;}];RACSignal*reduceSignal=[RACSignal combineLatest:@[reduceSignalA,reduceSignalB]reduce:^id(NSNumber*a,NSNumber*b){//reduce中主要是对返回数据的处理  return[NSString stringWithFormat:@"%@ - %@",a,b];}];[reduceSignal subscribeNext:^(id  _Nullable x){//返回值x 取决于reduce之后的返回NSLog(@"reduce : %@",x);}];

9、map 数据过滤

map 的底层实现是通过flattenMap 实现的。map 直接对数据进行处理,并且返回处理后的数据

[[self.textField.rac_textSignal map:^id_Nullable(NSString*_Nullable value){// 当源信号发出,就会调用这个block,修改源信号的内容// 返回值:就是处理完源信号的内容。return[NSString stringWithFormat:@"hello : %@",value];}]subscribeNext:^(id  _Nullable x){NSLog(@"Map : %@",x);// hello: "x"}];

10、flattenMap 信号过滤

flattenMap 的底层实现是通过bind实现的。拿到原数据,处理完成之后,包装成信号返回

[[self.textField.rac_textSignal flattenMap:^__kindof RACSignal*_Nullable(NSString*_Nullable value){return[RACSignalreturn:[NSString stringWithFormat:@"hello : %@",value]];}]subscribeNext:^(id  _Nullable x){NSLog(@"flattenMap : %@",x);// hello "x"}];

10、filter 过滤

过滤信号,获取满足条件的信号

[[self.textField.rac_textSignal filter:^BOOL(NSString*value){returnvalue.length>6;}]subscribeNext:^(NSString*_Nullable x){NSLog(@"filter : %@",x);// x 值位数大于6}];

六、结合网络请求使用

以下网络接口均基于MVVM模式

1、请求单个接口

//创建请求接口的信号,该方法可以定义并实现在ViewModel层#pragmamark - 获取指定时间的课程+(RACSignal*)getCourseInfoByTime:(NSInteger)time{//此处为接口所需参数NSMutableDictionary*paramter=[NSMutableDictionary dictionary];[paramter setObject:@(time)forKey:@"exerciseTime"];return[RACSignal createSignal:^RACDisposable*(id<RACSubscriber>subscriber){//这里为接口请求方法 //接口url: CombinePath(TY_DEBUG_HOST, SPORT_HOME_COURSE) //postRequest:接口请求类型 post//paramter:参数//[SportCourseDataModel class]:接口返回类型model[Networking requestWithPath:CombinePath(TY_DEBUG_HOST,SPORT_HOME_COURSE)requestType:postRequest requestParamter:paramter responseObjctClass:[SportCourseDataModel class]completionBlock:^(BOOL isSuccess,id object,NSError*error){//当接口返回结果后,根据状态,分别传递object或者error给订阅者if(isSuccess){//将接口返回的接口object传递给subscribeNext[subscriber sendNext:object];//信号完成之后,最好调用sendCompleted[subscriber sendCompleted];}else{[subscriber sendError:error];}}];returnnil;}];}//信号订阅//在ViewController中定义一个方法,用来调用网络接口方法-(void)getCourseByIsExperience:(BOOL)isExperience{//使用 @weakify(self) 和 @strongify(self) 避免循环引用@weakify(self)[[SportViewModel getCourseInfoByTime:0isExperience:isExperience]subscribeNext:^(id  _Nullable x){@strongify(self)//接口请求成功,订阅者可以在这里获取到接口返回的内容 x}error:^(NSError*_Nullable error){@strongify(self)//当接口出错时,这里可以处理错误信息}];}

分析:

1、在ViewModel类中,创建了一个信号,这个信号请求了一个获取课程的接口。信号创建之后,并不会立即执行,要等订阅者,订阅并调用subscribeNext时,才会执行。

2、在ViewController中,经过用户操作,开始调用getCourseByIsExperience方法。此时,订阅者开始订阅信号,信号中的createSignal开始执行接口请求方法。

3、当接口请求成功后,根据状态,将对应的object或者error通过sendNext: 和sendError:传递给订阅者

4、订阅者开始执行subscribeNext 或者 error block中的代码

(ps:如果接口请求之后,不需要获取返回值,则可以在信号中这样返回 [subscriber sendNext:nil])

优点:这个接口请求过程,ViewController只需要将接口所需参数传入,即可得到接口的结果,大大简化了控制器层面的内容,使得控制器更加专注于页面之间的业务处理,数据传递等功能。

2、多个接口的同时调用 (以下的接口信号创建过程,不再描述)

//获取血压收缩的数据 接口信号RACSignal*systolicSignal=[DataStatisticsViewModel getItemDataByPersonId:personId baseItemId:self.systolicItemModel.baseItemId];//获取血压舒张压的数据 接口信号RACSignal*diastolicSignal=[DataStatisticsViewModel getItemDataByPersonId:personId baseItemId:self.diastolicItemModel.baseItemId];@weakify(self)//因为两个接口是需要同时获取到数据的,所以可以使用combineLatest组合信号[[RACSignal combineLatest:@[systolicSignal,diastolicSignal]]subscribeNext:^(RACTuple*_Nullable x){@strongify(self)//因为是请求了多个接口,所以会有多个数据返回,此处的x是一个元祖,所以使用RACTupleUnpack解包元祖//返回结果值(DataItemRecordModel)的顺序对应combineLatest中数组的信号顺序RACTupleUnpack(DataItemRecordModel*systolicModel,DataItemRecordModel*diastolicModel)=x;//这里可以直接使用返回值  systolicModel  和  diastolicModel}error:^(NSError*_Nullable error){@strongify(self)//没有数据[selfhandleTheErrorMessage:error];}];

多个接口同时调用的过程同单个接口请求类似。

需注意:

(一)多个接口同时请求时,只要有其中一个返回错误信息,整个结果即为失败,即会走error:^(NSError * _Nullable error){}这个block,所以必须多个接口都成功时,才会调用subscribeNext:^(RACTuple * _Nullable x){}block。

(二)可以对结果先做聚合处理,返回再返回结果,比如:

[[RACSignal combineLatest:@[systolicSignal,diastolicSignal]reduce:^id(DataItemRecordModel*systolicModel,DataItemRecordModel*diastolicModel){//reduce中对数据进行处理,可以将多个接口请求的数据,处理之后,统一返回一个结果returnsystolicModel;//也可以将处理完的数据包装成元祖返回RACTuple*tuple=RACTuplePack(systolicModel,diastolicModel);returntuple;}]subscribeNext:^(id  _Nullable x){//这里获取到reduce处理完成之后的数据}error:^(NSError*_Nullable error){//这里处理错误信息}];

以上为网络请求接口时的例子,更多的使用方式可以结合(5、事件信号)中的各种信号事件😆

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351