Android 开发艺术探索笔记之八 -- 理解 Window 和 WindowManager

写在最前:本文涉及到源码的部分,查看的是 Android 8.1.0_r33 的源码,部分与原文中代码有出入。

附上查看源码的网址:http://androidxref.com/


学习内容:

  • Window 和 WindowManager
  • Window 的内部工作原理
    • Window 的添加、更新和删除
  • Actvitiy、Dialog 等类型的 Window 对象的创建过程

原文开篇部分:

  • Window 是一个抽象类,具体实现是 PhoneWindow
  • WindowManager 是外界访问 Window 的入口,Window 的实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程
  • Window 实际是 View 的管理者,视图都是通过 Window 呈现的。

1.Window 和 WindowManger

通过 WindowManager 添加一个 Window

mFloatingButton = new Button(this);
mFloatintButton.setText("button");

mLayoutParams = new WindowManager.LayoutParams(LayoutParams.RTAP_CONTENT,LayoutParams.WRAP_CONTENT,0,0,PixelFormat.TRANSPARENT);
mLayoutParams.flags = KatiytOarams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;

mWindowManager.addView(mFloatingButton, mLayoutParams);

通过以上代码,可以将一个 Button 添加到坐标为(100,300)的位置上。

下面说明 WindowManager.LayoutParams 中的 flagstype 这两个参数:

  1. flags 参数表示 Window 的属性,下面列出一个常用的选项:

    1. FLAG_NOT_FOCUSABLE
      表示 WIndow 不需要获取焦点,也不需要接收各种输入事件,最终事件会传递给下层的具有焦点的 Window
  1. FLAG_NOT_TOUCH_MODAL
    系统会将当前 Window 区域以外的单击事件传递给底层的 Window,当前 Window 区域以内的事件则自己处理。一般开启,否则其他 Window 将无法收到单击事件
  1. FLAG_SHOW_WHEN_LOCKED
    开启此模式可以让 Window 显示在锁屏界面

  2. Type 参数表示 Window 的类型,Window 有三种类型:

    1. 应用 Window:对应一个 Activity;层级范围是 1 ~ 99。
    2. 子 Window:不能单独存在,需要附属再特定的父 Window 上,比如常见的 Dialog;层级范围是 1000 ~ 1999
    3. 系统 Window:需要声明权限才能创建的 Window,比如 Toast;层级范围是 2000 ~ 2999

    关于上面提到的层级范围,此处进行说明:Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖再层级小的 Window 的上面。

WindowManager 提供的功能

提供的功能很简单,一般只有三个方法:添加 View、更新 View 和删除 View,这三个方法定义在接口 ViewManager 中(WindowManager 继承了 ViewManager)

public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

//继承关系
public interface WindowManager extends ViewManager {
    //...
}

原文中说道,"WindowManager 操作 Window 的过程更像是在操作 Window 中的 View"。


2. Window 的内部机制

Window 是一个抽象概念,每一个 Window都对应着一个 View 和一个 ViewRootImpl,WIndow 和 View 通过 ViewRootImpl 建立联系,因此 Window 以 View 的形式存在,View 才是 Window 存在的实体

实际开发中,对 Window 的访问必须通过 WindowManager。

2.1 Window 的添加过程

添加过程通过 WindowManager 的 addView 来实现,具体实现类是 WindowManagerImpl 类。

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

可以看到,实际上 WindowManagerImp 采用了桥接模式,将具体的实现 委托 给了 mGlobal(WindowManagerGlobal)来处理,WindowManagerGlobal 以工厂的形式向外提供自己的实例。

WindowManagerGlobal 的 addView 方法有如下几步:

  1. 检查参数是否合法,如果是子 Window 那么还需要调整一些布局参数

    if (view == null) {
     throw new IllegalArgumentException("view must not be null");
    }
    if (display == null) {
     throw new IllegalArgumentException("display must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
     throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }
    
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
     parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    
  1. 创建 ViewRootImpl 并将 View 添加到列表中

    root = new ViewRootImpl(view.getContext(), display);
    
    view.setLayoutParams(wparams);
    
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    

    关于上面代码中的 mViews,mRoots 等:

    //存储所有 Window 所对应的 View
    private final ArrayList<View> mViews = new ArrayList<View>();
    //存储所有 Window 所对应的 ViewRootImpl
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    //存储所有 Window 中对应的布局参数
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
    //存储正在被删除的 View 对象,或者说已经调用 removeView 方法但是删除操作尚未完成 Window 对象
    private final ArraySet<View> mDyingViews = new ArraySet<View>();
    
  1. 通过 ViewRootImpl 来更新界面并完成 Window 的添加过程

    这个步骤通过 ViewRootImpl 的 setView 方法完成:

    //WindowManagerGlobal.addView() 方法内部
    root.setView(view, wparams, panelParentView);
    
    //ViewRootImpl.setView(...) 方法内部
    //...
    requestLayout();
    //...
    
    //ViewRootImpl
    @Override
    public void requestLayout() {
     if (!mHandlingLayoutInLayoutRequest) {
         checkThread();
         mLayoutRequested = true;
         scheduleTraversals();
     }
    }
    

    可以看到,通过 requestLayout 发起异步刷新请求,而其中的 scheduleTraversals 实际上是 View 绘制的入口。

    接着会通过 WindowSession 最终来完成 Window 的添加过程。

    //ViewRootImpl.setView(..)方法内部
    try {
     mOrigWindowType = mWindowAttributes.type;
     mAttachInfo.mRecomputeGlobalAttributes = true;
     collectViewAttributes();
     res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
     getHostVisibility(), mDisplay.getDisplayId(),
     mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
     mAttachInfo.mOutsets, mInputChannel);
    } catch (RemoteException e) {
     mAdded = false;
     mView = null;
     mAttachInfo.mRootView = null;
     mInputChannel = null;
     mFallbackEventHandler.setView(null);
     unscheduleTraversals();
     setAccessibilityFocus(null, null);
     throw new RuntimeException("Adding window failed", e);
    }
    

    上面代码中,mWindowSession 类型是 IWindowSession,是一个 Binder 对象,真正的实现类是 Session,也就是说 WIndow 的添加过程是一个 IPC 调用。

    在 Session 内部会通过 WindowManagerService 实现 Window 的添加:

    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) {
     return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }
    

    如此,Window 的添加请求就交给 WindowManagerService 去处理了,在 WindowManagerService 中会为每一个应用保留一个单独的 Session,具体不再分析。
    (原文分析到此,认为到此添加流程已经很清晰,再深入 WindowManagerSercice 也只是一系列代码细节)

2.2 Window 的删除过程

Window 的删除过程和添加过程初始阶段类似,直接分析 WindowManagerGlobal 的 removeView 方法:

public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        View curView = mRoots.get(index).getView();
        removeViewLocked(index, immediate);
        if (curView == view) {
            return;
        }

        throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView);
    }
}

逻辑很清晰,首先通过 findViewLocked 方法查找待删除的 View 的索引,然后调用 removeViewLocked 来做进一步删除:

private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();

    if (view != null) {
        InputMethodManager imm = InputMethodManager.getInstance();
        if (imm != null) {
            imm.windowDismissed(mViews.get(index).getWindowToken());
        }
    }
    boolean deferred = root.die(immediate);
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

removeViewLocked 是通过 ViewRootImpl 来完成删除操作的,调用了其 die 方法。

上面代码中的 immediate 参数需要注意,该参数对应了 WindowManager 中的两种删除接口 removeView(immediate 为 false) 和 removeViewImmediate(immediate 为 true),分别表示异步删除和同步删除。

boolean die(boolean immediate) {
    // Make sure we do execute immediately if we are in the middle of a traversal or the damage
    // done by dispatchDetachedFromWindow will cause havoc on return.
    if (immediate && !mIsInTraversal) {
        doDie();
        return false;
    }
    if (!mIsDrawing) {
        destroyHardwareRenderer();
    } else {
        Log.e(mTag, "Attempting to destroy the window while drawing!\n" + "  window=" + this + ", title=" + mWindowAttributes.getTitle());
    }
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

die 方法内部做了简单判断:

  • 如果是同步删除,那么直接调用 doDie 方法;
  • 如果是异步删除,则会发送一个 MSG_DIE 的请求删除消息,ViewRootImpl 中的 Handler 会处理此消息并调用 doDie 方法;在此过程中,由于 View 尚未完成删除操作,因此在上面 removeViewLocked 方法末尾部分代码中,会将其添加到 mDyingViews 中。

在 doDie 方法内部会调用 dispatchDetachedFromWindow 方法,在其中实现真正删除 View 的逻辑,该方法中主要做 四件事:

  1. 垃圾回收相关的工作,比如清楚数据和消息、移除回调
  2. 通过 Session 的 remove 方法删除Window,同样是一个 IPC 过程,最终会调用 WindowManagerService 的 removeView 方法
  3. 调用 View 的 dispatchDetachedFromWindow 方法,内部会调用 View 的 onDetachedFromWindow(View 从 Window 中移除时的回调,做终止动画、停止线程等一系列资源回收工作) 以及 onDetachedFromWindowInternal。
  4. 调用 WindowManagerGloabl 的 doRemoveView 方法刷新数据,包括 mRoots、mParams 以及 mDyingViews,需要将当前 Window 关联的这三类对象从列表中删除。

2.3 Window 的更新过程

直接看 WindowManagerGlobal 的 updateViewLayout 方法:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
}
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be LayoutParams");
    }
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    view.setLayoutParams(wparams);
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

这个过程就比较简单:

  • 首先更新 View 的 LayoutParams 并替换旧的 LayoutParams
  • 更新 ViewRootImpl 的 LayoutParams
  • ViewRootImpl 会通过 schedulTraversals 方法对 View 重新布局,包括 测量、布局、重绘三个过程
  • ViewRootImpl 还会通过 WindowSession 更新 Window 的视图,这个过程最终由 WindowManagerService 的 relayoutWindow 具体实现,同样是一个 IPC 过程

3.Window 的创建过程

3.1 Activity 的 Window 的创建过程

首先应该了解 Activity 的启动过程,这一部分留待第九章再说。

简单来说,最终会由 ActivityThread 的 performLaunchActivity 来完成整个启动过程,此方法中会通过类加载器创建 Activity 的实例对象,并调用 attach 方法为其关联运行过程中所依赖的一系列上下文环境对象。

在 Activity 的 attach 方法里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口,Window 的对象创建由 PolicyManager 的 makeNewWindow 方法实现。同时当 Window 接收到外界的状态改变时就会通过 Callback 接口,回调 Activity 的方法。

关于 Window 的创建:

  • Activity 的 Window 通过 PolicyManager 的工厂方法创建,PolicyManager 是一个契约类,实现了 IPolicy 策略接口,该接口中定义了众多工厂方法。
  • PolicyManager 的真正实现是 Policy 类,在 makeNewWindow 方法中,返回了 PhoneWindow 对象,由此得出 Window 的具体实现是 PhoneWindow。

关于 Activity 的视图如何附属到 Window:

Activity 的 setContentView 方法交由 Window 处理,直接分析 PhoneWindow 的 setContentView 即可

PhoneWindow 的 setContentView 方法遵循以下几个步骤

  1. 如果没有 DecorView,那么创建之。

    1. 通过 installDecor 方法内部的 generateDecor 直接创建 DecorView
    2. 通过 generateLayout 方法加载具体的布局文件到 DecorView
  1. 将 View 添加到 DecorView 的 mContentParent 中

    到此步,Activity 的布局文件已经添加到 DecorView 里面。

  1. 回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
    Activity 的 onContentChanged 方法是个空实现,可以在 子Activity 中处理该回调。

但是!!

经过上面的三个步骤,DecorView 尚未被 WindowManager 正式添加到 Window。由于此时 DecorView 未被 WindowManager 识别,所以这个时候的 Window 无法提供具体功能,因为它还无法接收外界的输入信息。

在 ActivityThread 的 handleResumeActivity 方法中,首先调用 Activity 的 onResume 方法,接着调用 Activity 的 makeVisible(),在 makeVisible 方法中,DecorView 真正完成添加和显式这两个过程,此时 Activity 的视图才能被用户看到,如下所示:

void makeVisible() {
    if(!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAtrributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

3.2 Dialog 的 Window 的创建过程

Dialog 的 Window 创建过程和 Activity 类似,有如下几个步骤:

1. 创建 Window
2. 初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
3. 将DecorView 添加到 Window 中显示

​ 在 Dialog 的 show 方法中,通过 WindowManager 的 addView 方法将 DecorView 添加到 Window 中。

Dialog 的 Window 创建和 Activity 的 Window 创建过程很类似,几乎没有什么区别;当 Dialog 被关闭时,会通过 WindowManager 的 removeViewImmediate(mDecor) 移除 DecorView。

需要注意

  • 普通 Dialog 必须采用 Activity 的 Context,如果采用 Application 的 Context 会报错,因为需要 应用token,而只有 Activity 拥有
  • 系统 Window 比较特殊,不需要 token,所以也可以采用 Application 的 Context 的同时,指定对话框的 Window 为系统类型即可正常弹出对话框

3.3 Toast 的 Window 的创建过程

首先 Toast 也是基于 Window 实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。

Toast 内部有两类 IPC 过程:

  1. Toast 访问 NotificationManagerService(NMS)
  2. NMS 回调 Toast 里的 TN 接口

Toast 提供 show 和 cancel 分别用于显示和隐藏 Toast,二者内部都是一个 IPC 过程:

public void show() {
    if (mNextView == null) {
    throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

public void cancel() {
    mTN.cancel();
}

显示和隐藏都需要 NMS 实现,而 NMS 无法运行在系统的进程中,所以需要通过远程调用的方式。

关于 TN 这个类,它是一个 Binder 类,在 Toast 和 NMS 进行 IPC 的过程中,当 NMS 处理 Toast 的显示或隐藏请求时会跨进程回调 TN 中的方法,此时由于 TN 运行在 Binder 线程池中,所以需要通过 Handler 将其切换到当前线程中。同时这也意味着 Toast 无法在没有 Looper 的线程中弹出。

首先分析 Toast 的显示过程( show 方法)

  • 调用了 NMS 中的 enqueueToast 方法

  • enqueueToast 首先将 Toast 请求封装为 ToastRecord 对象并将其添加到 mToastQueue 队列中。

    mToastQueue 是一个 ArrayList,对非系统应用而言,mToastQueue 最多同时存在 50 个 ToastRecord,此举是为了防止 DOS 拒绝服务攻击,避免其他应用没有机会弹出 Toast

  • 添加后,NMS 通过 showNextToastLocked 方法显示当前 Toast。

    void showNextToastLocked() {
      ToastRecord record = mToastQueue.get(0);
      while (record != null) {
          if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
          try {
              record.callback.show(record.token);
              scheduleTimeoutLocked(record);
              return;
          } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
          }
      }
    }
    

    上面的代码很简单,Toast 的显示通过 ToastRecord 的 callback 来完成,这个callback 实际上就是 Toast 的 TN 对象的远程 Binder,通过 callback 来访问 TN 中的方法是需要跨进程来完成的,最终被调用的 TN 中的方法会运行在发起 Toast 请求的应用的 Binder 线程池中。

  • Toast 显示之后,NMS 会通过 scheduleTimeoutLocked 方法发送延时消息,具体延时取决于 Toast 的时长:LONG_DELAY 是 3.5s,SHORT_DELAY 是 2s

  • private void scheduleTimeoutLocked(ToastRecord r)
    {
      mHandler.removeCallbacksAndMessages(r);
      Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
      long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
      mHandler.sendMessageDelayed(m, delay);
    }
    

    延时时间过后,NMS 通过 cancelToastLocked 方法隐藏 Toast 并将其从 mToastQueue 中移除,此时如果 mToastQueue 中还有其他 Toast,那么 NMS 就继续显示其他 Toast。

    void cancelToastLocked(int index) {
      ToastRecord record = mToastQueue.get(index);
      try {
          record.callback.hide();
      } catch (RemoteException e) {
      Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg);
      // don't worry about this, we're about to remove it from
      // the list anyway
      }
    
        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
        keepProcessAliveIfNeededLocked(record.pid);
      if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
         showNextToastLocked();
      }   
    }
    

再分析 Toast 的隐藏过程(cancel 方法)

  • 同样是通过 ToastRecord 的 callback 完成的,同样是一次 IPC 过程,其工作过程和 Toast 的显示过程是类似的

    try {
        record.callback.hide();
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg);
        // don't worry about this, we're about to remove it from
        // the list anyway
    }
    

再看 TN 类

通过上面的分析,Toast 的显示和隐藏实际上是通过 Toast 的 TN 这个类来实现的,它有两个方法 show 和 hide,分别对应 Toast 的显示和隐藏。这两个方法运行在 Binder 线程池中,内部使用了 Handler。

/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
    mHandler.obtainMessage(HIDE).sendToTarget();
}

上面代码中,分别发送了 SHOW 和 HIDE 两种消息,在 handleMessage 中分别调用 handleShow 和 handleHide 方法,这二者才是真正完成显示和隐藏 Toast 的地方。

  • handleShow 中 addView 方法将 Toast 的视图添加到 Window 中
  • handleHide 中调用 remoteView 方法将 Toast 的视图从 Window 中移除

到这里 Toast 地 Window 的创建过程就分析完了。


本章结束。

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

推荐阅读更多精彩内容