BlastBufferQueue 原理解读

一、重点API功能介绍

Google针对新的同步机制,在BBQ对象JAVA层面设计了一系列功能接口,列举功能更新较大几个接口:

  1. setNextTransaction(@Nullable SurfaceControl.Transaction t)

提供用于下一次缓冲区要更新的事务。 BBQ 不会立即提交此事务,通过该接口将下一帧的提交控制在调用者手中,调用者可以将其用于更高级别的同步。

  1. mergeWithNextTransaction(SurfaceControl.Transaction t, long frameNumber)

将传入的事务合并到 BBQ 中的下一个事务。 当具有指定帧号的下一帧可用时,将直接与含有Buffer的事务进行合并并提交。

  1. setTransactionCompleteCallback(long frameNumber @Nullable TransactionCompleteCallback completeCallback)

客户端可以监听Buffer的合成状态,在 SurfaceFlinger 中已应用包含带有 framenumber 的缓冲区的事务时触发回调,通知客户端合成完成。

与之前Android版本不同的是,Surface对象的创建、Buffer size与Surface size的更新也支持直接通过BBQ进行操作。

二、BBQ初始化

Android 12 Google将BufferQueue(简称BQ)组件从SF端移动到了客户端,BQ组件的初始化也放在BBQ的初始化中。通过类名可以看出BBQ更像是BQ的装饰者,在BQ本来功能特性的基础上添加了同步的功能。

BBQ 模型

通过官图大概了解,整个生产消费模型都在客户端,图形缓冲区的出队、入队、获取等操作都在客户端完成,预示着生产着模型从远程通讯变成了本地通讯, 消费者监听器也从SF端的ContentsChangedListener。带来的改变就是客户端需要通过事务Transaction来向SF端提交Buffer与图层的属性。

接下来以应用显示流程为例,梳理下BBQ的初始化流程:

流程的梳理过程所贴的代码,为了突出重点会删除部分逻辑。

  1. 创建BBQ对象

应用端通过方法relayoutWindow向WMS服务申请窗口布局,创建应用对应SurfaceControl,随后根据SurfaceControl创建BlastBufferQueue:

frameworks/base/core/java/android/view/ViewRootImpl.java

1958    Surface getOrCreateBLASTSurface() {
1959        if (!mSurfaceControl.isValid()) {
1960            return null;
1961        }
1962
1963        Surface ret = null;
1964        if (mBlastBufferQueue == null) {
                //第一次获取会创建BBQ
1965            mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
1966                mSurfaceSize.x, mSurfaceSize.y,
1967                mWindowAttributes.format);
1968            // 通过BBQ创建Surface对象
1970            ret = mBlastBufferQueue.createSurface();
1971        } else {
                //BBQ已创建,则更新Buffer与Layer的几何属性
1972            mBlastBufferQueue.update(mSurfaceControl,
1973                mSurfaceSize.x, mSurfaceSize.y,
1974                mWindowAttributes.format);
1975        }
1976
1977        return ret;
1978    }

  1. 通过JNI方法创建对应的Native对象

BBQ主要核心逻辑的初始化都放在了Native对象的构造函数,做了以下几件事:

  1. 创建图形缓冲区生产消费模型;
  2. 连接本地的图形缓冲区消费者;
  3. 设置图形缓冲区监听器。

frameworks/native/libs/gui/BLASTBufferQueue.cpp

133BLASTBufferQueue::BLASTBufferQueue(const std::string& name, const sp<SurfaceControl>& surface,
134                                   int width, int height, int32_t format)
135      : mSurfaceControl(surface),
136        mSize(width, height),
137        mRequestedSize(mSize),
138        mFormat(format),
139        mNextTransaction(nullptr) {
       // 创建图形缓冲区生产消费模型
140    createBufferQueue(&mProducer, &mConsumer);
141    // 由于适配器在客户端进程中,显式设置 dequeue 超时,以便 dequeueBuffer 流程阻塞
143    mProducer->setDequeueTimeout(std::numeric_limits<int64_t>::max());
145    // 默认设置最大dequeue buffer 数量
146    mProducer->setMaxDequeuedBufferCount(2);
       // 初始化图形缓冲区消费者
147    mBufferItemConsumer = new BLASTBufferItemConsumer(mConsumer,
148                                                      GraphicBuffer::USAGE_HW_COMPOSER |
149                                                              GraphicBuffer::USAGE_HW_TEXTURE,
150                                                      1, false);
       //设置消费者标识
156    mBufferItemConsumer->setName(String8(consumerName.c_str()));
       //添加图形缓冲区入队监听
157    mBufferItemConsumer->setFrameAvailableListener(this);
       //添加图形缓冲区消费者释放监听
158    mBufferItemConsumer->setBufferFreedListener(this);
       //设置缓冲区Size
159    mBufferItemConsumer->setDefaultBufferSize(mSize.width, mSize.height);
160    mBufferItemConsumer->setDefaultBufferFormat(convertBufferFormat(format));
161    mBufferItemConsumer->setBlastBufferQueue(this);
162    //通过SF端计算并获取最大可消费缓冲区数量
163    ComposerService::getComposerService()->getMaxAcquiredBufferCount(&mMaxAcquiredBuffers);
164    mBufferItemConsumer->setMaxAcquiredBufferCount(mMaxAcquiredBuffers);
165
166    mTransformHint = mSurfaceControl->getTransformHint();
167    mBufferItemConsumer->setTransformHint(mTransformHint);
168    SurfaceComposerClient::Transaction()
169            .setFlags(surface, layer_state_t::eEnableBackpressure,
170                      layer_state_t::eEnableBackpressure)
171            .setApplyToken(mApplyToken)
172            .apply();
173    mNumAcquired = 0;
174    mNumFrameAvailable = 0;
177}

  1. 创建图形缓冲区生产消费模型

frameworks/native/libs/gui/BLASTBufferQueue.cpp

797void BLASTBufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
798                                         sp<IGraphicBufferConsumer>* outConsumer) {
       //创建BufferQueue核心类,主要负责缓冲区的调度工作
802    sp<BufferQueueCore> core(new BufferQueueCore());
       //创建生产者模型,等待图形生产者连接使用
805    sp<IGraphicBufferProducer> producer(new BBQBufferQueueProducer(core));
       //创建消费者模型,等待图形消费者连接使用
809    sp<BufferQueueConsumer> consumer(new BufferQueueConsumer(core));
810    consumer->setAllowExtraAcquire(true);

814    *outProducer = producer;
815    *outConsumer = consumer;
816}

  1. 连接本地的图形缓冲区消费者

BLASTBufferItemConsumer(简称BBIC)继承自ConsumerBase,创建BBIC的同时,消费者模型与消费者监听器建立起了连接:

frameworks/native/libs/gui/ConsumerBase.cpp

58ConsumerBase::ConsumerBase(const sp<IGraphicBufferConsumer>& bufferQueue, bool controlledByApp) :
59        mAbandoned(false),
60        mConsumer(bufferQueue),
61        mPrevFinalReleaseFence(Fence::NO_FENCE) {
62    //进行类型转换,将 ConsumerBase 转换为 ConsumerListener
69    wp<ConsumerListener> listener = static_cast<ConsumerListener*>(this);
      //嵌套一层监听器代理
70    sp<IConsumerListener> proxy = new BufferQueue::ProxyConsumerListener(listener);
71    //连接消费者模型
72    status_t err = mConsumer->consumerConnect(proxy, controlledByApp);
73    if (err != NO_ERROR) {
76    } else {
77        mConsumer->setConsumerName(mName);
78    }
79}

frameworks/native/libs/gui/BufferQueueConsumer.cpp

510status_t BufferQueueConsumer::connect(
511        const sp<IConsumerListener>& consumerListener, bool controlledByApp) {
       ...
528    //将BufferQueue::ProxyConsumerListener 连接至 BufferQueueCore.mConsumerListener
529    mCore->mConsumerListener = consumerListener;
530    mCore->mConsumerControlledByApp = controlledByApp;
531
532    return NO_ERROR;
533}

这一步也就让 BBIC 建立了对Buffer状态的监听。接下来看BBQ如何有选择性的监听Buffer的状态。

  1. 设置图形缓冲区监听器

BBIC 拥有监听Buffer所有状态的能力,BBQ对Buffer特定状态的监听离不开 BBIC,因此,BBQ 继承了两个抽象类 ConsumerBaseBufferItemConsumer,分别针对 Buffer 消费状态与生产状态进行监听。

frameworks/native/libs/gui/BufferQueueConsumer.cpp

133BLASTBufferQueue::BLASTBufferQueue(const std::string& name, const sp<SurfaceControl>& surface,
134                                   int width, int height, int32_t format)
135      : mSurfaceControl(surface),
136        mSize(width, height),
137        mRequestedSize(mSize),
138        mFormat(format),
139        mNextTransaction(nullptr) {
       //设置消费者标识
156    mBufferItemConsumer->setName(String8(consumerName.c_str()));
       //添加图形缓冲区可消费状态监听
157    mBufferItemConsumer->setFrameAvailableListener(this);
       //添加图形缓冲区可生产状态监听
158    mBufferItemConsumer->setBufferFreedListener(this);
       //设置缓冲区Size
159    mBufferItemConsumer->setDefaultBufferSize(mSize.width, mSize.height);
160    mBufferItemConsumer->setDefaultBufferFormat(convertBufferFormat(format));
177}

BBQ初始化完成,消费者模型建立完成,由于BBQ动态监听缓冲区的状态,如果有可消费的缓冲区,BBQ会触发缓冲区的事务提交:

546void BLASTBufferQueue::onFrameAvailable(const BufferItem& item) {
       //如果客户通过setNextTransaction函数设置了自定义事务,触发线程阻塞
550    const bool nextTransactionSet = mNextTransaction != nullptr;
551    if (nextTransactionSet) {
552        while (mNumFrameAvailable > 0 || maxBuffersAcquired(false /* includeExtraAcquire */)) {
553            BQA_LOGV("waiting in onFrameAvailable...");
554            mCallbackCV.wait(_lock);
555        }
556    }
557    // add to shadow queue
558    mNumFrameAvailable++;
       //执行缓冲区的提交流程
564    processNextBufferLocked(nextTransactionSet /* useNextTransaction */);
565}

  1. 创建Surface对象

通过梳理BBQ的初始化,对消费者端的大概流程有了一定的认识,接下来梳理下生产者方的代表,也就是Surface。Android 显示的的内容来源于各种绘制模块,而这些绘制模块需要与BQ建立连接,获取Buffer用以绘制,这样才能将绘制的画像通过BBQ提交给SF合成。Surface作为生产者模型与绘制模块之间桥梁,相关的流程掌握显得尤为重要。

绘制模块指的是那些图像生产者,如以使用SurfaceView、GlSurfaceView、TextureView控件为代表的Video模块、Camera模块、游戏应用等,以及使用软件绘制、硬件加速绘制为代表的普通控件。

回到创建BBQ的流程,在ViewRootImpl.getOrCreateBLASTSurface方法中,创建完BBQ,紧接着会创建Surface对象,直接看Native 对象的构造函数:

66Surface::Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp,
67                 const sp<IBinder>& surfaceControlHandle)
68      : mGraphicBufferProducer(bufferProducer),
69        mCrop(Rect::EMPTY_RECT),
70        mBufferAge(0),
71        mGenerationNumber(0),
72        mSharedBufferMode(false),
73        mAutoRefresh(false),
74        mAutoPrerotation(false),
75        mSharedBufferSlot(BufferItem::INVALID_BUFFER_SLOT),
76        mSharedBufferHasBeenQueued(false),
77        mQueriedSupportedTimestamps(false),
78        mFrameTimestampsSupportsPresent(false),
79        mEnableFrameTimestamps(false),
80        mFrameEventHistory(std::make_unique<ProducerFrameEventHistory>()) {
81    // Initialize the ANativeWindow function pointers.
82    ANativeWindow::setSwapInterval  = hook_setSwapInterval;
83    ANativeWindow::dequeueBuffer    = hook_dequeueBuffer;
84    ANativeWindow::cancelBuffer     = hook_cancelBuffer;
85    ANativeWindow::queueBuffer      = hook_queueBuffer;
86    ANativeWindow::query            = hook_query;
87    ANativeWindow::perform          = hook_perform;
88
89    ANativeWindow::dequeueBuffer_DEPRECATED = hook_dequeueBuffer_DEPRECATED;
90    ANativeWindow::cancelBuffer_DEPRECATED  = hook_cancelBuffer_DEPRECATED;
91    ANativeWindow::lockBuffer_DEPRECATED    = hook_lockBuffer_DEPRECATED;
92    ANativeWindow::queueBuffer_DEPRECATED   = hook_queueBuffer_DEPRECATED;
116    mSurfaceControlHandle = surfaceControlHandle;
117}

首先Surface的创建会传入生产者模型 GraphicBufferProducer,这样Surface对象拥有了操作缓冲区的能力,同时在构造函数中Surface提供了一系列hook为首的函数,连接到ANativeWindow的函数指针,为的是给EGL模块提供对缓冲区操作的入口。而hook函数会直接调用内部的本地函数,以hook_queueBuffer 为例:

379int Surface::hook_dequeueBuffer(ANativeWindow* window,
380        ANativeWindowBuffer** buffer, int* fenceFd) {
381    Surface* c = getSelf(window);
382    {
383        std::shared_lock<std::shared_mutex> lock(c->mInterceptorMutex);
384        if (c->mDequeueInterceptor != nullptr) {
385            auto interceptor = c->mDequeueInterceptor;
386            auto data = c->mDequeueInterceptorData;
387            return interceptor(window, Surface::dequeueBufferInternal, data, buffer, fenceFd);
388        }
389    }
390    return c->dequeueBuffer(buffer, fenceFd);
391}

632int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {

       ....
661    status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, dqInput.width,
662                                                            dqInput.height, dqInput.format,
663                                                            dqInput.usage, &mBufferAge,
664                                                            dqInput.getTimestamps ?
665                                                                    &frameTimestamps : nullptr);
680
739    .....
740    return OK;
741}

同时软件绘制不需要通过hook函数来中转,当上层通过Surface.lockCanvas方法获取画布时会直接调用本地函数函数 Surface::dequeueBuffer

Surface只是绘制的中介,还需要与绘制模块进行连接后,绘制模块才能获取缓冲区和绘制图像数据,关于绘制模块如何连接到Surface,这里不做记录。

三、重点API功能原理解析

结合第一节的关于BBQ 重点API功能介绍与BBQ的初始化流程,回过头看下这三个API功能是如何实现的。

  1. setNextTransaction 功能原理解析

首先看 setNextTransaction 函数,调用者通过该接口可以实现将当前帧 Buffer 的提交权利控制在自己手中,同时可以加入其他图层想要的更新,然后提交,放在同一帧生效。可以思考下,如果当前帧的控制权交给了调用者,是否会导致下一帧的紊乱呢?看下这块流程:

582 void BLASTBufferQueue::setNextTransaction(SurfaceComposerClient::Transaction* t) {
583    std::lock_guard _lock{mMutex};
584    mNextTransaction = t;
585}

546 void BLASTBufferQueue::onFrameAvailable(const BufferItem& item) {
       //如果客户通过setNextTransaction函数设置了自定义事务,触发线程阻塞
550    const bool nextTransactionSet = mNextTransaction != nullptr;
551    if (nextTransactionSet) {
552        while (mNumFrameAvailable > 0 || maxBuffersAcquired(false /* includeExtraAcquire */)) {
554            mCallbackCV.wait(_lock);
555        }
556    }
557    // add to shadow queue
558    mNumFrameAvailable++;
       //执行缓冲区的提交流程
564    processNextBufferLocked(nextTransactionSet /* useNextTransaction */);
565}

339void BLASTBufferQueue::releaseBufferCallback(const ReleaseCallbackId& id,
340                                             const sp<Fence>& releaseFence, uint32_t transformHint,
341                                    uint32_t currentMaxAcquiredBufferCount) {
362    ...
       //唤醒线程
386    mCallbackCV.notify_all();
387}

这里BBQ做了线程阻塞的机制,当绘制模块绘制完成下一帧,并将Buffer放回了缓冲区队列,触发BBQ的 onFrameAvailable回调,如果调用者使用了setNextTransaction函数传入了自定义事务,那么就会在 onFrameAvailable函数中阻塞住线程, 暂停执行下一帧的 processNextBufferLocked。而唤醒线程的任务交给了 releaseBufferCallback 函数。

当前帧会执行绘制提交函数 processNextBufferLocked,但是不会立即提交,会将事务控制在自己手中。可以看到,releaseBufferCallback的回调函数会通过 t->setBuffer传递到SF端。

389void BLASTBufferQueue::processNextBufferLocked(bool useNextTransaction) {
403
404    SurfaceComposerClient::Transaction localTransaction;
405    bool applyTransaction = true;
406    SurfaceComposerClient::Transaction* t = &localTransaction;
       //如果客户通过setNextTransaction函数设置了自定义事务,applyTransaction 为false
407    if (mNextTransaction != nullptr && useNextTransaction) {
408        t = mNextTransaction;
409        mNextTransaction = nullptr;
410        applyTransaction = false;
411    }
412    ...
525    //存在客户端设置的自定义事务,放弃自此的缓冲区提交,将提交权给与客户端
526    if (applyTransaction) {
527        t->setApplyToken(mApplyToken).apply();
528    }
537}

也就是说当调用者主动提交事务后,SF端合成完成后会回调该通知,唤醒线程。否则会一直阻塞等待调用者提交。

大概流程如图示:

setNextTransaction 功能流程图

根据BBQ相关文档提示:

该机制在同步单个帧时阻塞在 UI 线程中很好,但在尝试同步多个帧时效果不佳。 它最终会减慢渲染速度。 相反,在 RenderThread 级别处理同步以允许 UI 线程继续处理帧

因此多帧同步还是有优化空间。

  1. mergeWithNextTransaction 功能原理解析

将调用者传入的事务合并到 BBQ 中的下一个事务。 当具有指定帧号的下一帧可用时,将直接与含有Buffer的事务进行合并并提交。也就是说将调用者事务所包含的其他对图层属性的更新合入到BBQ的事务中,与BBQ的事务在指定帧数一同生效。这个怎么实现的呢?

697void BLASTBufferQueue::mergeWithNextTransaction(SurfaceComposerClient::Transaction* t,
698                                                uint64_t frameNumber) {
699    std::lock_guard _lock{mMutex};
700    if (mLastAcquiredFrameNumber >= frameNumber) {
701        // Apply the transaction since we have already acquired the desired frame.
702        t->apply();
703    } else {
704        mPendingTransactions.emplace_back(frameNumber, *t);
705        // Clear the transaction so it can't be applied elsewhere.
706        t->clear();
707    }
708}

这个函数会将调用者传入的事务都保存在 mPendingTransactions 集合中,当执行到下一帧的
processNextBufferLocked 函数时,将集合中的事务都合入到BBQ事务中,然后直接提交:

389void BLASTBufferQueue::processNextBufferLocked(bool useNextTransaction) {
403
404    ...
479
       //如果上层通过mergeWithNextTransaction方法传入了事务,会被保存在mPendingTransactions
       //在下一帧提交时统一合入到 t 事务中
511    auto mergeTransaction =
512            [&t, currentFrameNumber = bufferItem.mFrameNumber](
513                    std::tuple<uint64_t, SurfaceComposerClient::Transaction> pendingTransaction) {
514                auto& [targetFrameNumber, transaction] = pendingTransaction;
515                if (currentFrameNumber < targetFrameNumber) {
516                    return false;
517                }
518                t->merge(std::move(transaction));
519                return true;
520            };
521
522    mPendingTransactions.erase(std::remove_if(mPendingTransactions.begin(),
523                                              mPendingTransactions.end(), mergeTransaction),
524                               mPendingTransactions.end());
525    //存在客户端设置的自定义事务,放弃自此的缓冲区提交,将提交权给与客户端
526    if (applyTransaction) {
527        t->setApplyToken(mApplyToken).apply();
528    }
537}

大概流程如图示:

mergeWithNextTransaction 功能流程图
  1. setTransactionCompleteCallback 功能原理解析

客户端可以监听Buffer的合成状态,在 SurfaceFlinger 中已应用包含带有 frameNumber 的缓冲区的事务时触发回调,通知调用者合成完成。

611void BLASTBufferQueue::setTransactionCompleteCallback(
612        uint64_t frameNumber, std::function<void(int64_t)>&& transactionCompleteCallback) {
613    std::lock_guard _lock{mMutex};
614    if (transactionCompleteCallback == nullptr) {
615        mTransactionCompleteCallback = nullptr;
616    }
620}

389void BLASTBufferQueue::processNextBufferLocked(bool useNextTransaction) {
403
       //设置缓冲区acquire Fence
475    t->setAcquireFence(mSurfaceControl,
476                       bufferItem.mFence ? new Fence(bufferItem.mFence->dup()) : Fence::NO_FENCE);
       //设置缓冲区合成完成的回调通知
477    t->addTransactionCompletedCallback(transactionCallbackThunk, static_cast<void*>(this));
478    mSurfaceControlsWithPendingCallback.push(mSurfaceControl);
479
525    //存在客户端设置的自定义事务,放弃自此的缓冲区提交,将提交权给与客户端
526    if (applyTransaction) {
527        t->setApplyToken(mApplyToken).apply();
528    }
537}

通过 t->addTransactionCompletedCallback 将 transactionCallbackThunk 回调函数传给了SF,当合成完成会触发回调,并通知调用者状态。

四、总结

根据上面流程的梳理,用一张图总结下BBQ与相关模块之间的结构关系:

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

推荐阅读更多精彩内容