多线程在iOS 中的应用


一、进程

1.1 什么是进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
  • 比如同时打开Xcode和QQ,就会开启两个进程
  • 通过活动“活动监视器”可以查看MAC系统中所有的进程

二、线程

2.1 什么是线程

  • 一个进程要想执行任务,必须得有线程(每一个进程至少要有一个线程)
  • 线程是进程的基本单元,一个程序所有的任务都做线程中执行
  • 使用迅雷下载电影、使用网易云听歌,都需要在线程中执行

2.2 线程的串行

  • 一个线程的任务是串行的(同一时间内,一个线程只能执行一个任务)

三、多线程

3.1 what

  • 一个进程中可以开启多条线程,每条线程可以同时(并行)执行不同的任务
  • 多线程可以提高程序的执行效率

3.2 多线程的原理

  • 同一时间,cpu只能处理一条线程
  • 多线程并发执行,其实是cpu快速的在多条线程之间调度(切换),如果cpu调度线程的时间足够快,就造成了多线程并发执行的假象。

3.3 多线程的优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(cpu 、内存)

3.4 多线程的缺点

  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,cpu在调度线程上的开销就越大
  • 使程序的设计更加复杂:比如多线程之间的通信,多线程之间的数据共享

四、多线程在iOS中的应用

4.1 什么是主线程

  • 一个ios程序运行后,默认会开启一条线程,称为“主线程”或“UI线程”

4.2 主线程的主要作用

  • 显示\刷新UI界面
  • 处理UI事件(比如点击事件、滚动事件、拖拽事件等)

4.3 主线程的使用注意

  • 别将比较耗时的操作放到主线程中,耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的体验。

4.4 实现方案


4种方案.png

五、NSThread

5.1 what
一个NSThread对象就代表一个线程

  • 创建、启动线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"哈哈"]; thread.name = @"线程A";//自己来区别的 // 开启线程 [thread start];
  • 获得当前线程
    NSThread *current = [NSThread currentThread]; NSLog(@"btnClick---%@", current);
  • 其他创建线程的方式
    //创建完线程直接(自动)启动 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"我是参数"]; // 在后台线程中执行 === 在子线程中执行 [self performSelectorInBackground:@selector(run:) withObject:@"abc参数"]; 以上2种方式的优点:快捷方便 缺点:无法对线程进行更详细的设置

六、线程的状态

线程一共有5种状态,如图:


线程的状态流程图.png

6.1新建状态和就绪状态
现在以NSThread为例,黄色为例。
[tehread start]之前是新建状态,start之后进入了就绪状态(cpu在调度其他进程的时候也是处于调度状态),这个时候就进入了可调度线程池,放进可调度的线程池的线程是可以来回进行调度的。
6.2运行状态
线程经过cpu调度之后就进入了运行状态。
6.3阻塞状态
调用了sleep方法/等待同步锁的时候,就进入了阻塞状态。
6.4死亡状态
线程任务执行完毕,或者异常/强制退出等,就进入了死亡状态。

七、多线程的安全隐患

7.1 why

  • 资源共享
    一个资源可能被多个资源共享(多个线程可能访问同一个资源),比如多个线程访问同一个变量、同一个对象、同一个文件等,当多个线程访问同一个资源时,很容易引发数据安全和数据错乱问题
    如下图分析:


    安全隐患分析.png

    7.2解决方法

  • 互斥锁


    安全隐患解决.png

@synchronized(锁对象){
}
注意:锁定一份代码只用一把锁,用多把锁是无效的
*互斥锁的优点、缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的cpu资源

  • 线程同步
    多条线程按顺序的执行任务
    互斥锁就是使用了线程同步技术

  • 原子性和非原子性
    oc在定义属性时有2种选择

    • atomic: 原子属性,为setter方法加锁(默认就是atomic),线程安全,需要消耗大量的资源
    • nonatmic: 非原子属性,不会为setter方法加锁,非线程安全,适合内存小的移动设备
  • ios开发建议:所有的属性都声明为nonatmic,尽量避免多线程抢夺同一块资源,尽量将加锁、抢夺资源的业务逻辑交给服务器端处理,减小移动端的压力。

八、线程之间的通信

8.1 what
在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
8.2 体现

  • 一个线程传递数据给另外一个线程
  • 在一个线程中执行完一个特定的任务后,转到另一个线程继续执行任务。

import "ViewController.h"

@interface HMViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation HMViewController
-(void)viewDidLoad{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event
{
// 在子线程中调用download方法下载图片
[self performSelectorInBackground:@selector(download) withObject:nil];
}
/

下载图片 : 子线程
*/
-(void)download
{
// 1.根据URL下载图片
NSURL *url = [NSURL URLWithString:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"];
NSLog(@"-------begin");
NSData data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
NSLog(@"-------end");
UIImage image = [UIImage imageWithData:data];
// 2.回到主线程显示图片
// [self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];
// setImage: 1s
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
// [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO];
}
/

设置(显示)图片: 主线程
*/
//- (void)settingImage:(UIImage *)image
//{
// self.imageView.image = image
//}
@end

九、GCD

9.1 what

  • 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”。
  • 纯c语言,提供了非常多、强大的函数

9.2 优势

  • GCD是苹果公司为多核的并行运算提出的解决法案
  • GCD会自动利用更多的CPU内核,会自动管理线程的生命周期(创建线程、调度线程、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

9.3 队列和任务

  • 任务:执行什么操作
  • 队列:用来存放任务
    • 并发队列:可以让多个任务同时执行,只有在异步函数下才有效
  • 串行队列:让任务一个接着一个的执行
  • 同步:在当前线程中执行,不具备开启线程的能力
  • 异步:在另一条线程中执行,具备开启线程的能力

GCD会自动将队列中的任务取出,放到对应的线程中执行,任务的取出遵循队列的FIFO原则:先进先出,后进后出
9.4 并发对列
GCD默认已经提供了全局的并发对列,供整个应用使用,不需要手动创建
// 1.获得全局的并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
9.5串行队列
GCD中获得串行有2中途径
// 1.创建串行队列 dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", NULL);
//2.使用主队列(跟主线程相关联的队列) dispatch_queue_t queue = dispatch_get_main_queue();
各队列的执行效果

e.png

上代码

#import "ViewController.h"

@interface HMViewController ()

@end

@implementation HMViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self performSelectorInBackground:@selector(test) withObject:nil];
    
//    [self syncMainQueue];
}

- (void)test
{
    NSLog(@"test --- %@", [NSThread currentThread]);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务 --- %@", [NSThread currentThread]);
    });
}

/**
 * 使用dispatch_async异步函数, 在主线程中往主队列中添加任务
 */
- (void)asyncMainQueue
{
    // 1.获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
}

/**
 * 使用dispatch_sync同步函数, 在主线程中往主队列中添加任务 : 任务无法往下执行
 */
- (void)syncMainQueue
{
    // 1.获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
//    dispatch_sync(queue, ^{
//        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
//    });
//    dispatch_sync(queue, ^{
//        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
//    });
    
    // 不会开启新的线程, 所有任务在主线程中执行
}

// 凡是函数名种带有create\copy\new\retain等字眼, 都需要在不需要使用这个数据的时候进行release
// GCD的数据类型在ARC环境下不需要再做release
// CF(Core Foundation)的数据类型在ARC环境下还是需要再做release

/**
 * 用dispatch_sync同步函数往串行列中添加任务
 */
- (void)syncSerialQueue
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", NULL);
    
    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });
    
    // 3.释放资源
//    dispatch_release(queue);   // MRC(非ARC)
    
    // 总结: 不会开启新的线程
}

/**
 * 用dispatch_sync同步函数往并发队列中添加任务
 */
- (void)syncGlobalQueue
{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });
    
    // 总结: 不会开启新的线程, 并发队列失去了并发的功能
}

/**
 * 用dispatch_async异步函数往串行队列中添加任务
 */
- (void)asyncSerialQueue
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", NULL);
    
    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });
    
    // 总结: 只开1个线程执行任务
}

/**
 * 用dispatch_async异步函数往并发队列中添加任务
 */
- (void)asyncGlobalQueue
{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });
    
    // 总结: 同时开启了3个线程
}

@end

9.6 线程间的通信
从子线程回到主线程
上代码

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSLog(@"--download--%@", [NSThread currentThread]);
        // 下载图片
        NSURL *url = [NSURL URLWithString:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
        UIImage *image = [UIImage imageWithData:data];
        
        // 回到主线程显示图片
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"--imageView--%@", [NSThread currentThread]);
            self.imageView.image = image;
        });
    });
}

9.7 延时执行
2种方法

- (void)delay
{
    //    NSLog(@"----touchesBegan----%@", [NSThread currentThread]);
    
    //    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self performSelector:@selector(run) withObject:nil afterDelay:2.0];//第一种
    //    });
    // 1.全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2.计算任务执行的时间
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
    
    // 3.会在when这个时间点, 执行queue中的任务。第二种
    dispatch_after(when, queue, ^{
        NSLog(@"----run----%@", [NSThread currentThread]);
    });
}
//- (void)run
//{
//    NSLog(@"----run----%@", [NSThread currentThread]);
//}

9.8一次性代码
保证某段代码在程序运行的过程中只被执行1次

   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       NSLog(@"-------touchesBegan");
   });
}

9.9 队列组
首先有一个需求,分别异步执行2个耗时的操作,等2个异步操作都执行完毕后,再回到主线程执行操作。
dispatch_group_t group = dispatch_group_create();
上代码

 /**
     1.下载图片1和图片2
     
     2.将图片1和图片2合并成一张图片后显示到imageView上
     
     思考:
     * 下载图片 : 子线程
     * 等2张图片都下载完毕后, 才回到主线程
     */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 创建一个组
    dispatch_group_t group = dispatch_group_create();
    
    // 开启一个任务下载图片1
    __block UIImage *image1 = nil;
    dispatch_group_async(group, global_queue, ^{
        image1 = [self imageWithURL:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"];
    });
    
    // 开启一个任务下载图片2
    __block UIImage *image2 = nil;
    dispatch_group_async(group, global_queue, ^{
        image2 = [self imageWithURL:@"http://news.baidu.com/z/resource/r/image/2014-06-22/b2a9cfc88b7a56cfa59b8d09208fa1fb.jpg"];
    });
    
    // 同时执行下载图片1\下载图片2操作
    
    // 等group中的所有任务都执行完毕, 再回到主线程执行其他操作
    dispatch_group_notify(group, main_queue, ^{
        self.imageView1.image = image1;
        self.imageView2.image = image2;
        
        // 合并
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0);
        [image1 drawInRect:CGRectMake(0, 0, 100, 100)];
        [image2 drawInRect:CGRectMake(100, 0, 100, 100)];
        self.bigImageView.image = UIGraphicsGetImageFromCurrentImageContext();
        // 关闭上下文
        UIGraphicsEndImageContext();
    });

}
- (UIImage *)imageWithURL:(NSString *)urlStr
{
    NSURL *url = [NSURL URLWithString:urlStr];
    NSData *data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
    return [UIImage imageWithData:data];
}

十、NSOperation

10.1 what
作用:配合使用NSOperation和NSOperationQueue也能实现多线程编程
实现步骤:

  • 先将需要执行的操作封装到一个NSOperation对象中

  • 然后将NSOperation对象添加到NSOperationQueue中

  • 系统会自动将NSOperation中封装的操作放到一条新线程中执行

10.2 how
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
使用NSOperationa子类有3种

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation,实现内部相应的方法

10.2.1 NSInvocationOperation

  • 创建SInvocationOperation对象
  • 调用start方法开始执行操作
    注意:
    默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到NSOperationQueue中,才会异步执行操作。
-(void)invocationOperation
{
    // 1.创建操作对象, 封装要执行的任务
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    // 2.执行操作(默认情况下, 如果操作没有放到队列queue中, 都是同步执行)
    [operation start];
}
-(void)download
{
    for (int i = 0; i<10; i++) {
        NSLog(@"------download---%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.1];
    }
}

10.2.2 NSBlockOperation
如下代码会开启3个线程,线程数取决于任务的个数

-(void)blockOperation
{
    // 1.封装操作
//    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//        NSLog(@"NSBlockOperation------下载图片1---%@", [NSThread currentThread]);
//    }];
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];
    
    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片1---%@", [NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片2---%@", [NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片3---%@", [NSThread currentThread]);
    }];
    
    // 2.执行操作
    [operation start];
}

添加到NSOperationQueue的操作

- (void)operationQueue
{
    // 1.封装操作
    NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
//    operation1.queuePriority = NSOperationQueuePriorityHigh
    
    NSInvocationOperation *operation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i<10; i++) {
            NSLog(@"NSBlockOperation------下载图片---%@", [NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.1];
        }
    }];
//    [operation3 addExecutionBlock:^{
//        for (int i = 0; i<10; i++) {
//            NSLog(@"NSBlockOperation------下载图片2---%@", [NSThread currentThread]);
//            [NSThread sleepForTimeInterval:0.1];
//        }
//    }];
    
    // 2.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//设置并发数
    queue.maxConcurrentOperationCount = 2; // 2 ~ 3为宜
    
    // 设置依赖
    [operation2 addDependency:operation3];
    [operation3 addDependency:operation1];
    
    // 3.添加操作到队列中(自动执行操作, 自动开启线程)
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];
    
//    [queue setSuspended:YES];
}

10.3 对列的暂停、取消和恢复
[queue setSuspended:YES];//yes代表暂停,no代表恢复
[queue cancel]// 取消
10.4 操作依赖(代码在上边)
10.5 NSOperation下载图片的思路

h.png

github上有一个比较好的下载图片框架,sdwebimage,可以去研究一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容