Android 10.0 Input System 源码分析

前言

这篇文章主要解决以下问题:

  1. 什么是Linux标准输入协议?
  2. Android Input System架构是怎样的?
  3. Android ANR产生的原理是什么?如何避免ANR?
  4. 如何调试Android Input System?

本文结构

1、功能介绍
2、总体设计
3、详细设计
4、开发调试
5、总结
6、资料

一、功能介绍

Linux 输入协议linux/input.h 内核头文件中定义了一组标准事件类型和代码,输入设备驱动程序负责通过 Linux 输入协议将设备特定信号转换为标准输入事件格式。
接下来,Android EventHub 组件通过打开与每个输入设备关联的 evdev 驱动程序从内核读取输入事件。然后,Android InputReader 组件根据设备类别解码输入事件,并生成 Android 输入事件流。在此过程中,Linux 输入协议事件代码将根据输入设备配置、键盘布局文件和各种映射表,转化为 Android 事件代码。
最后,InputReader 将输入事件发送到 InputDispatcher,后者将这些事件转发到相应的窗口。(注:这段话选自官方输入系统文档

二、总体设计

2.1 Input System UML图

Android Input System UML批注.png
2.1.1 InputReader主要做三件事
  1. mEventHub获取驱动的RawEvent进行处理
  2. mDevice将RawEvent 加工成成熟的NotifyArgs,并添加到mQueueInputListener
  3. 调用mQueueInputListener.flush(),触发队列里的NotifyArgs.notify(),Dispatcher把NotifyArgs加工成EventEntry,并添加到mInboundQueue。
2.1.2 InputDispatcher主要做三件事,
  1. 从mInboundQueue获取EventEntry
  2. 通过mWindowHandlesByDisplay找到焦点窗口
  3. mConnectionsByFd把EventEntry转行成InputMessage并分发到InputChannel
2.1.3 ViewRootImpl主要做三件事
  1. 在scheduleTraversals()中mChoreographer.postCallback()一个mConsumedBatchedInputRunnable
  2. 等待下一帧到来时,调用mInputEventReceiver.consumeBatchedInputEvents()开始取出InputChannel内的InputMessage转化成InputEvent。
  3. mFirstInputStage根据不同的策略把InputEvent分发到不同字View处理。
    ViewRootImpl对InputEvent分发过程如下图。


    ViewRootImpl对InputEvent分发过程.png

2. 2 InputEvent 处理流程图。

根据UML我们可以得出InputEvent数据加工流程图,如下图。


image.png
  1. 首先EventHub从驱动获取RawEvent,接着InputReader根据事件的type把RawEvent加工成Android事件NotifyArgs,比如NotifyMotionArgs或NotifyKeyArgs,然后InputDispatcher把NotifyArgs转化成EventEntry,然后根据当前焦点窗口,把EventEntry转化成InputMessage,存放到InputChannel。
  2. 当处于焦点窗口的应用下一帧渲染触发的时候,会从InputChannel取出InputMessage,再把InputMessage转化成InputEvent,比如MotionEvent或KeyEvent,最终发到ViewRootImpl分发到给对应的子View处理。

三、详细设计

3.1 关键类的职责

  • InputManager:事件处理的核心,负责事件使用
  1. InputReaderThread(称为“InputReader”)读取和预处理原始输入事件,应用策略,并将消息发送到DispatcherThread管理的队列中。
  2. InputDispatcherThread(称为“InputDispatcher”)线程等待队列上的新事件,并异步地将它们分派给应用程序。
    根据设计,InputReaderThread类和InputDispatcherThread类不共享任何内部状态。 而且,所有通信都是从InputReaderThread到InputDispatcherThread的一种方式,绝不会相反。 但是,这两个类都可以与InputDispatchPolicy交互。
    InputManager类从不对Java本身进行任何调用。 相反,InputDispatchPolicy负责与系统执行所有外部交互,包括调用DVM服务。
  • EventHub: 事件的中心车站。
    EventHub汇总系统上所有输入设备(包括模拟器)接收的输入事件。 此外,EventHub通过生成伪造的输入事件以指示何时添加或删除设备。
    EventHub还提供输入事件流(通过getEvent方法)。
    它还支持查询输入设备的当前状态,例如识别当前按下的键。 最后,EventHub还有跟踪各个输入设备的功能,例如它们的类别和它们支持的键控代码集。

  • InputReader: InputReader从EventHub读取原始事件RawEvent数据,并将其处理为InputEvent,并将其发送到InputListener。 InputReader的某些功能(例如低功耗状态下的早期事件过滤)由单独的策略对象控制。
    InputReader拥有InputMappers的集合。 它所做的大部分工作都在InputReaderThread上进行,InputReader也可以接收在任意线程上运行的其他系统组件的查询。 为了使内容易于管理,InputReader使用单个Mutex来保护其状态。 互斥对象可以在调用EventHub或InputReaderPolicy时保留,但从不保留在调用InputListener时保留。

  • InputDevice: 表示单个输入设备的状态。
    InputMapper :输入映射器将源数据(raw data)转成熟数据(cooked data),单个输入设备可以具有多个关联的输入映射器,以便解释事件的不同类别。

  • InputReaderThread:InputReaderThread实现了类Threads.cpp的threadLoop(),Threads内部实现了一个线程循环,会不断调用threadLoop(),threadLoop()内调用InputReader loopOnce(),loopOnce()方法先从EventHub的getEvent获取RawEvent,然后把RawEvent传递给EventDevice的process(),process()方法内调用EventDevice内所有的InputMapper,InputMapper调用process()把RawEvent加工成不同类型的Event

  • InputDispatcherThread:InputDispatcherThread循环处理入队和调度事件。

  • InputDispatcherPolicyInterface :输入调度程序策略接口。
    输入阅读器策略由InputReader用来与Window Manager和其他系统组件进行交互。
    通过JNI到DVM的回调部分支持了实际的实现。 单元测试中也模拟了此接口。

  • InputChannel InputChannel由一个本地unix域套接字组成,该套接字用于跨进程发送和接收输入消息。 每个通道都有一个用于调试目的的描述性名称。每个端点都有自己的InputChannel对象,用于指定其文件描述符。释放所有对输入通道的引用后,将关闭该通道。

  • InputPublisher: InputDispatcher通过InputPublisher将InputEvent发布到InputChannel。

  • InputConsumer 消费来自InputChannel的Input Event。

3.2 InputMapper如何加工Raw数据

我们以InputMapper的子类TouchInputMapper为例分析下Raw数据是如何加工成Touch数据的
首先TouchMapper调用process(),接着调用sync(),然后调用processRawTouches(), 在调用cookAndDispatch(),最后cookPointerData(),这个方法才是处理对raw数据处理成触摸数据的。这样处理完数据后,回到cookAndDispatch()方法,它接下去调用dispatchPointerUsage()分发cookEvent
dispatchMotion()发送事件.
InputDispatcher的dispatchOnce() 接着dispatchOnceInnerLocked(),然后dispatchMotionLocked(),接着dispatchEventLocked() prepareDispatchCycleLocked() ->enqueueDispatchEntriesLocked()->enqueueDispatchEntryLocked() -> traceOutboundQueueLength()

比如我们在MotionEvent收到的压感Pressure,是乘以mPressureScale才得到我们MotionEvent中获取的压感Pressure。如下图


image.png

image.png

这样当我们想在驱动层对Pressure增加协议时就需要这个知识点了,我们应用上层要先对Pressure进行还原成原始数据,最后才能获取我们协议的内容。

有了这些知识之后,如果提一个需求,底层要用倾斜角作为协议字段,上层解析协议,技术可行吗?
首先我们找到Liunx Input定义的文件
https://source.android.com/devices/input/touch-devices#orientation-and-tilt-fields

image.png

接着我们查看官方文档 Liunx Input ABS_TILT_X和ABS_TILT_Y对应着raw.tiltX和raw.tiltY
最后从Android源码中找到TouchInputMapper::cookPointerData()中raw.tiltX和raw.tiltY的部分


image.png

把raw.tiltX和raw.tiltY三角函数操作得出tilt,tilt是无法逆还原成raw.tiltX和raw.tiltY,所以我们计划倾斜角作为协议字段技术是不可行的。


image.png

3.1 InputDispatcher是如何调度数据的

分析思路:下面我们围绕MotionEvent来分析调度流程。
当InputReader处理完事件后,会调用mQueuedInputListener.flush(),flush()会把集合所有的NotifyArgs都notify() ,notify()最终调用InputDispatcher的notify(),notify()内调用mLooper.wake() 唤醒线程loop,最后触发dispatcherOnce()。
dispatcherOnce()内部实际上是调用dispatchOnceInnerLocked()来调度事件, dispatchOnceInnerLocked()主要作用是从mInboundQueue取出mPendingEvent,然后根据事件类型调度事件。dispatchOnceInnerLocked()代码如下:

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // 准备处理新的Event
    if (!mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
            return;
        } else {
            // 入站队列中至少有一个Entry
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }
        // 准备发送事件
        resetANRTimeoutsLocked();
    }
    // 现在我们有一个事件要调度,根据事件type调度事件
    // 所有事件最终都会以这种方式出队和处理,即使我们打算删除它们也是如此
    bool done = false;
    switch (mPendingEvent->type) {
        case EventEntry::TYPE_CONFIGURATION_CHANGED: {
            //省略
            break;
        }

        case EventEntry::TYPE_DEVICE_RESET: {
            //省略
            break;
        }

        case EventEntry::TYPE_KEY: {
            //省略
            break;
        }

        case EventEntry::TYPE_MOTION: {
            MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
            // 调度MotionEvent
            done = dispatchMotionLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
            break;
        }
        
        default:
            ALOG_ASSERT(false);
            break;
    }
    if (done) {
        // 调度完事件后,重置全局变量状态,比如mPendingEvent置空。
        releasePendingEventLocked();
        // 强制下一个轮询强制唤醒。
        *nextWakeupTime = LONG_LONG_MIN;
    }
}

从上面代码我们知道,dispatchOnceInnerLocked()内调用dispatchMotionLocked()来处理MotionEvent,代码如下:

bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {
    bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;

    // 确定输入目标。
    std::vector<InputTarget> inputTargets;
    bool conflictingPointerActions = false;
    int32_t injectionResult;
    if (isPointerEvent) {
        // 指针 event.  (eg. 触摸屏)
        // 1. 找到事件所在的焦点窗口inputTargets
        injectionResult =
                findTouchedWindowTargetsLocked(currentTime, entry, inputTargets, nextWakeupTime,
                                               &conflictingPointerActions);
    }
    
    // 2. 从事件或重点显示中添加监视频道。
    addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(entry));
    
    // 省略代码
    // 3.调度MotionEvent
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

通过上面的代码我们发现dispatchMotionLocked()方法主要做三件事,首先通过findTouchedWindowTargetsLocked()找到事件所在的焦点窗口,然后inputTargets保存焦点窗口的副本。最后dispatchEventLocked()调度MotionEvent。
findTouchedWindowTargetsLocked()是如何获取焦点窗口呢?我们看看findTouchedWindowTargetsLocked()方法的实现,代码如下:

int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
                                                        const MotionEntry* entry,
                                                        std::vector<InputTarget>& inputTargets,
                                                        nsecs_t* nextWakeupTime,
                                                        bool* outConflictingPointerActions) {
    // 1.根据Touch类型判断是否刷新mTempTouchState(可以理解为缓存了焦点窗口状态)里面保存的窗口                               
    // 当新手势或是Down时,查找焦点窗口
    if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
      bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
      // 找焦点窗口
        sp<InputWindowHandle> newTouchedWindowHandle =
                findTouchedWindowAtLocked(displayId, x, y, isDown /*addOutsideTargets*/,
                                          true /*addPortalWindows*/);
      if (newTouchedWindowHandle != nullptr) {
         // mTempTouchState添加查找到的newTouchedWindowHandle
         mTempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);
      }
      mTempTouchState.addGestureMonitors(newGestureMonitors);
    } else {
        // 事件类型 move/up/cancel/不可分割的指针down
        // 从mTempTouchState获取焦点窗口
    }
    // 2.检查mTempTouchState.windows所有窗口是否都已经准备好了。
    for (const TouchedWindow& touchedWindow : mTempTouchState.windows) {
        if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
            // 通过checkWindowReadyForMoreInputLocked()检测窗口是否ready。
            std::string reason =
                    checkWindowReadyForMoreInputLocked(currentTime, touchedWindow.windowHandle,
                                                       entry, "touched");
            if (!reason.empty()) {
                // 没有ready则发送ANR
                injectionResult = handleTargetsNotReadyLocked(currentTime, entry, nullptr,
                                                              touchedWindow.windowHandle,
                                                              nextWakeupTime, reason.c_str());
                // 代码跳转到无响应部分                                          
                goto Unresponsive;
            }
        }
    }
                                                        }

我们知道方法checkWindowReadyForMoreInputLocked()用于检测窗口是否准备好了,那他检测窗口准备好的条件是哪些呢?老方法我们从方法代码入手,代码如下:

    std::string InputDispatcher::checkWindowReadyForMoreInputLocked(
        nsecs_t currentTime, const sp<InputWindowHandle>& windowHandle,
        const EventEntry* eventEntry, const char* targetType) {
    // 如果窗口被暂停了,则继续等待
    if (windowHandle->getInfo()->paused) {
        return StringPrintf("Waiting because the %s window is paused.", targetType);
    }
    if (eventEntry->type == EventEntry::TYPE_KEY) {
        // 判断key事件的逻辑
    } else {
        // Touch事件窗口是否ready逻辑
        // 会导致暂停输入事件传递的一种情况是:由于应用程序没有响应,因此waitQueue等待队列中堆积了很多事件。
        if (!connection->waitQueue.isEmpty() &&
            currentTime >= connection->waitQueue.head->deliveryTime + STREAM_AHEAD_EVENT_TIMEOUT) {
            return StringPrintf("Waiting to send non-key event because the %s window has not "
                                "finished processing certain input events that were delivered to "
                                "it over "
                                "%0.1fms ago.  Wait queue length: %d.  Wait queue head age: "
                                "%0.1fms.",
                                targetType, STREAM_AHEAD_EVENT_TIMEOUT * 0.000001f,
                                connection->waitQueue.count(),
                                (currentTime - connection->waitQueue.head->deliveryTime) *
                                        0.000001f);
        }
    }
}

以上代码我们可以看出会导致Touch事件无法消费的一种情况是,应用程序的等待队列堆积了事件,并且等待队列的头部事件deliveryTime + STREAM_AHEAD_EVENT_TIMEOUT超过当前事件,就会导致ANR。checkWindowReadyForMoreInputLocked()返回超时原因之后,就会执行handleTargetsNotReadyLocked()发送ANR,代码如下:

// 默认超时时间5s
constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL;
 
int32_t InputDispatcher::handleTargetsNotReadyLocked(
        nsecs_t currentTime, const EventEntry* entry,
        const sp<InputApplicationHandle>& applicationHandle,
        const sp<InputWindowHandle>& windowHandle, nsecs_t* nextWakeupTime, const char* reason) {
    nsecs_t timeout;
            // 窗口
            if (windowHandle != nullptr) {
                timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
            // Application
            } else if (applicationHandle != nullptr) {
                timeout =
                        applicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
            } else {
                timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;
            }
   onANRLocked(currentTime, applicationHandle, windowHandle, entry->eventTime,
                    mInputTargetWaitStartTime, reason);
}

handleTargetsNotReadyLocked() 方法会调到onANRLocked(),onANRLocked()会doNotifyANRLockedInterruptible(),doNotifyANRLockedInterruptible()会调用NativeInputManager::notifyANR(),最终调到Java层的InputManagerService.notifyANR(),最终通过WindowManagerServer的InputMonitor把ANR抛给对应的窗口。这就解答了为什么ANR?这是因为应用的UI线程有耗时的操作,导致InputDispatcher的TouchEvent堆积,当超过设置的超时时间(5s)时,就抛出ANR,所以我们把耗时操作尽量放在子线程执行,避免ANR的产生。
扩展问题:在View的onTouchEvent()做耗时操作会导致后续的MotionEvent接收变慢吗?
答案:是会,原因同样onTouchEvent()是执行在UI线程,如果做耗时操作,TouchEvent会无法及时分发导致堆积,分发事件的速率变慢。

接着我们回到方法dispatchMotionLocked(),获取MotionEvent所在焦点窗口后,我们看看dispatchEventLocked()是如何调度MotionEvent,看下代码

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime, EventEntry* eventEntry,
                                          const std::vector<InputTarget>& inputTargets) {
    
}

dispatchEventLocked() -> prepareDispatchCycleLocked() -> enqueueDispatchEntriesLocked() -> startDispatchCycleLocked() -> connection->InputPublisher .publishMotionEvent()

InputDispatcher最终把InputEvent发布到InputPublisher,那Android系统什么时候处理事件呢?
我们追踪代码,发现处理InputEvent的触发是下一帧渲染开始执行的,下面是方法调用链。
ViewRootImpl.scheduleTraversals() -> ViewRootImpl.scheduleConsumeBatchedInput()
FrameDisplayEventReceiver.onVsync() -> Choreographer.doFrame() ->
mConsumedBatchedInputRunnable.run() -> android_view_InputEventReceiver.nativeConsumeBatchedInputEvents() -> NativeInputEventReceiver.consumeEvents() -> InputEventReceiver.dispatchInputEvent() ->WindowInputEventReceiver.onInputEvent()

方法调用链大概执行了这些处理,ViewRootImpl执行scheduleTraversals()后在Choreographer内注册一个mConsumedBatchedInputRunnable,在下一帧开始的时候,mConsumedBatchedInputRunnable.run()会执行,调到native层nativeConsumeBatchedInputEvents(),方法内部实际上是调用NativeInputEventReceiver.consumeEvents()来调批量处理InputEvents,consumeEvents()代码如下:

status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
    for (;;) {
        uint32_t seq;
        InputEvent* inputEvent;
        // 1. 从InputConsumer取出事件
        status_t status = mInputConsumer.consume(&mInputEventFactory,
                consumeBatches, frameTime, &seq, &inputEvent);
                jobject inputEventObj;
            switch (inputEvent->getType()) {
            case AINPUT_EVENT_TYPE_KEY:
                // key 事件
                break;

            case AINPUT_EVENT_TYPE_MOTION: {
                // 2. inputEvent强转成MotionEvent,再生成Java MotionEvent对象
                MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
                inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
                break;
            }

            default:
                inputEventObj = NULL;
            }
            // 3.inputEventObj分发到ViewRootImpl内部类WindowInputEventReceiver.onInputEvent()
            env->CallVoidMethod(receiverObj.get(),
                        gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
    }
}

大概是从InputConsumer取出数据InputEvent,并根据InputEvent生成Java成的MotionEvent,最后InputEventReceiver.dispatchInputEvent()把数据分发出去,

四、开发调试

4.1 dumpsys input

比如我们系统触摸不稳定,我们怀疑Android Input System传上来的Input Event有问题,我们如何获取EventHub/InputReader/InputDispatcher信息呢?
Android 中提供了dumpsys 输入命令可转储系统输入设备(例如键盘和触摸屏)的状态以及输入事件的处理。
命令如下:

adb shell dumpsys input 

我们根据InputEvent 的流程分析EventHub/InputReader/InputDispatcher这三种状态数据是否正确或符合预期,从而排查问题。
dumpsys 输入诊断详细官方文档

4.2 getevent与sendevent

网上有很多资料这里不再赘述。
getevent/sendevent 使用说明

五、总结

我们关于Android Input System源码分析就到这里,源码分析总的思路就是顺着RawEvent的处理过程为突破口,研究RawEvent是如何加工成InputEvent的?为何要这样加工,加工完InputEvent是如何分发到对应的焦点窗口所在的应用进程,应用进程在什么时机读取InputEvent,ViewRootImpl如何把InputEvent分发到对应的子View。数据WindowManager与InputManager的关系、ViewRootImpl接收到事件是如何分发的,WindowManager本文暂时没有分析,留在下节。

六、资料

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