老司机出品————多线程实践

多线程实践

有段时间没写博客了,不过这也不是一次两次了。

厚颜无耻

嗯,就不找理由也不检讨了,直奔主题吧。

在今天的博客中你将会看到:

  • 异步线程同步
  • NSOperation子类重写
  • 条件模块
  • 请求类封装

异步线程同步

老司机今天讲的不是多线程的基本用法,这个东西往上的博客其实蛮多的,而且也基本是多线程的基本用法。老司机今天主要的是介绍多个异步线程执行结束后进行回调的解决方案,如果说这么说不太清楚的话,最常见的场景就是多个网络请求都结束后触发列表刷新。

其实这个需求呢,还是挺常见的。主要呢,目前有两种解决思路,一种呢是GCD中的dispatch_group,一种是NSOperation

dispatch_group

这个方案呢,实现起来还比较简单,先放一下代码吧。

-(void)testGCDGroup {
    dispatch_group_t g = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"Will enter task1");
    dispatch_group_enter(g);
    dispatch_group_async(g, q, ^{
        [self task1];
        dispatch_group_leave(g);
    });
    NSLog(@"Will enter task2");
    dispatch_group_enter(g);
    dispatch_group_async(g, q, ^{
        [self task2];
        dispatch_group_leave(g);
    });
    
    NSLog(@"Come to notify");
    dispatch_group_notify(g, q, ^{
        NSLog(@"Enter notify");
        [self taskComplete];
    });
    NSLog(@"Pass notify");
}

-(void)task1 {
    NSLog(@"Enter sleep 10.");
    [NSThread sleepForTimeInterval:10];
    NSLog(@"Leave sleep 10.");
}

-(void)task2 {
    NSLog(@"Enter sleep 5.");
    [NSThread sleepForTimeInterval:5];
    NSLog(@"Leave sleep 5.");
}

-(void)taskComplete {
    NSLog(@"All task finished.");
}

控制台输出是这个样子的:

2018-03-26 14:28:02.317556+0800 test[3446:287435] Will enter task1.
2018-03-26 14:28:02.317714+0800 test[3446:287435] Will enter task2.
2018-03-26 14:28:02.317733+0800 test[3446:287484] Enter sleep 10.
2018-03-26 14:28:02.317847+0800 test[3446:287435] Come to notify.
2018-03-26 14:28:02.317865+0800 test[3446:287486] Enter sleep 5.
2018-03-26 14:28:02.318093+0800 test[3446:287435] Pass notify.
2018-03-26 14:28:07.318474+0800 test[3446:287486] Leave sleep 5.
2018-03-26 14:28:12.321389+0800 test[3446:287484] Leave sleep 10.
2018-03-26 14:28:12.321740+0800 test[3446:287484] Enter notify.
2018-03-26 14:28:12.321932+0800 test[3446:287484] All task finished.

他呢,基本流程就是当调用的dispatch_group_leave()与dispatch_group_enter()相等时,就会调用dispatch_group_notify()中的回调。不过这种实现方案呢,还是有一个需要注意的点就是dispatch_group_enter()与dispatch_group_leave()要成对使用,否则就会进入无限的等待状态

第二个解决方案就是使用NSOperation。呐,我会放在第二节着重介绍一下的。


NSOperation子类重写

我们知道,NSOperation是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。他在使用上更加符合面向对象的思想,更加方便的为任务添加依赖关系,同时提供了四个支持KVO监听的代表当前任务执行状态的属性cancelled、executing、finished、ready。NSOperation内部对这四个状态行为作了预处理,根据任务的不同状态这四个属性的值会自动改变。当NSOperation配合NSOperationQueue使用时,Queue会监听所有Operation的状态从而分配任务的启动时机。总之,NSOperation隐藏了很多内部细节,让我们开发者无需关心任务的各种状态。

系统行为

首先,为了模仿系统行为,我们先观察下系统的NSOperation的cancelled、executing、finished、ready四个属性的状态变化情况。那我们去监听一下NSOperation的四个属性。代码如下:

TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp1");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp1");
}];
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
[self logOp:bp1 keyPathes:keyPathes];  
[self addObserverForOp:bp1 keyPathes:keyPathes];
[bp1 start];
[bp1 cancel];

控制台输出:
2018-04-18 11:45:01.277354+0800 OperationDemo[72212:1655503] bp1 isReady = true
2018-04-18 11:45:01.277539+0800 OperationDemo[72212:1655503] bp1 isCancelled = false
2018-04-18 11:45:01.278212+0800 OperationDemo[72212:1655503] bp1 isExecuting = false
2018-04-18 11:45:01.278449+0800 OperationDemo[72212:1655503] bp1 isFinished = false
2018-04-18 11:45:01.278682+0800 OperationDemo[72212:1655503] bp1 before start
2018-04-18 11:45:01.278954+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 11:45:01.279063+0800 OperationDemo[72212:1655503] bp1 before main
2018-04-18 11:45:01.279245+0800 OperationDemo[72212:1655503] enter bp1
2018-04-18 11:45:04.279669+0800 OperationDemo[72212:1655503] leave bp1
2018-04-18 11:45:04.280074+0800 OperationDemo[72212:1655503] bp1 after main
2018-04-18 11:45:04.281164+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 11:45:04.281404+0800 OperationDemo[72212:1655503] bp1---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 11:45:04.281557+0800 OperationDemo[72212:1655503] bp1 after start
2018-04-18 11:45:04.281782+0800 OperationDemo[72212:1655503] bp1 before cancel
2018-04-18 11:45:04.281917+0800 OperationDemo[72212:1655503] bp1 after cancel

上述代码中,我们监听了四个属性并执行了Operation。根据日志我们可以总结如下:

初始状态下,ready为YES,其他均为NO

当我们调用 -start 后,执行 -main 之前 isExecuting 属性从NO被置为YES

调用 -main 之后开始执行提交到Operation中的任务

任务完成后 isExecuting 属性从YES被置为NO,isFinished 属性从NO被置为YES

我们再看一下如果在执行 -start 之前先执行 -cancel 后会是什么状态:

TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp2");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp2");
}];
[self addObserverForObj:bp2 keyPathes:keyPathes];
self.bp2 = bp2;
[bp2 cancel];
[bp2 start];

控制台输出:
2018-04-18 11:44:03.597414+0800 OperationDemo[72184:1653790] bp2 before cancel
2018-04-18 11:44:03.597684+0800 OperationDemo[72184:1653790] bp2---isCancelled---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 11:44:03.597881+0800 OperationDemo[72184:1653790] bp2---isReady---{
    kind = 1;
    new = 1;
    old = 1;
}
2018-04-18 11:44:03.598051+0800 OperationDemo[72184:1653790] bp2 after cancel
2018-04-18 11:44:03.598138+0800 OperationDemo[72184:1653790] bp2 before start
2018-04-18 11:44:03.598279+0800 OperationDemo[72184:1653790] bp2---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 11:44:03.598393+0800 OperationDemo[72184:1653790] bp2 after start

在执行 -start 之前调用 -cancel 后,isCancelled 属性从NO被置为YES,isReady 属性无论什么状态都会被置为YES。这里后面讲到dependency的时候会说明。

-cancel 之后再调用 -start ,会将 isFinished 属性从NO被置为YES,然后并不调用 -main 方法。

单个Operation的行为我们已经基本了解,那么接下来我们来看一下当两个Operation添加到Queue中会是什么结果。

TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp1");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp1");
}];
bp1.name = @"bp1";
bp1.completionBlock = ^{
        NSLog(@"bp1 complete");
};
    
TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp2");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp2");
}];
bp2.name = @"bp2";
bp2.completionBlock = ^{
        NSLog(@"bp2 complete");
};
    
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
    
[self addObserverForOp:bp1 keyPathes:keyPathes];
[self addObserverForOp:bp2 keyPathes:keyPathes];
    
NSOperationQueue * q = [NSOperationQueue new];
[bp1 addDependency:bp2];
[q addOperation:bp1];
[q addOperation:bp2];
    
控制台输出:
2018-04-18 16:37:16.004963+0800 OperationDemo[84411:1940169] bp1 before addDependency:
2018-04-18 16:37:16.005291+0800 OperationDemo[84411:1940169] bp1---isReady---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 16:37:16.005640+0800 OperationDemo[84411:1940169] bp1 after addDependency:
2018-04-18 16:37:16.005842+0800 OperationDemo[84411:1940219] bp2 before start
2018-04-18 16:37:16.006277+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:37:16.007394+0800 OperationDemo[84411:1940219] bp2 before main
2018-04-18 16:37:16.007669+0800 OperationDemo[84411:1940219] enter bp2
2018-04-18 16:37:19.010134+0800 OperationDemo[84411:1940219] leave bp2
2018-04-18 16:37:19.010351+0800 OperationDemo[84411:1940219] bp2 after main
2018-04-18 16:37:19.010701+0800 OperationDemo[84411:1940218] bp1 before start
2018-04-18 16:37:19.010707+0800 OperationDemo[84411:1940219] bp1---isReady---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:37:19.010857+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:37:19.011126+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 16:37:19.011134+0800 OperationDemo[84411:1940218] bp1 before main
2018-04-18 16:37:19.011143+0800 OperationDemo[84411:1940220] bp2 complete
2018-04-18 16:37:19.011229+0800 OperationDemo[84411:1940218] enter bp1
2018-04-18 16:37:19.011233+0800 OperationDemo[84411:1940219] bp2---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:37:19.011458+0800 OperationDemo[84411:1940219] bp2 after start
2018-04-18 16:37:22.011382+0800 OperationDemo[84411:1940218] leave bp1
2018-04-18 16:37:22.011571+0800 OperationDemo[84411:1940218] bp1 after main
2018-04-18 16:37:22.012029+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 16:37:22.012050+0800 OperationDemo[84411:1940219] bp1 complete
2018-04-18 16:37:22.012375+0800 OperationDemo[84411:1940218] bp1---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:37:22.013382+0800 OperationDemo[84411:1940218] bp1 after start

当为bp1添加bp2作为依赖以后,bp1的 isReady 属性从YES置为NO。

由于bp2是bp1的依赖,所以优先执行bp2。

在bp2中任务完成之后,-main 方法调用结束之后, -start 方法调用结束之前,bp1调用 -start 并将 isReady 属性置为YES。

其他行为与单个调用时基本一致。

我们再来看看当bp1添加bp2作为依赖,并且在调用之前bp2调用 -cancel 时的状态变化,代码基本一致,唯一变化是在添加在q之前bp2调用 -cancel,我就不放代码了,直接看日志输出:

2018-04-18 16:39:38.612072+0800 OperationDemo[84462:1944038] bp1 before addDependency:
2018-04-18 16:39:38.612500+0800 OperationDemo[84462:1944038] bp1---isReady---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 16:39:38.612712+0800 OperationDemo[84462:1944038] bp1 after addDependency:
2018-04-18 16:39:38.613460+0800 OperationDemo[84462:1944038] bp2 before cancel
2018-04-18 16:39:38.613984+0800 OperationDemo[84462:1944038] bp2---isCancelled---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:39:38.614337+0800 OperationDemo[84462:1944038] bp2---isReady---{
    kind = 1;
    new = 1;
    old = 1;
}
2018-04-18 16:39:38.614512+0800 OperationDemo[84462:1944038] bp2 after cancel
2018-04-18 16:39:38.614804+0800 OperationDemo[84462:1944152] bp2 before start
2018-04-18 16:39:38.615286+0800 OperationDemo[84462:1944158] bp1 before start
2018-04-18 16:39:38.615321+0800 OperationDemo[84462:1944152] bp1---isReady---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:39:38.615614+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:39:38.615629+0800 OperationDemo[84462:1944150] bp2 complete
2018-04-18 16:39:38.615661+0800 OperationDemo[84462:1944152] bp2---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:39:38.616030+0800 OperationDemo[84462:1944158] bp1 before main
2018-04-18 16:39:38.616115+0800 OperationDemo[84462:1944152] bp2 after start
2018-04-18 16:39:38.616132+0800 OperationDemo[84462:1944158] enter bp1
2018-04-18 16:39:41.618815+0800 OperationDemo[84462:1944158] leave bp1
2018-04-18 16:39:41.619170+0800 OperationDemo[84462:1944158] bp1 after main
2018-04-18 16:39:41.619551+0800 OperationDemo[84462:1944152] bp1 complete
2018-04-18 16:39:41.619591+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
    kind = 1;
    new = 0;
    old = 1;
}
2018-04-18 16:39:41.619941+0800 OperationDemo[84462:1944158] bp1---isFinished---{
    kind = 1;
    new = 1;
    old = 0;
}
2018-04-18 16:39:41.620073+0800 OperationDemo[84462:1944158] bp1 after start

与单个Operation调用 -cancel 行为一致,不影响 -start 的调用,同样不会调用 -main,不同点是在bp1调用 -start 之前 isReady 属性会被置为YES,之后行为与单个Operation调用 -start 一致。

上述行为可以用一张流程图来表现:


Operation流程

重写子类

通过观察上述的日志我们可以看出,当一个任务作为另一个任务的依赖时,只有当被依赖的任务完成后,才会执行另一个任务,而这个时间点的时候,executing、finished两个属性会发生变化。那我们需要做的就是实现一个NSOperation的子类,让他可以再我们需要的时候才被标记为完成状态,这样,我们只要给刷新列表任务添加网络请求任务作为依赖即可。所以,我们需要做的只有两件事,就是接过executing、finished两个属性的管理权以及在我们需要的时候改变他们的状态。

需求知道了,实现就很简单了。老司机直接就放一份简单的源码就好了。

@class DWManualOperation;
typedef void(^OperationHandler)(DWManualOperation * op);
@interface DWManualOperation : NSOperation

/**
 以需要实现的任务生成operation对象

 @param handler 需要实现的任务
 @return operation实例
 
 */
+(instancetype)manualOperationWithHandler:(OperationHandler)handler;

/**
 立刻将当前任务标识为完成状态,isExecuting 为 NO,isFinished 为 YES。
 */
-(void)finishOperation;

@end

@interface DWManualOperation ()

@property (nonatomic ,assign ,getter=isFinished) BOOL finished;

@property (nonatomic ,assign ,getter=isExecuting) BOOL executing;

@property (nonatomic ,copy) OperationHandler handler;

@property (nonatomic ,strong) DWManualOperation * cycleSelf;

@end

@implementation DWManualOperation
@synthesize finished = _finished;
@synthesize executing = _executing;

#pragma mark --- interface method ---
+(instancetype)manualOperationWithHandler:(OperationHandler)handler {
    DWManualOperation * op = [DWManualOperation new];
    if (handler) {
        op.handler = handler;
    }
    return op;
}

-(void)finishOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    _finished = YES;
    _executing = NO;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

#pragma mark --- override ---
-(instancetype)init {
    if (self = [super init]) {
        _concurrentHandler = YES;
        self.completionBlock = nil;
    }
    return self;
}

-(void)start {
    NSLog(@"start");
    ///如果是被取消状态则置为完成状态并返回,为了配合NSOperationQueue使用
    if (self.isCancelled) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    if (self.isExecuting || self.isFinished) {///正在执行或已经完成的任务不可以调用开始方法。
        return;
    }
    self.cycleSelf = self;
    [super start];
}

-(void)cancel {
    [super cancel];
}

-(void)main {///系统实现中 -start 方法中会调用 -main 方法
    [self willChangeValueForKey:@"isExecuting"];
    _executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
    [super main];
    __weak typeof(self)weakSelf = self;
    if (self.handler) {
        self.handler(weakSelf);
    }
}

-(void)dealloc {
    NSLog(@"dealloc");
}

#pragma mark --- tool func ---
static inline void freeOperation(DWManualOperation * op) {
    op.cycleSelf = nil;
}

#pragma mark --- setter/getter ---

-(void)setCompletionBlock:(void (^)(void))completionBlock {
    __weak typeof(self)weakSelf = self;
    dispatch_block_t ab = ^(void) {
        if (completionBlock) {
            completionBlock();
        }
        freeOperation(weakSelf);
    };
    [super setCompletionBlock:ab];
}

@end

呐,写到这里,我们就基本实现了一个跟系统Operation具有相同行为,但是我们可以随意控制Operation是否完成的子类了。


条件模块

不知道该叫什么我就随便起了个名,其实就是一个应用,场景就是操作A一定要建立在某种状态下才能执行。最简单的就是比如点赞功能必须是登录后才可进行,那么我们就要对这种状态做出判断。如下图:

条件模块

你可能说这无非就是一个判断的事,的确是,不过像登录状态这种很多地方都要用的功能这样写也能很好的复用。这个思路能主要还是借鉴的大神Delpan的这篇博客:《操作抽象设计-实践》,写的很好,同学们感兴趣可以去看看。

Demo传送门


请求类封装

呐,写到这里其实就只是讲思路了,至此我们已经具有了一个可以控制完成时机的Operation了,只要我们将网络请求与Operation同时 -start 后,请求回调结束后标志Operation为完成状态后就可以为请求添加依赖了,同时也可以配合系统的其他Operation和Queue同时使用完成线程间通信。

说到这要是就结束了那就太虎头蛇尾了,而且真爱粉们应该知道,一般到这个时候就是老司机的软广环节了,着急的童鞋们可以关掉浏览器了哈~

2.jpg

老司机给予这个思路对AFN进行了二次封装,写了一个自用的请求框架DWFlashFlow

首先它具有NSOperation的所有特性,可以跟普通Operation结合在一起使用,其次我还封装了批量请求和请求量功能,并且在功能层和逻辑层上进行了分离,也就是说你可以自由更换你的请求核心类,而逻辑层不变~哎,最近都不会吹牛逼了,剩下的东西喜欢的同学自己看吧~

传送门:DWFlashFlow

喜欢的童靴给点个小星星呗~

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

推荐阅读更多精彩内容