每日一问12——多线程之GCD

GCD介绍

Grand Central Dispatch (GCD) 是 Apple 开发的一个多核编程的解决方法。

一、主要概括
  • 和operation queue一样都是基于队列的并发编程API,他们通过集中管理大家协同使用的线程池。
  • 公开的5个不同队列:运行在主线程中的main queue,3个不同优先级的后台队列(High Priority Queue,Default Priority Queue,Low Priority Queue),以及一个优先级更低的后台队列Background Priority Queue(用于I/O)
  • 可创建自定义队列:串行或并列队列。自定义一般放在Default Priority Queue和Main Queue里。
  • 操作是在多线程上还是单线程主要是看队列的类型和执行方法,并发队列异步执行才能在多线程,并发队列同步执行就只会在这个并发队列在队列中被分配的那个线程执行。

二、基本概念

1.串行与并发
Serial Queues 串行队列

串行队列中的任务一次执行一个,每个任务只在前一个任务完成时才开始。

gcd-串行队列

这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。

dispatch_queue_t queue1 = dispatch_queue_create("serialqueue", DISPATCH_QUEUE_SERIAL);
    for (int i=0; i<100; i++) {
        dispatch_async(queue1, ^{
            NSLog(@"%d",i);
        });
    }

打印顺序一定是1.2.3......98.99。保证了执行顺序。

2.Concurrent Queues 并发队列

在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。

gcd-并发队列

何时开始一个 Block 完全取决于 GCD 。如果一个 Block 的执行时间与另一个重叠,也是由 GCD 来决定是否将其运行在另一个不同的核心上,如果那个核心可用,否则就用上下文切换的方式来执行不同的 Block 。

dispatch_queue_t queue = dispatch_queue_create("concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<100; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d",i);
        });
    }

打印顺序1.2.3.4....55.56.53.....20..99,按照顺序添加进队列,但执行结果并不能保证顺序,结果与GCD的调度有关。

2.GCD中的队列

我们可以通过dispatch_queue_create的方式手动创建队列,通过第二个参数设置队列的类型。既上面提到的(串行或并发)队列。

dispatch_queue_t queue = dispatch_queue_create("queue-label", DISPATCH_QUEUE_CONCURRENT);

除此之外,系统还为我们提供了5种全局队列

  • Main Dispatch Queue 类型:Serial Dispatch Queue 主线程执行
  • Global Dispatch Queue (HIGH) 类型:Concurrent Dispatch Queue 执行优先级:高
  • Global Dispatch Queue (DEFAULT) 类型:Concurrent Dispatch Queue 执行优先级:默认
  • Global Dispatch Queue (LOW) 类型:Concurrent Dispatch Queue 执行优先级:低
  • Global Dispatch Queue (BACKGROUND) 类型: Concurrent Dispatch Queue 执行优先级:后台
dispatch_queue_t
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);
}
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

我们可以发现,除了Main Dispatch Queue是串行队列,其他的均为并发队列,并且拥有不同的优先级。
我们可以通过以下方式获取这些全局队列

//获取主队列
dispatch_queue_t main_queue = dispatch_get_main_queue();
//通过第一个参数获取其他全局队列。
dispatch_queue_t global_queue = dispatch_get_global_queue(`DISPATCH_QUEUE_PRIORITY_HIGH`, 0);
自定义队列的优先级

我们可以看到,系统提供的全局队列的不同之处就是队列的优先级不同。不同的优先级适合于不同的业务场景。GCD中提供了以下几种队列优先级:

__QOS_ENUM(qos_class, unsigned int,
    QOS_CLASS_USER_INTERACTIVE
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x21,
    QOS_CLASS_USER_INITIATED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x19,
    QOS_CLASS_DEFAULT
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x15,
    QOS_CLASS_UTILITY
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x11,
    QOS_CLASS_BACKGROUND
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x09,
    QOS_CLASS_UNSPECIFIED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x00,
);
  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
  • QOS_CLASS_USER_INITIATED:user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
  • QOS_CLASS_UTILITY:utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
  • QOS_CLASS_BACKGROUND:background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。

我们可以通过以下方式为自定义的队列设置队列优先级

1.使用dispatch_queue_attr_make_with_qos_class自定义一个队列描述。
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);
dispatch_queue_t queue = dispatch_queue_create("com.starming.gcddemo.qosqueue", attr);

dispatch_queue_attr_make_with_qos_class的参数。
1.dispatch_queue_attr_t:串行或者并发
2.dispatch_qos_class_t:填写上面的调度类型。
3.relative_priority:qos内的相对优先级,这个参数必须比0大比设置的QOS Class小。

2.使用dispatch_set_target_queue设置与目标相同类型的队列。
dispatch_queue_t queue = dispatch_queue_create("com.starming.gcddemo.settargetqueue",NULL); //需要设置优先级的queue
dispatch_queue_t referQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); //参考优先级
dispatch_set_target_queue(queue, referQueue); //设置queue和referQueue的优先级一样

dispatch_set_target_queue:可以设置优先级,也可以设置队列层级体系,比如让多个串行和并发队列在统一一个串行队列里串行执行.

dispatch_queue_t serialQueue = dispatch_queue_create("com.starming.gcddemo.serialqueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t firstQueue = dispatch_queue_create("com.starming.gcddemo.firstqueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t secondQueue = dispatch_queue_create("com.starming.gcddemo.secondqueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_set_target_queue(firstQueue, serialQueue);
    dispatch_set_target_queue(secondQueue, serialQueue);
    
    dispatch_async(firstQueue, ^{
        NSLog(@"1");
    });
    dispatch_async(secondQueue, ^{
        NSLog(@"2");
    });
    dispatch_async(secondQueue, ^{
        NSLog(@"3");
    });
3.异步和同步

同步和异步的概念主要针对的是我们的线程。

  • 同步是指在当前线程下执行队列中的任务,并且同步任务执行完毕后才执行后面的任务。
  • 异步是指在子线程(非当前线程)下执行队列中的任务。执行任务的顺序与当前线程中的任务无关。
    在GCD中,异步执行使用的是dispatch_async,同步执行使用dispatch_sync
dispatch_sync同步执行
- (void)viewDidLoad
{
  [super viewDidLoad];
 
  dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
 
      NSLog(@"First Log");
 
  });
 
  NSLog(@"Second Log");
}
同步执行解释

下面是图中几个步骤的说明:

1.主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2.viewDidLoad 在主线程执行。
3.主线程目前在 viewDidLoad 内,正要到达 dispatch_sync 。
4.dispatch_sync Block 被添加到一个全局队列中,将在稍后执行。进程将在主线程挂起直到该 Block 完成。同时,全局队列并发处理任务;要记得 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
5.全局队列处理 dispatch_sync Block 加入之前已经出现在队列中的任务。
6.终于,轮到 dispatch_sync Block 。
7.这个 Block 完成,因此主线程上的任务可以恢复。
8.viewDidLoad 方法完成,主队列继续处理其他任务。

小结:dispatch_sync 添加任务到一个队列并等待直到任务完成。dispatch_async 做类似的事情,但不同之处是它不会等待任务的完成,而是立即继续“调用线程”的其它任务。

使用dispatch_async

- (void)viewDidLoad
{
  [super viewDidLoad];
 
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
 
      NSLog(@"First Log");
 
  });
 
  NSLog(@"Second Log");
}
异步执行解释

1.主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2.viewDidLoad 在主线程执行。
3.主线程目前在 viewDidLoad 内,正要到达 dispatch_async 。
4.dispatch_async Block 被添加到一个全局队列中,将在稍后执行。
5.viewDidLoad 在添加 dispatch_async 到全局队列后继续进行,主线程把注意力转向剩下的任务。同时,全局队列并发地处理它未完成地任务。记住 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
6.添加到 dispatch_async 的代码块开始执行。
7.dispatch_async Block 完成,两个 NSLog 语句将它们的输出放在控制台上。

小结:在这个特定的实例中,第二个 NSLog 语句执行,跟着是第一个 NSLog 语句。并不总是这样——着取决于给定时刻硬件正在做的事情,而且你无法控制或知晓哪个语句会先执行。“第一个” NSLog 在某些调用情况下会第一个执行。

三、GCD的使用

1.用 dispatch_async 处理后台任务

通过异步执行下载任务可以避免界面被一些耗时操作卡死,例如读取网络数据,大数据IO,还有大量数据的数据库读写,这时需要在另一个线程中处理,然后通知主线程更新界面。我们常常会经常这样使用GCD

//代码框架
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     // 耗时的操作
     dispatch_async(dispatch_get_main_queue(), ^{
          // 更新界面
     });
});

当我们需要下载一张图片,并显示到界面时,我们可以这样处理。防止界面因下载图片被卡死。

//下载图片的示例
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
     NSData * data = [[NSData alloc]initWithContentsOfURL:url];
     UIImage *image = [[UIImage alloc]initWithData:data];
     if (data != nil) {
          dispatch_async(dispatch_get_main_queue(), ^{
               self.imageView.image = image;
          });
     }
});

运行逻辑:

  • 创建一个并发队列并使用dispatch_async异步执行这个队列里面的任务
  • 在子线程中下载图片数据,生成对应的UIimage对象。
  • 异步切换到主线程队列,修改UI。
2.dispatch_after延后执行

dispatch_after只是延时提交block,不是延时立刻执行

NSLog(@"1");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"2");
});
NSLog(@"3");

打印顺序是1,3,2。dispatch_after并不会阻塞当前线程,只是延迟提交block,在当前线程继续执行。
其中的dispatch time参数的函数原型如下:

dispatch_time_t dispatch_time ( dispatch_time_t when, int64_t delta );

第一个参数为DISPATCH_TIME_NOW表示当前。第二个参数的delta表示纳秒,一秒对应的纳秒为1000000000,系统提供了一些宏来简化

 #define NSEC_PER_SEC 1000000000ull //每秒有多少纳秒
 #define USEC_PER_SEC 1000000ull    //每秒有多少毫秒
 #define NSEC_PER_USEC 1000ull      //每毫秒有多少纳秒
3.dispatch_once创建一个线程安全的单例

先说说不安全的创建情况

+ (instancetype)sharedManager {
    static SingleClass *manager = nil;
    if(!manager) {
        manager = [SingleClass new];
    }
    return manager;
}

当我们在不同线程多次调用改方法时,很有可能发生A线程进入if代码块后,系统切换上下文,B线程也进入代码块造成对象被初始化2次的问题。于是我们就需要保证单例的线程安全。

static SingleClass *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [SingleClass new];
    });
    return manager;

其实就是使用dispatch_once 取代if 条件判断,dispatch_once() 以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。

4.dispatch_apply进行快速迭代
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_apply(999, queue, ^(size_t i) {
        NSLog(@"%d",i);
    });
    NSLog(@"end");

输出结果1,2....998,end
说明这个方法是会阻塞当前线程的。并且可以循环执行并发操作。它的优点在于执行并发操作时GCD会进行线程管理。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
    if (explode) {
        //有问题的情况,可能会死锁
        for (int i = 0; i < 999 ; i++) {
            dispatch_async(concurrentQueue, ^{
                NSLog(@"wrong %d",i);
                //do something hard
            });
        }
    } else {
        //会优化很多,能够利用GCD管理
        dispatch_apply(999, concurrentQueue, ^(size_t i){
            NSLog(@"correct %zu",i);
            //do something hard
        });
    }
5.dispatch_barrier_async解决多线程对同一资源进行读写的冲突问题
__block NSString *str = @"1";
    dispatch_queue_t dataQueue = dispatch_queue_create("com.starming.gcddemo.dataqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(dataQueue, ^{
        NSLog(@"%@",str);
    });
    dispatch_async(dataQueue, ^{
        NSLog(@"%@",str);
    });
    //等待前面的都完成,在执行barrier后面的
    dispatch_barrier_async(dataQueue, ^{
        str = @"2";
        NSLog(@"%@",str);
    });
    dispatch_async(dataQueue, ^{
        NSLog(@"%@",str);
    });
    dispatch_async(dataQueue, ^{
        NSLog(@"%@",str);
    });

dispatch_barrier_async保证了同一队列中,先提交的任务执行完毕后才执行自己提交的block。并且提交的block执行完毕后,队列会恢复成以前的样子。

dispatch_barrier_async只在自己创建的队列上有这种作用,在全局并发队列和串行队列上,效果和dispatch_sync一样

6. dispatch_group线程组

dispatch groups是专门用来监视多个异步任务。dispatch_group_t实例用来追踪不同队列中的不同任务。
最常见的例子就是,多个异步并发任务执行完毕后再执行另外的操作。比如上传一组图片,或者下载多个文件。希望在全部完成时给用户一个提示。

当group里所有事件都完成GCD API有两种方式发送通知,

  • 第一种是dispatch_group_wait,会阻塞当前进程,等所有任务都完成或等待超时。
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    //在group中添加队列的block
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"1");
    });
    dispatch_group_async(group, concurrentQueue, ^{
        NSLog(@"2");
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"go on");
  • 第二种方法是使用dispatch_group_notify,异步执行闭包,不会阻塞。
dispatch_group_t serviceGroup = dispatch_group_create();
    
    // 开始第一个请求
    // 进入组
    dispatch_group_enter(serviceGroup);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:1.f];
        NSLog(@"1");
        dispatch_group_leave(serviceGroup);
    });
    
    // 开始第二个请求
    // 先进入组
    dispatch_group_enter(serviceGroup);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2");
        dispatch_group_leave(serviceGroup);
    });
    
    // 当小组里的任务都清空以后,通知主线程完成了所有任务
    dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
        // Assess any errors
        NSLog(@"finish");
    });
NSLog(@"go on");

此处会先打印go on。

7.使用信号量Dispatch Semaphore

dispatch_semaphore_t 类似信号量,可以用来控制访问某一资源访问数量。

  • dispatch_semaphore_create 初始化一个信号量,并设置它最大访问数量。
  • dispatch_semaphore_wait 向某个信号量对象发送等待信号,信号量-1,设置该次等待的时间,使用DISPATCH_TIME_FOREVER可以表示永久等待。
  • dispatch_semaphore_signal 向某个信号量发送释放信号,信号量+1。
    当信号量计数没有超过最大访问数量时,资源区可以被多个线程同时访问。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    __block NSString *strTest = @"test";
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"--%@--1-", strTest);
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"--%@--2-", strTest);
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"--%@--3-", strTest);
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"--%@--4-", strTest);
        dispatch_semaphore_signal(semaphore);
    });

打印结果:1,2,3,4.
解析:并发队列只能保证按提交顺序执行,本来是不能保证执行结果顺序的,但在这里我们使用了dispatch_semaphore_t限制了资源的访问,由于最大只允许1个资源访问,所以这段代码一定是按顺序执行的。

8.Dispatch Source监听进程事件

建议阅读iOS多线程——Dispatch Source
我试了一下里面的几个例子,对Dispatch Source有了一定的理解,但没有在开发中实际使用,等后面有实际经验再记录分享。

细说GCD(Grand Central Dispatch)如何用
iOS多线程GCD简介(一)
iOS多线程GCD简介(二)
GCD 深入理解:第一部分

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

推荐阅读更多精彩内容