从源码角度解析悬浮窗

今天我们从源码角度来一探 Android 悬浮窗的究竟。

一、如何创建一个悬浮窗

  1. 在 AndroidManifest.xml 中声明权限 SYSTEM_ALERT_WINDOW;
  2. 新建一个 Service, 在其 onCreate() 函数中添加如下代码:
@Override
public void onCreate() {
    super.onCreate();

    // WindowManager
    final WindowManager windowManager =
        (WindowManager) getSystemService(Context.WINDOW_SERVICE);

    // 定义一个按钮
    final Button btn = new Button(getBaseContext());
    btn.setText("Dismiss Me");
    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            wm.removeView(btn);
            stopSelf();
        }
    });

    // 布局参数
    WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    p.gravity = Gravity.TOP;
    p.width = WRAP_CONTENT;
    p.height = WRAP_CONTENT;
    p.x = 0;
    p.y = 0;
    p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    
    // 添加按钮到 WindowManager
    windowManager.addView(btn, p);
}

当我们的 Service 被启动后,手机屏幕顶部就会显示一个按钮,点击此按钮后,它会消失。

s1.png

二、如何定制悬浮窗?

从上述代码片段可知,定制悬浮窗的关键在于 WindowManager.LayoutParams 这个类,
我们来看看几个主要的成员。

  • x, y
    窗口的绝对位置(考虑gravity属性)
  • width, height
    窗口的尺寸
  • gravity
    窗口的布局方式
  • type
    窗口的类型,值越大显示的位置越在上面
// type 共分三大类:

/* --------- Application Window ---------- */
//  id 从 1 到 99, 目前有效值 1~3
//  Activity -> TYPE_APPLICATION(2)(需要 token 设置成 Activity 的 token)

//开始应用程序窗口
public static final int FIRST_APPLICATION_WINDOW = 1;
//所有程序窗口的base窗口,其他应用程序窗口都显示在它上面
public static final int TYPE_BASE_APPLICATION   = 1;
//普通应用程序窗口,token必须设置为Activity的token来指定窗口属于谁
public static final int TYPE_APPLICATION        = 2;
//应用程序启动时所显示的窗口,应用自己不要使用这种类型,它被系统用来显示一些信息,直到应用程序可以开启自己的窗口为止
public static final int TYPE_APPLICATION_STARTING = 3;
//结束应用程序窗口
public static final int LAST_APPLICATION_WINDOW = 99;


/* --------- Sub Window ---------- */
//  id 从 1000 到 1999, 目前有效值 1000~1005
// (与顶层窗口相关联,需将 token 设置成它所附着宿主窗口的 token)
//  PopupWindow -> TYPE_APPLICATION_PANEL(1000)

//SubWindows子窗口,子窗口的Z序和坐标空间都依赖于他们的宿主窗口
public static final int FIRST_SUB_WINDOW = 1000;
// 面板窗口,显示于宿主窗口的上层
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
//媒体窗口(例如视频),显示于宿主窗口下层
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;
//应用程序窗口的子面板,显示于所有面板窗口的上层
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
//对话框,类似于面板窗口,绘制类似于顶层窗口,而不是宿主的子窗口
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
//媒体信息,显示在媒体层和程序窗口之间,需要实现半透明效果
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;
//子窗口结束
public static final int LAST_SUB_WINDOW = 1999;

/* --------- System Window ---------- */
// id 从 2000 到 2999, 目前有效值 2000~2033
// 系统功能使用,不能用于应用程序,使用时需要特殊权限,它是特定的系统功能才能使用;

//系统窗口,非应用程序创建
public static final int FIRST_SYSTEM_WINDOW     = 2000;
//状态栏,只能有一个状态栏,位于屏幕顶端,其他窗口都位于它下方
public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
//搜索栏,只能有一个搜索栏,位于屏幕上方
public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
//电话窗口,它用于电话交互(特别是呼入),置于所有应用程序之上,状态栏之下
public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
//系统提示,出现在应用程序窗口之上
public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
//锁屏窗口
public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
//信息窗口,用于显示Toast
public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
//系统顶层窗口,显示在其他一切内容之上,此窗口不能获得输入焦点,否则影响锁屏
public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
//电话优先,当锁屏时显示,此窗口不能获得输入焦点,否则影响锁屏
public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
//系统对话框
public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
//锁屏时显示的对话框
public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
//系统内部错误提示,显示于所有内容之上
public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
//内部输入法窗口,显示于普通UI之上,应用程序可重新布局以免被此窗口覆盖
public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
//内部输入法对话框,显示于当前输入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
//墙纸窗口
public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
//状态栏的滑动面板
public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
//安全系统覆盖窗口,这些窗户必须不带输入焦点,否则会干扰键盘
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
//拖放伪窗口,只有一个阻力层(最多),它被放置在所有其他窗口上面
public static final int TYPE_DRAG               = FIRST_SYSTEM_WINDOW+16;
//状态栏下拉面板
public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
//鼠标指针
public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
//导航栏(有别于状态栏时)
public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
//音量级别的覆盖对话框,显示当用户更改系统音量大小
public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
//起机进度框,在一切之上
public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
//假窗,消费导航栏隐藏时触摸事件
public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
//梦想(屏保)窗口,略高于键盘
public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
//导航栏面板(不同于状态栏的导航栏)
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
//universe背后真正的窗户
public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
//显示窗口覆盖,用于模拟辅助显示设备
public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
//放大窗口覆盖,用于突出显示的放大部分可访问性放大时启用
public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
//......
public static final int TYPE_KEYGUARD_SCRIM           = FIRST_SYSTEM_WINDOW+29;
public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
//系统窗口结束
public static final int LAST_SYSTEM_WINDOW      = 2999;
  • flags
    flags 和 type 可以共同决定 Window 的表示和行为
// FLAG_ALLOW_LOCK_WHILE_SCREEN_ON:
// 当窗口可见时,允计锁屏界面出现。

// FLAG_DIM_BEHIND:
// 窗口之后的内容变暗

// FLAG_NOT_FOCUSABLE:
// 不能获得按键输入焦点,所以不能向它发送按键或按钮事件。那些事件将发送给它后面的可以获得焦点的窗口。
// 此选项还会设置 FLAG_NOT_TOUCH_MODAL 选项。设置此选项,意味着窗口不能与软输入法进行交互,
// 所以它的 Z 序独立于任何活动的输入法(它可以全屏显示,如果需要的话,可覆盖输入法窗口)。
// 要修改这一行为,可参考 FLAG_ALT_FOCUSALBE_IM 选项。

// FLAG_NOT_TOUCHABLE:
// 不接受触摸屏事件

// FLAG_NOT_TOUCH_MODAL:
// 当窗口可以获得焦点(没有设置 FLAG_NOT_FOCUSALBE 选项)时,仍然将窗口范围之外的点设备事件
//(鼠标、触摸屏)发送给后面的窗口处理。否则它将独占所有的点设备事件,而不管它们是不是发生在窗口范围之内。

// FLAG_TOUCHABLE_WHEN_WAKING:
// 如果设置了这个标志,当设备休眠时,点击触摸屏,设备将收到这个第一触摸事件。
// 通常第一触摸事件被系统所消耗,用户不会看到他们点击屏幕有什么反应。

// FLAG_KEEP_SCREEN_ON
// 当此窗口为用户可见时,保持设备常开,并保持亮度不变。

// FLAG_LAYOUT_IN_SCREEN:
// 窗口占满整个屏幕,忽略周围的装饰边框(例如状态栏)。此窗口需考虑到装饰边框的内容。

// FLAG_LAYOUT_NO_LIMITS:
// 允许窗口扩展到屏幕之外

// FLAG_FULLSCREEN:
// 窗口显示时,隐藏所有的屏幕装饰(例如状态条)。使窗口占用整个显示区域。

// FLAG_FORCE_NOT_FULLSCREEN:
// 此选项将覆盖 FLAG_FULLSCREEN 选项,并强制屏幕装饰(如状态条)弹出。

// FLAG_SECURE:
// 不允许屏幕截图

// FLAG_SCALED:
// 一种特殊模式,布局参数用于指示显示比例

// FLAG_IGNORE_CHEEK_PRESSES:
// 当屏幕有可能贴着脸时,这一选项可防止面颊对屏幕造成误操作

// FLAG_LAYOUT_INSET_DECOR:
// 当请求布局时,你的窗口可能出现在状态栏的上面或下面,从而造成遮挡。
// 当设置这一选项后,窗口管理器将确保窗口内容不会被装饰条(状态栏)盖住。 

// public static final int FLAG_ALT_FOCUSABLE_IM:
// 如果同时设置了FLAG_NOT_FOCUSABLE选项和本选项,窗口将能够与输入法交互,允许输入法窗口覆盖; 
// 如果FLAG_NOT_FOCUSABLE没有设置而设置了本选项,窗口不能与输入法交互,可以覆盖输入法窗口。 


// FLAG_WATCH_OUTSIDE_TOUCH:
// 如果你设置了FLAG_NOT_TOUCH_MODAL,那么当触屏事件发生在窗口之外时,
// 可以通过设置此标志接收到一个MotionEvent.ACTION_OUTSIDE事件。
// 注意,你不会收到完整的down/move/up事件,只有第一次down事件时可以收到ACTION_OUTSIDE。 

// FLAG_SHOW_WHEN_LOCKED:
// 当屏幕锁定时,窗口可以被看到。这使得应用程序窗口优先于锁屏界面。
// 可配合 FLAG_KEEP_SCREEN_ON 选项点亮屏幕并直接显示在锁屏界面之前。
// 可使用FLAG_DISMISS_KEYGUARD选项直接解除非加锁的锁屏状态。此选项只用于最顶层的全屏幕窗口。 

// FLAG_SHOW_WALLPAPER:
// 请求系统墙纸显示在你的窗口后面。窗口必须是半透明的。

// FLAG_TURN_SCREEN_ON:
// 窗口一旦显示出来,系统将点亮屏幕,正如用户唤醒设备那样。

// FLAG_DISMISS_KEYGUARD:
// 解除锁屏。只有锁屏界面不是加密的才能解锁。
// 如果锁屏界面是加密的,那么用户解锁之后才能看到此窗口,除非设置了FLAG_SHOW_WHEN_LOCKED选项。 

// FLAG_SPLIT_TOUCH:

// FLAG_HARDWARE_ACCELERATED:
// 硬件加速

// FLAG_LOCAL_FOCUS_MODE:

// FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS:

示例:

  • 使悬浮窗可以覆盖通知栏
    type = TYPE_SYSTEM_ERROR
    flags = FLAG_LAYOUT_IN_SCREEN
  • 使悬浮窗不遮挡输入法
    FLAG_ALT_FOCUSABLE_IM
  • 使悬浮窗不可点击(可以获得焦点,会拦截 Back, Home 键)
    FLAG_NOT_TOUCHABLE
  • 使悬浮窗不可获得焦点(不拦截 Back, Home 键)
    FLAG_NOT_FOCUSABLE

三、悬浮窗是如何显示到屏幕上的?

我们知道视图是通过 WindowManager 添加到屏幕上的,所以第一个要了解的类便是 WindowManager。

// android.view.WindowManager
public interface WindowManager extends ViewManager {
    // 获取 Display
    public Display getDefaultDisplay();
    // 立即删除 View
    public void removeViewImmediate(View view);
}

我们看到 WindowManager 继承自 ViewManager, 有一个获取 DefaultDisplay 的方法和删除 View 的方法;
再来看看 ViewManager 的代码:

// android.view.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);
}

非常简洁的三个函数:添加视图,更新视图,删除视图。
如果再查看一个 ViewManager 的其它实现类,还会发现 ViewGroup 也实现了这个接口。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {...}

我们再来看看 WindowManager 的实现者是谁?

Log.d("wm", windowManager.getClass().getName());
--------------------------------------------------
D/wm      (28372): android.view.WindowManagerImpl

原来是 WindowManagerImpl,如果有同学觉见得打 Log 的方式太偷懒了,可以阅读这篇文章来了解 getSystemService(...) 的实现原理。

那我们就来看看 WindowManagerImpl 的 addView(...), updateViewLayout(...), removeView(...) 这三个函数吧:

// android.view.WindowManagerImpl
...
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, 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);
}

这几个函数无一例外地将实现都转移给了 WindowManagerGlobal:

// android.view.WindowManagerGlobal
public final class WindowManagerGlobal {

    private WindowManagerGlobal() {
    }

    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
    }
    ...
}

毫无疑问,WindowManagerGlobal 使用了单例,所以一个进程中最多只有一个 WindowManager,所有 WindowManager 实例都是这个 WindowManagerGlobal 实例的代理

到此,我们对 WindowManager 的结构体系便有了一个概括的了解:


diagram.png

四、 WindowManagerGlobal 详解

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<>();
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<>();
    private final ArraySet<View> mDyingViews = new ArraySet<>();

WindowManagerGlobal 有 4 个重要的集合:

  • mView : 根视图集合
  • mRoots : ViewRootImpl 集合
  • mParams : 根视图的布局的集合
  • mDyingViews : 要移除和根视图的集合

我们先来分析 添加、删除、更新 3 种操作对这 4 个集合的影响。

1. 添加视图

// android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
   ...
   final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
   ...
   ViewRootImpl root;
   View panelParentView = null;
   synchronized (mLock) {
       ...
       int index = findViewLocked(view, false);
       if (index >= 0) {
           if (mDyingViews.contains(view)) {
               // Don't wait for MSG_DIE to make it's way through root's queue.
               mRoots.get(index).doDie();
           } else {
               ...
           }
       }
       ...
       root = new ViewRootImpl(view.getContext(), display);
       view.setLayoutParams(wparams);
       mViews.add(view);
       mRoots.add(root);
       mParams.add(wparams);
   }

   try {
       root.setView(view, wparams, panelParentView);
   } catch (RuntimeException e) {
       // BadTokenException or InvalidDisplayException, clean up.
       synchronized (mLock) {
           final int index = findViewLocked(view, false);
           if (index >= 0) {
               removeViewLocked(index, true);
           }
       }
       throw e;
   }
}

添加视图时,做了以下几件事:

  1. 将 view, viewRootImpl, layoutParams 分别添加到对应的集合中;
  2. 将 layoutParams 设置给 view;
  3. 将 view 设置给 viewRootImpl;

2. 更新视图

    // android.view.WindowManagerGlobal
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        ...
        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);
        }
    }

更新视图比较简单:

  1. 设置 layoutParams 给 view;
  2. 更新 mParams 集合中对应的 layoutParams;
  3. 更新 对应 viewRootImpl 的 laoutParams;

3. 删除视图

    // android.view.WindowManagerGlobal
    public void removeView(View view, boolean immediate) {
        ...
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
        ...
        }
    }

我们看到,删除视图最终交给了 removeViewLocked 函数:

    // android.view.WindowManagerGlobal
    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        ...
        // 检查是否需要延迟删除
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

removeViewLocked 函数会调用 ViewRootImpl 的 die() 函数,该函数返回一个 boolean 值,标识是否需要延迟删除?如果需要延迟,则将 view 加入 mDyingViews 这个集合中。

接着我们看看 ViewRootImpl 的 die 函数:

    // android.view.ViewRootImpl
    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 {
            ...
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }
    

    public void handleMessage(Message msg) {
        ...
        case MSG_DIE:
            doDie();
            break;
        ...
    }

我们看到,所谓的延迟删除,其实就是使用了 Handler 消息队列机制,来调用 doDie() 函数;
如果是非延迟删除,则立即调用 doDie() 函数:

    // android.view.ViewRootImpl
    void doDie() {
        // 必须在主线程
        checkThread();
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                // 发送 DetachedFromWindow 消息事件
                dispatchDetachedFromWindow();
            }

            ...
            mAdded = false;
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

doDie() 函数会发送 DetachedFromWindow 消息给 View;然后调用 WindowManagerGlobal 的 doRemoveView() 函数:

    // android.view.WindowManagerGlobal
    void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
        ...
    }

doRemoveView() 函数的功能一目了然,即从 mRoots, mParams, mDyingViews 中将 数据移除掉。

五、总结

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

推荐阅读更多精彩内容