RunLoop知识点总结

RunLoop是多线程的难点. 在实际开发中我们如何使用RunLoop呢? 且容我一一道来, 不当之处, 敬请斧正.

先浏览一下RunLoop知识点的大致框架, 这也是本文即将要说明的:
RunLoop知识点的大致框架

RunLoop的概念和作用

RunLoop被称为运行循环, 你可以把RunLoop理解为一个死循环, 看一下CFRunLoop的源码就知道了:

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

它的基本作用是:

  • 保持程序的持续运行(保证程序不退出)
  • 处理App中的各种事件(触摸事件, 定时器事件, Selector事件)
但这个死循环是一个很特殊的死循环, 它能够在该做事情的时候做事情, 没事情做的时候休息待命, 以节省CPU资源, 提高程序性能.

在应用程序的入口main函数中, 如下所示

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
有一个UIApplicationMain(,,,)函数, 这个函数内部就启动了一个RunLoop运行循环

UIApplicationMain(,,,)函数是有一个int类型的返回值的, 但是一直不会返回,保持程序的持续运行, 这个默认启动的RunLoop运行循环是跟主线程相关联的.

获取RunLoop对象

苹果官方文档告诉我们:

Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop

  • Cocoa框架: NSRunLoop

  • Core Foundation框架:CFRunLoopRef
    NSRunLoop的底层其实还是调用的CFRunLoopRef的API, 是对其进行了OC层面上的封装

线程和RunLoop的关系
  • 一条线程对应一个RunLoop,是一一对应的关系
  • 主线程的RunLoop是默认存在的,已经创建好了, 子线程的RunLoop需要手动创建
  • RunLoop的生命周期: 在第一次获取得时候创建, 线程结束时销毁
获取RunLoop对象
  • Cocoa框架: [NSRunLoop currentRunLoop](获得当前线程的RunLoop对象)
    [NSRunLoop mainRunLoop](获得主线程的RunLoop对象)
  • Core Foundation框架:CFRunLoopGetCurrent()(获得当前线程的RunLoop对象)
    CFRunLoopGetMain()(获得主线程的RunLoop对象)
我们来看一下CFRunLoop的源码:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

上面的代码告诉我们: 线程和RunLoop是一一对应的关系, 当线程是主线程时,会创建一个mainRunLoop,然后保存在字典中:

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

当线程是子线程时, 会先看一下当前线程为key的对应的Value中有没有RunLoop,如果没有, 会先创建一个RunLoop, 然后再将其保存在字典中:

    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }

RunLoop的相关类

Core Foundation中关于RunLoop的关键类
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef
这5个类之间的关系可以用下图来表示:
RunLoop的相关类之间的关系

这个关系图其实就是说: 在一个RunLoop中可以存在两个或者两个以上的模式,但RunLoop每次只能选择一个模式运行, 就像空调有制冷和制热模式, 但是运行的时候只能选择制冷或者制热模式,** 要保证运行循环RunLoop不退出, 每个模式里面至少存在一个Source或者 一个Timer**,Observer可以有也可以没有, 只是监听RunLoop的运行状态.

RunLoop运行模式(一共有5种)
  • Default
    NSDefaultRunLoopMode (Cocoa)
    kCFRunLoopDefaultMode (Core Foundation)
  • Event tracking
    NSEventTrackingRunLoopMode (Cocoa)
  • Common modes
    NSRunLoopCommonModes (Cocoa)
    kCFRunLoopCommonModes (Core Foundation)
  • Connection
    NSConnectionReplyMode (Cocoa)
  • Modal
    NSModalPanelRunLoopMode (Cocoa)

后面两种运行模式就不做过多解读了, 我们需要关注的是前面三种模式

  • 默认模式
- (void)timer1 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"run1----------%@",[NSRunLoop currentRunLoop].currentMode);
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

当定时器以默认模式被添加到运行循环中时, 如果发生例如scrollView拖拽事件等, 运行循环将有默认模式自动切换到事件追踪模式, 这时候, 默认模式中的定时器就暂时不工作了, 当拖拽事件结束之后, 运行循环中的运行模式又由追踪模式切换到默认模式.

  • 事件追踪模式
- (void)timer2 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"run2----------%@",[NSRunLoop currentRunLoop].currentMode);
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
}

跟上面的模式刚好相反, 如果发生例如scrollView拖拽事件等, 事件追踪模式中的定时器就开始工作, 当拖拽事件结束之后, 运行循环中的运行模式又由事件追踪模式切换到默认模式, 那么在事件追踪模式中的定时器就不工作了.

**那么问题来了, 如果需要定时器不管添加到哪种模式下都正常工作应该怎么做呢? 有一种方法是, 同时把定时器添加到两种运行模式中, 不过这种方法太笨了, 正确的做法是: **

  • 通用模式
- (void)timer3 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"run3----------%@",[NSRunLoop currentRunLoop].currentMode);
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

这种模式其实不是真正的一种运行模式, 而是将两种模式都打上CommonModes的标签,那么就相当于timer可以在只要是带有CommonModes标签的运行模式下工作.

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block类似于这种scheduledTimer开头的方法, 是自动将定时器添加到默认的运行循环模式了, 如果要添加到事件追踪模式和通用模式, 还是需要手动添加.

**如果我在子线程创建了定时器, 那么需要创建子线程中的runLoop对象, 然后把timer添加到子线程的runLoop中: **

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [NSThread detachNewThreadWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"run2----------%@",[NSRunLoop currentRunLoop].currentMode);
        }];
        
        NSLog(@"%@",[NSThread currentThread]);
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [currentRunLoop addTimer:timer forMode:NSRunLoopCommonModes];
        
        [currentRunLoop run];
    }];
}

需要记住的是:

  • 创建子线程的RunLoop直接调用[NSRunLoop currentRunLoop];, 这个方法是懒加载的
  • 一定要让子线程的runLoop跑起来, 不然的话, 子线程一结束, 运行循环立马销毁, 即时添加了定时器也没个X用. [currentRunLoop run];这句代码不能少!

GCD定时器

插播另外一条知识点, 我们在使用timer的时候, 除了NSTimer, 还有其他的选择吗? 答案是有, GCD定时器就是一个很好的选择, xcode甚至为我们提供了默认的定时器代码块, 方便你的使用:

- (void)gcdTimer {
    /* 
     参数1 : 需要创建的源的种类, timer 也是一种数据源
     参数2,参数3: 在你创建timer的时候直接传0就可以了
     参数4: timer触发的代码运行的队列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    /* 
     参数1 : timer定时器
     参数2 : 从什么时间开始触发定时器, DISPATCH_TIME_NOW代表现在
     参数3 : 时间间隔
     参数4 : 表示精度, 传0表示绝对精准
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    /* 
     封装timer需要触发的操作
     */
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCDTimer-----%@",[NSThread currentThread]);
        NSLog(@"%@",[NSRunLoop currentRunLoop].currentMode);
    });
    dispatch_resume(timer);
    
    /* 
     用强指针引用, 防止timer释放
     */
    self.timer = timer;
}

RunLoop输入源

CFRunLoopSourceRef是输入源, 输入源的种类有两种分类方法:

  • 以前的分法:
    1.Port-Based Sources
    2.Custom Input Sources
    3.Cocoa Perform Selector Sources

  • 现在的分法:
    1.Source0:非基于端口的
    2.Source1:基于端口的

那么根据现在的分法, 什么是基于端口的呢? 就是系统默认的, 非基于端口的就是用户主动触发的事件, 比如用户点击了一个按钮等. 假设现在用户点击了一个按钮, 我们来观察函数调用栈的情况:

点击按钮时的函数调用栈

我们可以看出, 点击按钮时, 运行循环RunLoop处理的输入源是source0

RunLoop Observer

CFRunLoopObserverRef是观察者, 是用来监听RunLoop状态改变的

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 即将进入运行循环
kCFRunLoopBeforeTimers = (1UL << 1), 即将处理定时器事件
kCFRunLoopBeforeSources = (1UL << 2), 即将处理输入源事件
kCFRunLoopBeforeWaiting = (1UL << 5), 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), 退出运行循环
kCFRunLoopAllActivities = 0x0FFFFFFFU 运行循环所有活动

     };

**添加观察者到运行循环的代码: **

- (void)observer {
    
    /* 
     参数1 :分配内存空间的方式, 传默认
     参数2 :RunLoop的运行状态
     参数3 :是否持续观察
     参数4 :优先级, 传0
     参数5 :观察者观测到状态改变时触发的方法
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        /* 
         typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),
         kCFRunLoopBeforeTimers = (1UL << 1),
         kCFRunLoopBeforeSources = (1UL << 2),
         kCFRunLoopBeforeWaiting = (1UL << 5),
         kCFRunLoopAfterWaiting = (1UL << 6),
         kCFRunLoopExit = (1UL << 7),
         kCFRunLoopAllActivities = 0x0FFFFFFFU
         };
         */
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即将被唤醒");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理定时器事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理输入源事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"休眠结束");
                break;
            case kCFRunLoopExit:
                NSLog(@"运行循环退出");
                break;
            default:
                break;
        }
    });
    /* 
     参数1 :运行循环, 传入当前的运行循环
     参数2 :观察者, 观察运行循环的各种状态
     参数3 :运行循环的模式
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(),observer, kCFRunLoopDefaultMode);
}

重要的函数就两个

  • CFRunLoopObserverCreateWithHandler(_,_,_,_,_)创建观察者(包括block回调)

  • CFRunLoopAddObserver(_,_,_)添加观察者到运行循环

RunLoop运行流程

我们先看一下苹果官方文档的示意图:
RunLoop运行流程

从图中可以看出的是: 当线程中的运行循环开启之后, 将循环处理两大source事件:

  • Input Sources
  1. 端口事件
    2.用户输入源事件
    3.performSelector事件
  • Timer Sources
    定时器事件

然后运行循环就循环处理这些事件, 如果没有, 就休眠并等待被唤醒

详细的官方文档步骤见下图(感谢网友提供):

RunLoop运行流程

RunLoop的应用:常驻线程

开启常驻线程的作用: 让一个子线程不被销毁, 等待其他线程发来消息, 处理事件

#import "ViewController.h"

@interface ViewController ()


/**
 thread
 */
@property (nonatomic,strong)NSThread *thread;

@end

@implementation ViewController


- (IBAction)creatNewThread:(UIButton *)sender {
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil];
    [self.thread start];
}

- (IBAction)stableThread:(UIButton *)sender {

     /* 在子线程中实现方法 */
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
    
    
}

- (void)task1 {
    
    NSLog(@"创建了一个新的线程----%@",[NSThread currentThread]);
    
    
    /* 获取当前线程 */
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
     /* 添加端口, 保证运行循环不退出 */
    [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    [runLoop run];
    
    
}

- (void)task2 {
    NSLog(@"%s",__func__);
}

@end

最重要的就是添加端口到当前runLoop中,因为当前的runLoop就是子线程的runLoop, 添加端口的目的就是保证运行循环不退出, 当然你也可以添加timer到运行循环中, 但timer本身起不到任何用, 反而浪费资源

如上图所示, 我先创建了一个子线程, 在子线程里我创建了子线程的runLoop,然后在子线程中添加端口保证其不退出, 然后调用run方法让runLoop跑起来, 这样, 我不管在程序运行的任何阶段在这个子线程中调用方法, 该子线程都可以响应, 这就是常驻线程.

以上就是RunLoop的相关知识点, 再补充一个小的知识点, 即自动释放池的创建和销毁

  • 在启动RunLoop的时候, 会第一次创建自动释放池
  • RunLoop退出的时候, 会销毁自动释放池
  • 其他创建和销毁自动释放池的时机:
    • 当RunLoop即将进入睡眠的时候, 说明之前的事件都已经全部处理完毕, 那么之前的变量就没有必要再留着, 这时候会销毁自动释放池
    • 同时, 再创建一个新的自动释放池

NSURLConnection与RunLoop

  • 当我们创建一个网络连接, 并且设置代理的时候, 我们通常是这样做的:

[NSURLConnection connectionWithRequest:request delegate:self];

此时,代理回调会在子线程执行
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

比如上面这个代理方法, 是会在主线程执行的, 如果我们需要设置在子线程执行, 我们还需要写这么一句代码:

[connection setDelegateQueue:[[NSOperationQueue alloc] init]];
需要注意的是设置代理回调线程, 必须是子线程,不能设置[NSOperationQueue mainQueue], 因为默认就是在主线程调用的, 如此设置了之后, 代理方法反而不走

  • 但如果我们用另外一种方式创建网络连接, 情况又会如何呢?
 NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];

最后startImmediately里面的这个参数如果填YES的话, 跟前面那种[NSURLConnection connectionWithRequest:request delegate:self];创建没有任何区别

如果填写NO的话, 则网络连接不会被加入到运行循环RunLoop中,不被加到RunLoop中, connection立马就被释放掉了, 代理当然不会调用, 此时, 我们使用

[connection start];
这个方法, start内部会帮我们开启当前线程的runLoop, 并且帮我们把网络连接加入到这个runLoop中去.

  • 如果我们在子线程中用第一种方式创建网络连接又如何呢?
- (void)newThreadDelegate1 {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/login?username=123&pwd=123&type=JSON"]];
        NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"这里的线程是:%@",[NSThread currentThread]);
        
    });
}

我们知道, 主线程的runLoop默认是开启的, 子线程的runLoop需要手动创建, 创建了之后还要runLoop跑起来. 这样网络连接才能被加入到运行循环中, 代理回调才可以执行, 此时代理回调的执行线程默认和网络连接的线程是同一个线程, 如果, 网络请求重新重新分配代理回调执行的线程,[connection setDelegateQueue:[[NSOperationQueue alloc] init]]; 那么执行线程就不同了.

  • 如果我们在子线程中用第二种方式去创建网络连接的话
    • 也分两种情况, 当startImmediately的参数是YES, 那么跟上面那种情况没什么区别
    • 如果参数为NO, 则connection需要调用start方法, 这个方法会自动创建当前线程的运行循环runLoop, 并将网络连接添加到该运行循环runLoop中去.
关于RunLoop的更多知识, 请查看苹果的相关文档

RunLoop的相关文档

苹果官方文档
CFRunLoop源码

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

推荐阅读更多精彩内容