iOS ReactiveCocoa框架的简单使用

ReactiveCocoa框架的使用教程在网上有很多详细的博客可参考,通过学习,我自己也整理了一下,一来便于自己复习,二来分享给大家。先粘贴些优质博文的链接,然后下面以实例的形式一步步讲解。

优质技术博客链接

地址1:ReactiveCocoa入门教程——第一部分
地址2:ReactiveCocoa入门教程——第二部分
地址3:1个小时学会ReactiveCocoa基本使用

下面开始介绍ReactiveCocoa的使用

一、在项目中集成ReactiveCocoa框架

既然是第三方框架,那用CocoaPods集成是最方便的。
首先,创建一个工程
然后,在工程中创建Podfile文件,文件中的内容如下:
platform :ios,'9.0'
use_frameworks!
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 2.5'
end

注意1:

集成ReactiveCocoa框架和其他的不同之处是多了一个“use_frameworks!”,我在使用过程中发现,2.5版本以上的更高的版本要加上“use_frameworks!”,否则会报错,导致集成不了。而2.5版本之前的(包括2.5版本),就不需要加“use_frameworks!”,

注意2:

ReactiveCocoa现在的最高版本已经到5.0了,问题是,如果用swift编程,那么集成最新版本的ReactiveCocoa框架没有问题,但是如果使用OC编程的话,那最高只能集成2.5版本的RAC(RAC是ReactiveCocoa的简称),否则集成好了以后工程会报错。

简单的说就是,如果你用swift编程,用Cocoapods集成时,Podfile文件这么写
platform :ios,'9.0'
use_frameworks!
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 5.0'
end
如果你用的是oc编程,用Cocoapods集成时,Podfile文件这么写
platform :ios,'9.0'
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 2.5'
end
最后,上面的工作都做好了,就可以集成RAC了,很快.


屏幕快照 2017-02-27 上午10.59.13.png
二、RAC的简单使用----RACSignal

在要使用的RAC的控制器中导入RAC框架的头文件

#import <ReactiveCocoa/ReactiveCocoa.h>

现在来熟悉下RACSignal的使用,从名字就可以看出,它是信号。在viewDidload中加入下面的代码

//创建信号
    RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"想");
        [subscriber sendNext:@"发送了信号"];//发送信号
        NSLog(@"你");
        [subscriber sendCompleted];//发送完成,订阅自动移除
        //RACDisposable 可用于手动移除订阅
        return [RACDisposable disposableWithBlock:^{
            NSLog(@"豆腐");
        }];
    }];
    //订阅信号
    NSLog(@"我");
    [single subscribeNext:^(id x) {
        NSLog(@"吃");
//        NSLog(@"信号的值:%@",x);
    }];

运行,得到结果如下


RACSinale.png

这样就可以清楚的看明白,信号的运行流程,但是感觉好乱,下面分析一下:
1.createSignal方法 是创建信号,创建好的信号,没有被订阅前,只是冷信号,此时是不会走createSignal后面的block的。
程序往下,就走到“NSLog(@"我")”,

2.然后走到subscribeNext,这一步就是订阅信号,订阅号信号后,信号single就变成了热信号,

3.既然变成热信号,就开始走createSignal后面的block中的去,所以就打印出了“NSLog(@"想")”。

4.下面是sendNext,即发送信号,发送了信号,订阅者就会收到信号,发送的内容可以从订阅信号subscribeNext后面的block中获取到,程序就走到subscribeNext后面的block中,所以就打印了“NSLog(@"吃")”,

5.当订阅信号的subscribeNext后面的block走完以后,程序又回到,createSignal后面的block中,继续未完成的代码,所以就打印“NSLog(@"你")”,继续往下就是[subscriber sendCompleted],这句代码的意思是,发送完成了,订阅自动移除,没有了订阅者了,信号又变成了冷信号。

6.接下来就是return,返回一个RACDisposable对象,这个的作用就是,可以用来手动移除订阅。RACDisposable对象,创建完成,就走进创建方法的block中,也就是打印NSLog(@"豆腐")

综上,打印出来的结果就是“我想吃你豆腐”,它就是这样出来的

这里再介绍下RACDisposable的使用,将代码改一下

    //创建信号
    RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"想");
        [subscriber sendNext:@"发送了一个信号"];//发送信号
        NSLog(@"你");
        //RACDisposable 手动移除订阅者
        return [RACDisposable disposableWithBlock:^{
            NSLog(@"豆腐");
        }];
    }];
    //订阅信号
    NSLog(@"我");
    RACDisposable * disposable = [single subscribeNext:^(id x) {
        NSLog(@"吃");
        NSLog(@"信号的值:%@",x);
    }];
    //手动移除订阅
    [disposable dispose];

打印结果如下



在稍微分析一下,两份代码不同之处是,删去了自动移除订阅[subscriber sendCompleted],添加了手动删除订阅[disposable dispose],手动删除订阅,可以在你想要的地方,合适的时候进行操作。不过手动删除用的少。那既然用得少,我们还是用自动删除吧,优化下,见代码

//创建信号
    RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"想");
        [subscriber sendNext:@"发送了信号"];//发送信号
        NSLog(@"你");
        [subscriber sendCompleted];//发送完成,订阅自动移除
        //RACDisposable 手动移除订阅者
        return nil;
    }];
    //订阅信号
    NSLog(@"我");
    [single subscribeNext:^(id x) {
        NSLog(@"吃");
        NSLog(@"信号的值:%@",x);
    }];

打印结果如下



好了,没豆腐吃了!其实也不需要。返回nil就可以了

上面罗里吧嗦的说了那么多,就是为了理清里面的逻辑,没有结合实际使用,其实听起来还是很迷糊,下面就结合实际,来使用RAC

第二、RAC的常用方法

上面是使用RACSignal创建信号,其实文本框中文字改变也是信号,按钮点击也是信号,RAC为UITxtField和UIButton创建categary,并做好了封装,直接就可以调用它们的信号,这里就围绕着这两个类,进行ARC的使用讲解
在工程中新建一个控制器,添加几个控件,textField,textView,button,label,如下图

订阅textField信号

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

因为textField的信号肯定是NSString,类型的,所以可以写成下面的样子,也更方便使用些

[self.textfield.rac_textSignal subscribeNext:^(NSString* x) {
        NSLog(@"%@",x);
    }];

这样,当你在文本框输入时,控制台就会打印出输入的内容,如下


可以看到,每次输入都会获取到信号。

filter---信号过滤器

如果我只需要将字符串长度超过3的,才打印,那可以使用过滤器filter,使用方法如下

[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
        return value.length>3?YES:NO;
    }]subscribeNext:^(NSString* x) {
        NSLog(@"过滤后的到的信号:%@",x);
    }];

在文本框中输入字符,打印结果如下



可以看到,只有当字符串长度大于3的信号,才会被订阅到

map--转换器

map就是将一种信号转换成你想要的另一种信号,这里把字符串信号,转换成文字信号
如果想当文本框中输入的文字长度大于4的时候,改变文本框的背景色,一种方法是把过滤器的条件设置为4,然后在subscribeNext的block中直接给textField.backgroundColor赋值。不过RAC有转换信号的方法---map,如下

[[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
        return value.length>3?YES:NO;
    }]map:^id(NSString* value) {
        return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
    }]subscribeNext:^(UIColor* value) {
        self.textfield.backgroundColor = value;
    }];

上面的代码的意思是,当输入的字符串长度超过3,就将字符串信号转换成颜色信号,然后订阅该颜色信号,并将颜色赋值给textField的背景色。效果如下



这样的话,当字符串大于3,文本框的背景色变成了红色
另外,RAC提供了一个宏"RAC(对象,属性)"来简化代码并增强可读性,如下

RAC(self.textfield ,backgroundColor) = [self.textfield.rac_textSignal map:^id(NSString* value) {
        return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
    }];

RAC宏有两个参数,一个是需要设置的对象,一个是设置的属性。这句代码的意思是,当文本框输入的字符串长度大于4时,改变文本框的背景色。这样的话,看起来更清晰,而达到的效果是一样的。

总结一下,到现在为止,学了过滤器:filter,转换器:map,对象设置属性的宏:RAC(要设置的对象,要设置的属性)。可以想象,用这几个方法可以很方便的实现一些功能,比喻说替代通知,监听事件等。

textField的使用是这样,那textView的使用也是这样的,因为他们完全类似

RAC(self.textView ,backgroundColor) = [self.textView.rac_textSignal map:^id(NSString* value) {
        return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
    }];
combineLatest:reduce:

想象一下,如果当textField和textView同时满足某个条件时,才能进行某项操作的话,应该如何写呢?RAC为我们准备了一个方法--combineLatest:reduce:信号合并
先看代码

RACSignal * mergeTwoSignal = [RACSignal combineLatest:@[self.textfield.rac_textSignal,self.textView.rac_textSignal] reduce:^id(NSString * value1,NSString * value2){
        return [NSNumber numberWithBool:([value1 isEqualToString:@"11111"]&&[value2 isEqualToString:@"22222"])];
    }];
RAC(self.addButton,enabled) = [mergeTwoSignal map:^id(NSNumber* value) {
        return value;
    }];

上面的代码的意思是,当textField中的文字为"11111",同时textView中的文字为"22222"的时候,返回一个信号,信号的类型是NSNumber,然后通过转换器map,将值返回,返回的值用于确定按钮是否可用。
可能会疑问,map中返回的NSNumber类型的,而button的enabled属性是BOOL类型,怎么可以这样直接赋值,但是RAC它就是可以,就是做的这么好。
到这一步,就可以订阅button的点击信号了,看代码就懂了

[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
     subscribeNext:^(NSNumber * value) {
         //标签赋值
         self.displayLabel.text = @"1314";
     }];

运行验证一下,结果如下



确实能达到要求。真好,再也不用给button 添加点击事件了。

doNext

现在讲一下附加操作doNext,它的作用是,在不改变信号的基础上,进行一些附加的操作,比喻说,我在订阅到给label赋值前,改变label的背景色,当然也可以是做别的操作。反正是附加的不会影响信号流的。使用见代码

[[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
     doNext:^(id x) {
         //改变label的背景色
         self.displayLabel.backgroundColor = [UIColor redColor];
     }]
     subscribeNext:^(NSNumber * value) {
         self.displayLabel.text = @"1314";
     }];

这样就实现了,订阅信号前,改变label的背景色

@weakify和@strongify

RAC的所有方法中,大部分是block,所以无法避免在使用过程中导致循环引用,
以前的解决办法是这样的

    __weak SecondViewController *bself = self;
    [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
        doNext:^(id x) {
            //先清掉label中的文字
            bself.displayLabel.textColor = [UIColor redColor];
        }]
       subscribeNext:^(NSNumber * value) {
           bself.displayLabel.text = @"1314";
       }];

如果每个block都写的话,会很费劲,因为block太多了,还好RAC提供了两个宏,@weakify和@strongify,

    @weakify(self);
    [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
      doNext:^(id x) {
          @strongify(self);
          self.displayLabel.textColor = [UIColor redColor];
      }]
     subscribeNext:^(NSNumber * value) {
         @strongify(self);
         self.displayLabel.text = @"1314";
     }];

@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。这样就解决了循环引用的问题

第三、RAC在网络请求和图片加载中的使用

先创建一个控制器,添加若干控件,textView,用来展示请求到的数据,imageView,用来展示图片,

使用系统的方法请求数据

在viewDidload中添加下面的代码

    NSURL * url = [NSURL URLWithString:urlS];
    NSURLSession * session = [NSURLSession sharedSession];
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
    NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSString * dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@",dataString);
        NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        NSLog(@"%@",dic);
        [self performSelector:@selector(actionWithString:) onThread:[NSThread mainThread] withObject:dataString waitUntilDone:YES];
    }];
    [task resume];
//回到主线程给textView赋值
-(void)actionWithString:(id )value{
    self.textView.text = (NSString*)value;
}

结果如下


使用RAC请求网络数据

把系统请求网络数据的方法,封装成信号流

//rac网络请求
-(RACSignal *)racNetworkRequest{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSURL * url = [NSURL URLWithString:urlS];
        NSURLSession * session = [NSURLSession sharedSession];
        NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
        NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSString * dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//            NSLog(@"%@",dataString);
//            NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
//            NSLog(@"%@",dic);
            if (error ==nil) {//返回成功
                [subscriber sendNext:dataString];//发送信号
                [subscriber sendCompleted];//结束发送
            } else {
                [subscriber sendError:error];//发送错误
            }
        }];
        [task resume];
        return nil;
    }];
}

现在就来调用一下看看,在viewDidLoad中添加下面的代码,
self.requestDataButton是一个按钮,使用方法是点击按钮的时候加载数据

    [[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
    map:^id(id value) {
        return [self racNetworkRequest];
    }]
    subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];

订阅信号后,得到的数据如下



发现,得到的不是想要的数据,而是一个信号对象,其实从racNetworkRequest这个方法中就可以看出,返回就是一个RACSignal对象,如果能获取到RACSignal对象里面的信号流就对了,怎么办呢,RAC提供了这样的方法 flattenMap

flattenMap---获取信号中的信号

把上面的代码写成这样,就可以获取到数据了

    [[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
      flattenMap:^id(id value) {
          return [self racNetworkRequest];
      }]
     subscribeNext:^(id x) {
         NSLog(@"%@",x);
     } error:^(NSError *error) {
         NSLog(@"%@",error);
     }];

这样就会发现订阅到的信号,是你想要的数据了。

then---等待上一个信号的完成,然后订阅自己的信号
    [[[self racNetworkRequest]
    then:^RACSignal *{
        return self.textField.rac_textSignal;
    }]
    subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }error:^(NSError *error) {
        NSLog(@"error");
    }];

then方法会等待前面的信号中completed事件的发送完成,然后再订阅由then block返回的信号。这样就高效地把控制权从一个signal传递给下一个。如此就实现了:当请求数据完成,就可以监控到textField中的文字输入了

回到主线程---deliverOn

因为信号的流转及操作都是在block中完成的,也就是说大部分操作都是在子线程中执行的操作,但是有个时候需要回到主线程完成一些事情,比如,请求到数据后,要刷新UI,这就必须回到主线程,RAC提供了这样的方法deliverOn。用法见下面的代码

    @weakify(self)
    [[[[[self racNetworkRequest]
        then:^RACSignal *{
            @strongify(self);
            return self.textField.rac_textSignal;
        }]
       filter:^BOOL(NSString* value) {
           return value.length > 3?YES:NO;
       }]
    deliverOn:[RACScheduler mainThreadScheduler]]//回到主线程
    subscribeNext:^(NSString * value) {
         @strongify(self);
         self.textView.text =value;
         NSLog(@"%@",value);
        NSLog(@"当前线程%@",[NSThread currentThread]);
    } error:^(NSError *error) {
         NSLog(@"%@",error);
     }];

这样的话,实现的效果就是,在textField中输入文字而且当文字大于3的时候,会在textView中显示出来,而且可以看到订阅信号的block中打印出来的线程是主线程,如下:



悲催的是,如果没有加deliverOn:好像也是在主线程。我也不知道什么原因,不知道有没有用,姑且就认为deliverOn有用,可能在开启很多线程的时候会有用吧
不过我可以在subscribeNext的block中加入回到主线程的方法,也能达到目的,如下

    subscribeNext:^(NSString * value) {
         @strongify(self);
         self.textView.text =value;
         NSLog(@"%@",value);
        [self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:YES];
        NSLog(@"当前线程%@",[NSThread currentThread]);
    } error:^(NSError *error) {
         NSLog(@"%@",error);
     }];
信号节流---throttle

用文本框textField作比喻,当我在里面输入字符时,subscribeNext的block会不停的走,每输入一个字符,就会走一遍。如果我想在输入过程中不需要每改变一个字符就走一遍,而是等输入完成或停止的时候再走block里面的代码,那就可以用throttle,先看效果

    [[[self.textField.rac_textSignal
       filter:^BOOL(NSString* value) {
           return value.length > 3?YES:NO;
       }]
      throttle:1]
     subscribeNext:^(NSString * value) {
         @strongify(self);
         self.textView.text =value;
     } error:^(NSError *error) {
         NSLog(@"%@",error);
     }];

这样得到的效果是,当输入字符串长度大于3,而且该字符串的值在1s内没有改变,就把textField中的值,赋值给textView。所以简单的说throttle的作用:如果前面信号在设定的时间内没有变化时,throttle就会把信号传到下面的事件中去。

使用系统的方法加载图片

系统的方法,我就不说了 看代码

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSURL * url = [NSURL URLWithString:imageUrlString];
        UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
        if (image!=nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imageView.image = image;
            });
        }
    });

这样就可以完成图片的加载,看下面的效果


RAC加载图片

创建一个加载图片的方法,方法返回的是RACSignle信号对象,

-(RACSignal*)racRequestImage{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSURL * url = [NSURL URLWithString:imageUrlString];
        UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
        self.imageView.image = image;
        [subscriber sendNext:image];
        [subscriber sendCompleted];
        return nil;
    }];
}

下面就来调用这个方法。实现的效果是,点击按钮(self.requestImageDataButton),即开始加载图片,在viewDidLoad中添加下面的代码

    @weakify(self);
    [[[self.requestImageDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
       flattenMap:^RACStream *(id value) {
           return [self racRequestImage];
       }]
    deliverOn:[RACScheduler mainThreadScheduler]]
    subscribeNext:^(UIImage * image) {
        @strongify(self);
         self.imageView.image = image;
     }];

运行一下,发现,图片加载正常。从代码量来看,GCD可能方便一点,但是,如果是多个事件凑到一起影响图片加载的时候,RAC或许是不错的选择。

到这一步,就把ReactiveCocoa的初步使用讲完了。
总结一下总共学习哪些方法

filter---信号过滤器
map--转换器
combineLatest:reduce:--信号合并
doNext--附加操作
@weakify和@strongify--避免循环
flattenMap---获取信号中的信号
then---等待上一个信号的完成,然后订阅自己的信号
回到主线程---deliverOn
信号节流---throttle
第四、demo下载

GitHub下载地址

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

推荐阅读更多精彩内容