Android Input(八)- ANR原理分析

原创内容,转载请注明出处,多谢配合。

先针对前面的Input调用流程进行一个简单总结:

EventHub: 收集底层硬件设备tp报点。打开"/dev/input/"目录下的input设备,并将其注册到epoll的监控队列中。一旦对应设备上有可读的input事件,马上包装成event,上报给InputReader。

InputReader: 获取EventHub上报的input事件,通过deviceid,找到具体的InputDevice,然后匹配上对应的InputMapper来处理对应input事件。InputMapper最终会按对应的NotifyArgs数据结构对事件进行封装,并加入到ArgsQueue中。最终flush操作会将ArgsQueue中的每一个Arg取出来转成对应的Entry加入到InputDispatcher的mInboundQueue。

InputDispatcher: InboundQueue队头取出事件,匹配焦点窗口,通过mConnectionsByFd 获取到对应的Connection,将eventEntry转化为DispatchEntry并加入到Connection的outboundQueue队列,同时通过Connection获取InputChannel将事件发送到客户端,再将DispatchEntry从outboundQueue移到waitQueue里,等待客户端完成处理反馈完成消息,而InputDispatcher收到消息后回调handleReceiveCallback,将DispatchEntry从waitQueue里移出。

ViewRootImpl: 调用NaitveInputEventReceiver的handleEvent()接收input事件,转成java层对应的事件,通过InputStage体系相关类按责任链的模式进行事件分发操作。事件分发处理完毕后会执行finishInputEvent,通过InputChannel 发送完成的message,给到服务端InputDispatcher回调InputDispatcher.handleReceiveCallback()。

接下来解析Input ANR原理(代码来源: Android 8.0)。

一、ANR触发条件

frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    //ANR计算的起点时间
    nsecs_t currentTime = now(); 
...
//如果正在处理的mPendingEvent为空才会进来
if (! mPendingEvent) {
    if (mInboundQueue.isEmpty()) {
        if (isAppSwitchDue) {
            // The inbound queue is empty so the app switch key we were waiting
           // for will never arrive.  Stop waiting for it.
           resetPendingAppSwitchLocked(false);
            isAppSwitchDue = false;
        }
        // Synthesize a key repeat if appropriate.
       if (mKeyRepeatState.lastKeyEntry) {
            if (currentTime >= mKeyRepeatState.nextRepeatTime) {
                mPendingEvent = synthesizeKeyRepeatLocked(currentTime);
            } else {
                if (mKeyRepeatState.nextRepeatTime < *nextWakeupTime) {
                    *nextWakeupTime = mKeyRepeatState.nextRepeatTime;
                }
            }
        }
        // Nothing to do if there is no pending event.
       if (!mPendingEvent) {
            return;
        }
    } else {
        //从mInboundQueue头部取出一个事件
       mPendingEvent = mInboundQueue.dequeueAtHead();
        traceInboundQueueLengthLocked();
    }
    // Poke user activity for this event.
   if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
        pokeUserActivityLocked(mPendingEvent);
    }
   resetANRTimeoutsLocked();
}
...
switch (mPendingEvent->type) {
  case EventEntry::TYPE_KEY: {
    KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
...
    done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
    break;
}
}
…
if (done) {
    if (dropReason != DROP_REASON_NOT_DROPPED) {
        dropInboundEventLocked(mPendingEvent, dropReason);
    }
    mLastDropReason = dropReason;
    releasePendingEventLocked();
    *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
  }
}
void InputDispatcher::releasePendingEventLocked() {
    if (mPendingEvent) {
        resetANRTimeoutsLocked();
        releaseInboundEventLocked(mPendingEvent);
        mPendingEvent = NULL;
    }
}
void InputDispatcher::resetANRTimeoutsLocked() {
  // 这里reset等待超时原因和handle
   mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_NONE;
   mInputTargetWaitApplicationHandle.clear();
}

InputDispatcher从dispatchOnceInnerLocked开始事件的分发流程:

  • 先获取anr计算的起点时间
  • 获取事件,mPendingEvent不等于空才会进获取事件逻辑,mInboundQueue不为空的情况下,从头部取出事件,通过resetANRTimeoutsLocked来reset等待超时原因和handle。
  • 针对不同类型进行对应的事件分发
  • 事件处理完之后通过releasePendingEventLocked重置mPendingEvent = NULL;

因此前一个事件没有处理完,下一次分发是不会进入到获取事件逻辑的。

继续跟dispatchKeyLocked流程:

bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
        DropReason* dropReason, nsecs_t* nextWakeupTime) {
…        //寻找焦点窗口
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
...
    //分发事件
   dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

再看findFocusedWindowTargetsLocked

int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
...
if (mFocusedWindowHandle == NULL) {
    if (mFocusedApplicationHandle != NULL) {
        injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                mFocusedApplicationHandle, NULL, nextWakeupTime,
                "Waiting because no window has focus but there is a "
               "focused application that may eventually add a window "
               "when it finishes starting up.");
        goto Unresponsive;
    }
    ALOGI("Dropping event because there is no focused window or focused application.");
    injectionResult = INPUT_EVENT_INJECTION_FAILED;
    goto Failed;
}
…
// Check whether the window is ready for more input.
reason = checkWindowReadyForMoreInputLocked(currentTime,
        mFocusedWindowHandle, entry, "focused");
...
if (!reason.isEmpty()) {
    injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
            mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.string());
    goto Unresponsive;
}
...
   return injectionResult;
}

这里先看checkWindowReadyForMoreInputLocked,这个方法主要是针对window还没有准备好的情况,收集对应的reason:

String8 InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime,
        const sp<InputWindowHandle>& windowHandle, const EventEntry* eventEntry,
        const char* targetType) {
       //窗口暂停
   if (windowHandle->getInfo()->paused) {
        return String8::format("Waiting because the %s window is paused.", targetType);
    }
   ssize_t connectionIndex = getConnectionIndexLocked(windowHandle->getInputChannel());
    //窗口未连接
    if (connectionIndex < 0) {
        return String8::format("Waiting because the %s window's input channel is not "
               "registered with the input dispatcher.  The window may be in the process "
               "of being removed.", targetType);
    }
    //窗口连接已死亡
   sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
    if (connection->status != Connection::STATUS_NORMAL) {
        return String8::format("Waiting because the %s window's input connection is %s."
               "The window may be in the process of being removed.", targetType,
                connection->getStatusLabel());
    }
    //窗口连接已满
   if (connection->inputPublisherBlocked) {
        return String8::format("Waiting because the %s window's input channel is full.  "
               "Outbound queue length: %d.  Wait queue length: %d.",
                targetType, connection->outboundQueue.count(), connection->waitQueue.count());
    }
   if (eventEntry->type == EventEntry::TYPE_KEY) {
        //按键事件,输出队列或事件等待队列不为空
       if (!connection->outboundQueue.isEmpty() || !connection->waitQueue.isEmpty()) {
           return String8::format("Waiting to send key event because the %s window has not "
                   "finished processing all of the input events that were previously "
                   "delivered to it.  Outbound queue length: %d.  Wait queue length: %d.",
                   targetType, connection->outboundQueue.count(), connection->waitQueue.count());
       }
   } else {
       //非按键事件,事件等待队列不为空且头事件分发超时500ms
       if (!connection->waitQueue.isEmpty()
                && currentTime >= connection->waitQueue.head->deliveryTime
                        + STREAM_AHEAD_EVENT_TIMEOUT) {
            return String8::format("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);
        }
    }
    return String8::empty();
}

然后再看handleTargetsNotReadyLocked,这里currentTime作为参数一直从dispatchOnceInnerLocked传递到handleTargetsNotReadyLocked,这里是ANR计算的核心方法:

int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,
       const EventEntry* entry,
       const sp<InputApplicationHandle>& applicationHandle,
       const sp<InputWindowHandle>& windowHandle,
       nsecs_t* nextWakeupTime, const char* reason) {
   if (applicationHandle == NULL && windowHandle == NULL) {
       if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_SYSTEM_NOT_READY) {
#if DEBUG_FOCUS
           ALOGD("Waiting for system to become ready for input.  Reason: %s", reason);
#endif
           mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_SYSTEM_NOT_READY;
           mInputTargetWaitStartTime = currentTime;
           mInputTargetWaitTimeoutTime = LONG_LONG_MAX;
           mInputTargetWaitTimeoutExpired = false;
           mInputTargetWaitApplicationHandle.clear();
       }
   } else {
        //前面resetANRTimeoutsLocked执行时reset mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_NONE,所以能进方法
       if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
#if DEBUG_FOCUS
           ALOGD("Waiting for application to become ready for input: %s.  Reason: %s",
                   getApplicationWindowLabelLocked(applicationHandle, windowHandle).string(),
                   reason);
#endif
           nsecs_t timeout;
           if (windowHandle != NULL) {
               timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
           } else if (applicationHandle != NULL) {
               timeout = applicationHandle->getDispatchingTimeout(
                       DEFAULT_INPUT_DISPATCHING_TIMEOUT);
           } else {
               timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT; //5S
           }
           mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY;
           mInputTargetWaitStartTime = currentTime;
           mInputTargetWaitTimeoutTime = currentTime + timeout; //当前时间+5S
           mInputTargetWaitTimeoutExpired = false;
           mInputTargetWaitApplicationHandle.clear();
           if (windowHandle != NULL) {
               mInputTargetWaitApplicationHandle = windowHandle->inputApplicationHandle;
           }
           if (mInputTargetWaitApplicationHandle == NULL && applicationHandle != NULL) {
               mInputTargetWaitApplicationHandle = applicationHandle;
           }
       }
   }
   if (mInputTargetWaitTimeoutExpired) {
       return INPUT_EVENT_INJECTION_TIMED_OUT;
   }
    //当前时间大于设置的超时时间就会走ANR触发流程
   if (currentTime >= mInputTargetWaitTimeoutTime) {
       onANRLocked(currentTime, applicationHandle, windowHandle,
               entry->eventTime, mInputTargetWaitStartTime, reason);
       // Force poll loop to wake up immediately on next iteration once we get the
       // ANR response back from the policy.
       *nextWakeupTime = LONG_LONG_MIN;
       return INPUT_EVENT_INJECTION_PENDING;
   } else {
       // Force poll loop to wake up when timeout is due.
       if (mInputTargetWaitTimeoutTime < *nextWakeupTime) {
           *nextWakeupTime = mInputTargetWaitTimeoutTime;
       }
       return INPUT_EVENT_INJECTION_PENDING;
   }
}

可见ANR超时时间区间是从dispatchOnceInnerLocked()开始,到下次执行handleTargetsNotReadyLocked()这段应用为准备就绪的时间来决定是否触发ANR。

二、ANR执行流程

handleTargetsNotReadyLocked方法中,当前时间大于设置的超时时间就会走ANR触发流程onANRLocked。

void InputDispatcher::onANRLocked(
        nsecs_t currentTime, const sp<InputApplicationHandle>& applicationHandle,
        const sp<InputWindowHandle>& windowHandle,
        nsecs_t eventTime, nsecs_t waitStartTime, const char* reason) {
    float dispatchLatency = (currentTime - eventTime) * 0.000001f;
    float waitDuration = (currentTime - waitStartTime) * 0.000001f;
    ALOGI("Application is not responding: %s.  "
           "It has been %0.1fms since event, %0.1fms since wait started.  Reason: %s",
            getApplicationWindowLabelLocked(applicationHandle, windowHandle).string(),
            dispatchLatency, waitDuration, reason);
    // 收集ANR现场信息
   time_t t = time(NULL);
    struct tm tm;
    localtime_r(&t, &tm);
    char timestr[64];
    strftime(timestr, sizeof(timestr), "%F %T", &tm);
    mLastANRState.clear();
    mLastANRState.append(INDENT "ANR:\n");
    mLastANRState.appendFormat(INDENT2 "Time: %s\n", timestr);
    mLastANRState.appendFormat(INDENT2 "Window: %s\n",
            getApplicationWindowLabelLocked(applicationHandle, windowHandle).string());
    mLastANRState.appendFormat(INDENT2 "DispatchLatency: %0.1fms\n", dispatchLatency);
    mLastANRState.appendFormat(INDENT2 "WaitDuration: %0.1fms\n", waitDuration);
    mLastANRState.appendFormat(INDENT2 "Reason: %s\n", reason);
    //dump信息
    dumpDispatchStateLocked(mLastANRState);
    //将ANR命令加入commandQueue
    CommandEntry* commandEntry = postCommandLocked(
            & InputDispatcher::doNotifyANRLockedInterruptible);
    commandEntry->inputApplicationHandle = applicationHandle;
    commandEntry->inputWindowHandle = windowHandle;
    commandEntry->reason = reason;
}

这里收集ANR现场信息并执行dump操作,然后将ANR命令加入commandQueue,在下一轮InputDispatcher.dispatchOnce中是先通过runCommandsLockedInterruptible()方法取出 mCommandQueue队列的所有命令逐一执行。ANR对应的command为doNotifyANRLockedInterruptible。

void InputDispatcher::doNotifyANRLockedInterruptible(
        CommandEntry* commandEntry) {
    mLock.unlock();
    //mPolicy是指NativeInputManager
    nsecs_t newTimeout = mPolicy->notifyANR(
            commandEntry->inputApplicationHandle, commandEntry->inputWindowHandle,
            commandEntry->reason);
    mLock.lock();
    resumeAfterTargetsNotReadyTimeoutLocked(newTimeout,
            commandEntry->inputWindowHandle != NULL
                    ? commandEntry->inputWindowHandle->getInputChannel() : NULL);
}

这里调用NativeInputManager的notifyANR方法。

frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp

nsecs_t NativeInputManager::notifyANR(const sp<InputApplicationHandle>& inputApplicationHandle,
        const sp<InputWindowHandle>& inputWindowHandle, const String8& reason) {
...
    //jni调用java方法
    jlong newTimeout = env->CallLongMethod(mServiceObj,
                gServiceClassInfo.notifyANR, inputApplicationHandleObj, inputWindowHandleObj,
                reasonObj);
...
    return newTimeout;
}

这里通过JNI调用InputManagerService执行notifyANR。

frameworks/base/services/core/java/com/android/server/input/InputManagerService.java

// Native callback.
private long notifyANR(InputApplicationHandle inputApplicationHandle,
       InputWindowHandle inputWindowHandle, String reason) {
    return mWindowManagerCallbacks.notifyANR(
            inputApplicationHandle, inputWindowHandle, reason);
}

此处mWindowManagerCallbacks是指InputMonitor对象。

public long notifyANR(InputApplicationHandle inputApplicationHandle,
       InputWindowHandle inputWindowHandle, String reason) {
    AppWindowToken appWindowToken = null;
   WindowState windowState = null;
...
    // All the calls below need to happen without the WM lock held since they call into AM.
   mService.mAmInternal.saveANRState(reason);
…
    else if (windowState != null) {
        try {
            // Notify the activity manager about the timeout and let it decide whether
           // to abort dispatching or keep waiting.
           long timeout = ActivityManager.getService().inputDispatchingTimedOut(
                    windowState.mSession.mPid, aboveSystem, reason);
...
}

这里通过token打通对应Activity与Window的联系,最终调用AMS的inputDispatchingTimedOut

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

public long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
    if (checkCallingPermission(android.Manifest.permission.FILTER_EVENTS)
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("Requires permission "
               + android.Manifest.permission.FILTER_EVENTS);
   }
    ProcessRecord proc;
   long timeout;
   synchronized (this) {
        synchronized (mPidsSelfLocked) {
            proc = mPidsSelfLocked.get(pid);
       }
        timeout = getInputDispatchingTimeoutLocked(proc);
   }
    if (!inputDispatchingTimedOut(proc, null, null, aboveSystem, reason)) {
        return -1;
   }
    return timeout;
}

最终调用inputDispatchingTimedOut

public boolean inputDispatchingTimedOut(final ProcessRecord proc,
       final ActivityRecord activity, final ActivityRecord parent,
       final boolean aboveSystem, String reason) {
...
    if (proc != null) {
 ...
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation);
           }
        });
   }
    return true;
}

通过handler机制,交由“ActivityManager”线程执行ANR处理过程, appNotResponding会dump信息以及弹窗提示ANR等。

frameworks/base/services/core/java/com/android/server/am/AppErrors.java
    final void appNotResponding(ProcessRecord app, ActivityRecord activity,
            ActivityRecord parent, boolean aboveSystem, final String annotation) {
      ...
        // For background ANRs, don't pass the ProcessCpuTracker to
        // avoid spending 1/2 second collecting stats to rank lastPids.
        //dump信息
        File tracesFile = mService.dumpStackTraces(true, firstPids,
                                                   (isSilentANR) ? null : processCpuTracker,
                                                   (isSilentANR) ? null : lastPids,
                                                   nativePids);
...
        //DropBox收集信息
        mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
...

            // 弹App Not Responding dialog
            Message msg = Message.obtain();
            HashMap<String, Object> map = new HashMap<String, Object>();
            msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
            msg.obj = map;
            msg.arg1 = aboveSystem ? 1 : 0;
            map.put("app", app);
            if (activity != null) {
                map.put("activity", activity);
            }
            mService.mUiHandler.sendMessage(msg);
        }
    }

最后,可以通过adb shell dumpsys input来查看手机当前的input状态, 输出内容分别为EventHub.dump(), InputReader.dump(),InputDispatcher.dump()这3类,另外如果发生过input ANR,那么也会输出上一个ANR的状态。

最后借辉辉一张图:

Input ANR触发流程

ANR原理分析总结:
InputReader线程通过EventHub监听底层上报的输入事件,一旦收到输入事件则将其放至mInBoundQueue队列,并唤醒InputDispatcher线程

InputDispatcher开始分发输入事件,设置ANR监控的起点时间。先检测是否有正在处理的事件:mPendingEvent,如果没有则从mInBoundQueue队头取事件,并将其赋值给mPendingEvent,且重置ANR的timeout;否则不会从mInBoundQueue中取出事件,也不会重置timeout。然后检查窗口是否就绪:checkWindowReadyForMoreInputLocked,
满足以下任一情况,则会进入ANR检测状态:

  • 窗口暂停
  • 窗口未连接
  • 窗口连接已死亡
  • 窗口连接已满
  • 按键事件,outboundQueue或者waitQueue不为空
  • 非按键事件,waitQueue不为空且头事件分发超时500ms

若窗口未准备就绪,即满足以上条件:
就是窗口还没准备好跟你进行socket通信,那么你就只能等待,这个时候如果满足ANR超时时间会触发ANR。收集ANR信息,生成doNotifyANRLockedInterruptible加入commandQueue中,下次执行InputDispatcher::dispatchOnce 时候,先执行command,走notifyANR
流程,最终通过AMS发送消息来执行ANR处理过程:appNotResponding会dump信息以及弹窗提示ANR等。

若窗口准备就绪,连接成功:
则将mPendingEvent转移到outBoundQueue队列,当outBoundQueue不为空,且应用管道对端连接状态正常,则将数据从outboundQueue中取出事件,放入waitQueue队列,InputDispatcher通过socket告知目标应用所在进程可以准备开始干活,App在初始化时默认已创建跟中控系统双向通信的socketpair,此时App的主线程收到输入事件后,会层层转发到目标窗口来处理。完成工作后,会通过socket向中控系统汇报工作完成,则中控系统会将该事件从waitQueue队列中移除。

下一篇文章:
Android Input(九)-Input问题分析指北

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