ReactiveCocoa(RAC)-iOS

简介


ReactiveCocoa(简称为RAC),RAC具有函数响应式编程特性,由Matt Diephouse开源的一个应用于iOS和OS X的新框架。

为什么使用RAC?


因为RAC具有高聚合低耦合的思想所以使用RAC会让代码更简洁,逻辑更清晰。

如何在项目中添加RAC?


platform:ios, '7.0'
pod 'ReactiveCocoa','~>2.0'

  • 其他方法看最下方官方链接

工作原理


工作原理

常见类解释


1. Stream - 信号流值 - RACStream类
表示一个基本单元可以为任意值,其值会随着事件的变化而变化,可以在其上进行一些复杂的操作运算(map,filter,skip,take等.)此类不会被经常使用, 多情况下表现为signal和sequences(RACSignal 和RACSequence继承于RACStream类)

[[RACObserve(self, reactiveString)
    filter:^BOOL(NSString *value) {
        return [value hasPrefix:@"A"];
}]
subscribeNext:^(NSString *value) {
        NSLog(@"%@",value);
}];

2. Signals - 信号 - RACSignal类

RACSignal能力

什么是Signals?


Signals

有订阅者监听时信号才会发信息, Signals会向那个订阅者发送0或多个载有数值的”next”事件,后面跟着一个”complete”事件或一个”error”事件。
Signals会发送三种不同信号给Subscriber

  • next:是可以为nil的新值, RACStream方法只能在这个值上进行操作运算。
  • error:表示在Signals完成之前发生了错误,值不会在RACStream类中存储。
  • completed:表示Signals成功的完成,值不会在RACStream类中存储。
订阅者监听
__block int aNumber = 0;
// Signal that will have the side effect of incrementing `aNumber` block
// variable for each subscription before sending it.
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    aNumber++;
    [subscriber sendNext:@(aNumber)];
    [subscriber sendCompleted];
    return nil;
}];
        
// This will print "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber one: %@", x);
}];
        
// This will print "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber two: %@", x);
}];

如果需要对信号进行过滤,转换,分解和合并那些值的话则不同的订阅者可能需要使用信号通过不同方式发送的值。


信号处理
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
RAC(self.Button, alpha) = [usernameIsValidSignal
    map:^(NSNumber *valid) {
        return valid. boolValue?@1:@0.5;
}];

3. Subscriber - 订阅者 - RACSubscriber协议
表示能够接收信号的对象,订阅信号才会激活信号,实现RACSubscriber协议的对象都可以为订阅者。
可以通过- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock 订阅信号。

RACSignal *repeatSignal = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] repeat];
[repeatSignal subscribeNext: ^(NSDate* time){
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      [formatter setDateFormat:@"HH:mm:ss"];
      NSLog(@"%@",[formatter stringFromDate:time]);
}];

4. Subjects - 手动控制信号 - RACSubject
表示可以手动控制信号,
处理流程:创建信号-订阅信号-发送信号

// 1.创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号 First
[subject subscribeNext:^(id x) {
    // block调用时刻:当信号发出新值,就会调用.
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 2.订阅信号 Second
[subject subscribeNext:^(id x) {
      // block调用时刻:当信号发出新值,就会调用.
      NSLog(@"SecondSubscribeNext%@",x);
}];
// 3.发送信号
[subject sendNext:@"1"];
[subject sendNext:@"2"];

也是RAC代码与非RAC代码的Bridge 所以非常有用,此类继承于RACSignal类。

5. ReplaySubject - 手动回放控制信号 - RACReplaySubject
表示可以手动控制信号,底层实现和RACSubject不一样,它会先把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock然后调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock。
可以有以下两种处理流程:

处理流程 1:创建信号-订阅信号-发送信号(和Subjects一样)
处理流程 2:创建信号-发送信号-订阅信号

// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];
// 2.发送信号
[replaySubject sendNext:@"1"];
[replaySubject sendNext:@"2"];
// 3.订阅信号 First
[replaySubject subscribeNext:^(id x) {
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 3.订阅信号 Second
[replaySubject subscribeNext:^(id x) {
      NSLog(@"SecondSubscribeNext%@",x);
}];

6. Command- 命令信号 - RACCommand
表示订阅响应Action信号,通常由UI来出发,比如一个Button当控件被触发时会被自动禁用掉。

UIButton *reactiveBtn = [[UIButton alloc] init];
[reactiveBtn setTitle:@"点我" forState:UIControlStateNormal];
    reactiveBtn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(UIButton *input) {
    NSLog(@"点击了我:%@",input.currentTitle);
    //返回一个空的信号量
    return [RACSignal empty];
}];

7. Sequences- 集合 - RACSequence
表示一个不可变的序列值且不能包含空值,使用-rac_sequence.signal来获取Signal。

RACSignal *signal = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// Outputs
[signal subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

8. Disposables- 清理订阅 - RACDisposable
表示用于取消信号的订阅,当一个signal被subscriber后,当执行sendComplete或sendError时subscriber会被移除,或者手动调用[disposable dispose]进行移除操作。
当subscriber被移除后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。

9. Scheduler- 计划 - RACScheduler
表示一个信号队列,是信号执行任务时所在的队列或者信号执行完成后将结果放到队列里执行,它支持取消对列里的执行并总是串行执行。

RAC常用宏


RACObserve(TARGET, KEYPATH)
表现形式:RACObserve(self, stringProperty)
KVO的简化版本 相当于对TARGET中KEYPATH的值设置监听,返回一个RACSignal

RAC(TARGET, ...)
表现形式:RAC(self, stringProperty) = TextField.rac_textSignal
第一个是需要设置属性值的对象,第二个是属性名
RAC宏允许直接把信号的输出应用到对象的属性上
每次信号产生一个next事件,传递过来的值都会应用到该属性上

RACChannelTo(TARGET, ...)
RACChannelTo 用于双向绑定
RACChannelTo(self, stringProperty)=RACChannelTo(self.label, text) ;

RAC结构图


RAC结构图

RAC基础使用


创建一个TextField名为usernameTextField 设置监听TextField

[self.usernameTextField.rac_textSignal 
    subscribeNext:^(id x){
    NSLog(@"%@", x);
}];

filter:如果想添加一个条件 只输出x的长度大于3的,可以使用filter操作来实现这个目的

[self.usernameTextField.rac_textSignal
    filter:^BOOL(NSString* text){
    return text.length > 3;
}];
filter

map:把text转换成length进行输出,使用map可以对信号进行转换,一个源信号转换成另外一个新的信号输出

[[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
  return @(text.length);
}]
filter:^BOOL(NSNumber*length){
  return[length integerValue] > 3;
}]
subscribeNext:^(id x){
  NSLog(@"%@", x);
}];
map

信号可聚合也可以分割

聚合: 多个信号可以聚合成一个新的信号,这个可以是任何类型的信号

RACSignal *signal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                    reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
                      return @([usernameValid boolValue]&&[passwordValid boolValue]);
                    }];

分割:一个信号可以有很多subscriber,也就是作为很多后续步骤的源

RACSignal *signal = self.usernameTextField.rac_textSignal;
    [signal subscribeNext:^(id x) {
        NSLog(@"1111");
    }];
    [signal subscribeNext:^(id x) {
        NSLog(@"2222");
    }];
}

RAC设置Button的ControlEvents

[[self.signInButton
     rac_signalForControlEvents:UIControlEventTouchUpInside]
     subscribeNext:^(id x) {
     NSLog(@"button click");
}];

rac_signalForControlEvents

登陆功能举例说明
需要实现登陆功能需要点击登陆button

- (RACSignal *)signInSignal {
    return [RACSignal createSignal:^RACDisposable *(id subscriber){
     [self.signInService 
     signInWithUsername:self.usernameTextField.text
               password:self.passwordTextField.text
               complete:^(BOOL success){
                    [subscriber sendNext:@(success)];
                    [subscriber sendCompleted];
       }];
     return nil;
    }];
}
[[[[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];
    }
}];

flattenMap:[self signInSignal]返回的也是signal,所以是信号中的信号,使用这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。


doNext:为一个附加操作,在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

流程

RAC高级使用


error 和 completed,节流,线程,延伸,其他

内存管理

ReactiveCocoa设计的一个目标就是支持匿名生成管道这种编程风格。到目前为止,在你所写的所有响应式代码中,这应该是很直观的。
为了支持这种模型,ReactiveCocoa自己持有全局的所有信号。如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。

如何取消订阅一个signal?
在一个completed或者error事件之后,订阅会自动移除。你还可以通过RACDisposable 手动移除订阅。

RACSignal的订阅方法都会返回一个RACDisposable实例,它能让你通过dispose方法手动移除订阅。这个方法并不常用到,但是还是有必要知道可以这样做。

RACSignal *backgroundColorSignal =
  [self.searchText.rac_textSignal 
      map:^id(NSString *text) { 
          return [self isValidSearchText:text] ? 
              [UIColor whiteColor] : [UIColor yellowColor]; 
  }]; 

RACDisposable *subscription = 
[backgroundColorSignal 
    subscribeNext:^(UIColor *color) {
        self.searchText.backgroundColor = color; 
}]; 

[subscription dispose];​

避免循环引用
在ReactiveCocoa中提供了避免循环引用的方法
@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),
@strongify让你创建一个对之前传入@weakify对象的强引用。

@weakify(self) 
[[self.searchText.rac_textSignal 
map:^id(NSString *text) { 
    return [self isValidSearchText:text] ? 
        [UIColor whiteColor] : [UIColor yellowColor]; 
}] 
subscribeNext:^(UIColor *color) { 
    @strongify(self) 
    self.searchText.backgroundColor = color; 
}];​

signal能发送3种不同类型的事件
Next
Completed
Error

当应用获取访问社交媒体账号的权限时,用户会看见一个弹框。这是一个异步操作,因此把这封装进一个signal是很好的选择

-(RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error 
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                           code:RWTwitterInstantErrorAccessDenied 
                                       userInfo:nil];
                                   
// 2 - create the signal 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    // 3 - request access to twitter 
    @strongify(self) 
    [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType 
           options:nil 
        completion:^(BOOL granted, NSError *error) {
        // 4 - handle the response 
        if (!granted) { 
           [subscriber sendError:accessError]; 
        } else { 
            [subscriber sendNext:nil]; 
            [subscriber sendCompleted]; 
        } 
    }]; 
return nil; 
}]; 
}​

then:then方法会等待completed事件的发送,然后再订阅由then block返回的signal。这样就高效地把控制权从一个signal传递给下一个。

[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];​
then

实时搜索内容方法

  • 创建请求链接方法


-(SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; 
NSDictionary *params = @{@"q" : text}; 
SLRequest *request = [SLRequest   requestForServiceType:SLServiceTypeTwitter 
                                    requestMethod:SLRequestMethodGET 
                                              URL:url 
                                       parameters:params]; 
return request; 
}​
  • 创建请求signal
-(RACSignal *)signalForSearchWithText:(NSString *)text { 
// 1 - define the errors 
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                               code:RWTwitterInstantErrorNoTwitterAccounts 
                                           userInfo:nil]; 
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                                    code:RWTwitterInstantErrorInvalidResponse 
                                                    userInfo:nil]; 
                                                    
// 2 - create the signal block 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    @strongify(self); 
    
    // 3 - create the request 
    SLRequest *request = [self requestforTwitterSearchWithText:text]; 
    
    // 4 - supply a twitter account 
    NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];       
    if (twitterAccounts.count == 0) { 
        [subscriber sendError:noAccountsError]; 
    } else { 
        [request setAccount:[twitterAccounts lastObject]]; 
        
    // 5 - perform the request 
    [request performRequestWithHandler: ^(NSData *responseData, 
            NSHTTPURLResponse *urlResponse, NSError *error) { 
        if (urlResponse.statusCode == 200) { 
        
            // 6 - on success, parse the response 
            NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData 
                                            options:NSJSONReadingAllowFragments 
                                              error:nil]; 
            [subscriber sendNext:timelineData]; 
            [subscriber sendCompleted]; 
        } else { 
            // 7 - send an error on failure 
            [subscriber sendError:invalidResponseError]; 
        } 
    }]; 
} 
return nil; 
}];
}
  • 使用flattenMap来把每个next事件映射到一个新的signal
[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

线程

在subscribeNext:error:中的数据没有在主线程(Thread 1)中执行,更新UI只能在主线程中执行,所以更新UI需要转到主线程中执行。

要怎么更新UI呢?
通常的做法是使用操作队列但是ReactiveCocoa有更简单的解决办法,在flattenMap:之后添加一个deliverOn:操作就可以转到主线程上了。
:如果你看一下RACScheduler类,就能发现还有很多选项,比如不同的线程优先级,或者在管道中添加延迟。

[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

异步加载图片

-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { 
RACScheduler *scheduler = [RACScheduler 
    schedulerWithPriority:RACSchedulerPriorityBackground]; 
    
return [[RACSignal createSignal:^RACDisposable *(id subscriber) { 
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 
    UIImage *image = [UIImage imageWithData:data]; 
    [subscriber sendNext:image]; 
    [subscriber sendCompleted]; 
    return nil; 
}] subscribeOn:scheduler]; 
}

首先获取一个后台scheduler,来让signal不在主线程执行。然后,创建一个signal来下载图片数据,当有订阅者时创建一个UIImage。最后是subscribeOn:来确保signal在指定的scheduler上执行。

 -(UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
[[[[self signalForLoadingImage:tweet.profileImageUrl] 
takeUntil:cell.rac_prepareForReuseSignal] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(UIImage *image) { 
    cell.twitterAvatarView.image = image; 
}];
return cell;
}

cell是重用的,可能有脏数据,所以上面的代码首先重置图片。然后创建signal来获取图片数据。你之前也遇到过deliverOn:这一步,它会把next事件发送到主线程,这样subscribeNext:block就能安全执行了。

cell.rac_prepareForReuseSignal:Cell复用时的清理。
takeUntil:当给定的signal完成前一直取值

节流

每次输入一个字,搜索都会马上执行。如果你输入很快(或者只是一直按着删除键),这可能会造成应用在一秒内执行好几次搜索,这很不理想。
更好的解决方法是,当搜索文本在短时间内,比如说500毫秒,不再变化时,再执行搜索。
在filter之后添加一个throttle步骤:

[[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
throttle:0.5] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(NSDictionary *jsonSearchResult) { 
    NSArray *statuses = jsonSearchResult[@"statuses"]; 
    NSArray *tweets = [statuses linq_select:^id(id tweet) { 
        return [RWTweet tweetWithStatus:tweet]; 
    }]; 
    [self.resultsViewController displayTweets:tweets]; 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

throttle:只有当前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。

代替代理

如果想在其他地方监听到tableView的代理信息则需要设置如下方法

[[tableView rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:) fromProtocol:@protocol(UITableViewDelegate) ] subscribeNext:^(RACTuple * x) {
    NSLog(@"点击了");
}];

rac_signalForSelector: fromProtocol: 要先绑定在设置代理

测试Demo


ReactiveCocoaTest

官方链接地址


ReactiveCocoa开源地址
官方开发使用文档-Swift
官方开发使用文档-OC

扩展阅读


ReactiveCocoa 基本用法
说说ReactiveCocoa 2
学习RAC小记-适合给新手看的RAC用法总结
ReactiveCocoa-tutorial-pt1
ReactiveCocoa-tutorial-pt2
MVVC-RAC

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

推荐阅读更多精彩内容