【Android】Window和WindowManager那些事儿

每一个Activty都包含一个Window对象,Window作为抽象类,具体的处理逻辑都在其子类PhoneWindow中来处理,PhoneWindow将DecorView设置为应用窗口的根View,DecorView继承自FrameLayout,是最顶层视图,DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。


盗张图.png

下面我们先插播一段setContentView的流程,有利于后面对于Window的理解。

1.setContentView的流程

我们在Activity的onCreate里添加布局文件setContentView时,其实在DecorView中默认会添加一个id为content的根布局,我们setContentView的layout正是放在这个布局里面的,下面来分析一下setContentView的流程,我们从这里开始:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

查看setContentView源码:

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

我们发现getDelegate()方法,它是AppCompatDelegate类型,类似代理的作用,看下getDelegate()的源码:

    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

点击create继续追踪,最终在AppCompatDelegateImplV9类中找到了setContentView的具体实现逻辑:

    public void setContentView(View v) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        contentParent.addView(v);
        mOriginalWindowCallback.onContentChanged();
    }

表面上看着代码量不大,咱们一行一行来看,首先看这个ensureSubDecor()方法,精简版源码如下:

    private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
        }
    }

继续追踪createSubDecor()方法,源码如下:

private ViewGroup createSubDecor() {
        //获取Activity的主题
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        //进行一系列的主题匹配,确定主题类型
        if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
            requestWindowFeature(Window.FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
            requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
        }
        mWindow.getDecorView();
        //拿到inflater,后面要用
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
        //如果设置的主题是有title的,进入判断逻辑
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                //mIsFloating表示类似于Dialog这种类型的窗口,inflater对应的系统默认布局
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);
                mHasActionBar = mOverlayActionBar = false;
            } else if (mHasActionBar) {
                TypedValue outValue = new TypedValue();
                mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
                //mHasActionBar表示有Actionbar的主题,inflater对应的系统默认布局
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
                //根据ID拿到根布局的View
                mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
                mDecorContentParent.setWindowCallback(getWindowCallback());
            }
        } else {
            if (mOverlayActionMode) {
                //Action mode相关
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_screen_simple_overlay_action_mode, null);
            } else {
                //表示no title的情况,进入判断逻辑
                subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
            }
        }
        if (mDecorContentParent == null) {
            mTitleView = (TextView) subDecor.findViewById(R.id.title);
        }
        //取出ContentFrameLayout也就是一个FrameLayout布局
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
        //取出PhoneWindow中id为content的ViewGroup,看后面的代码是把此ViewGroup的子View取出放在contentView中,然后把contentView设置id为android.R.id.content。
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
        }
        //最后把布局subDecor与PhoneWindow绑定
        mWindow.setContentView(subDecor);
        return subDecor;
    }

具体逻辑代码中的注释,绕了一大圈,最终调用了mWindow.setContentView(subDecor),进入PhoneWindow的setContentView方法:

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

继续追踪:

@Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        
        if (mContentParent == null) {
            //创建DecorView,并给mContentParent赋值
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            //把咱们的layout布局add进mContentParent里
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        //布局添加完成,回调onContentChanged方法
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

有人可能会对mContentParent这个对象有疑问,从上面的代码可以看出mContentParent是在installDecor()方法中赋值的,我们来看下这个方法:

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //是在这里进行赋值的
            mContentParent = generateLayout(mDecor);
        }
}

接着看下generateLayout()方法:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

protected ViewGroup generateLayout(DecorView decor) {
        //省略部分代码
        //注意看这句,通过R.id.content拿到了根布局的ViewGroup
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
}

至此,咱们在onCreate中通过setContentView的布局view,通过mContentParent.addView(view, params)添加到页面中。

2. WindowManager和Window的关系

public interface WindowManager extends ViewManager {

      //一些异常定义
      public static class BadTokenException extends RuntimeException {···}
      public static class InvalidDisplayException extends RuntimeException {···}

      //该方法会获得这个WindowManager实例将Window添加到哪个屏幕上了,换句话说,就是得到WindowManager所管理的屏幕(Display)
      public Display getDefaultDisplay();
      //移除View
      public void removeViewImmediate(View view);

      public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
            //定义了一些window相关的属性,包括位置坐标,层级,类型等等。
            public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
            public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
            ·········
      }
}

WindowManager是一个接口,它的作用就是管理窗口,从代码可以看出,它继承ViewManager,我们看下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);
}

哎呀妈呀,这种代码看着真舒服,因为短啊,关于view的增删改三个方法,一目了然,它的实现类为WindowManagerImpl。如果我们想要对Window进行添加和删除等操作,就要使用WindowManager了,具体的工作都是由WindowManagerService(WMS)来处理的,WindowManager和WMS通过Binder来进行跨进程通信。

知道了Window和windowManager,那么这俩货是在什么时候创建的呢?

不用多想,肯定和Activity有关,因为window从属于Activity,关于Activity的启动流程,相信大家都见过下面这张图,虽然android版本一直在升级,但整体流程依然变化不大:


图片来源于网络

从startActivity开始,经过一系列的调用流程,最终会调用ActivityThread类的performLaunchActivity方法,performLaunchActivity又会调用attach方法,我们重点来看下attach方法:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);
        //创建window对象PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ······

        //window和WindowManager建立连接,
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
    }

如果继续追踪会发现,WindowManager最终会调用mContext.getSystemService(Context.WINDOW_SERVICE)来获取,Context.WINDOW_SERVICE其实就是个String常量,类似key的作用:

public static final String WINDOW_SERVICE = "window";

Context的子类ContextImpl中getSystemService方法如下:

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

    @Override
    public String getSystemServiceName(Class<?> serviceClass) {
        return SystemServiceRegistry.getSystemServiceName(serviceClass);
    }

又调用了SystemServiceRegistry的getSystemService方法:

    /**
     * Gets a system service from a given context.
     */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

发现最终取值是从SYSTEM_SERVICE_FETCHERS中获取的,而SYSTEM_SERVICE_FETCHERS其实就是个Map集合:

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();

其实这里的逻辑是SystemServiceRegistry在初始化的时候会注册系统各种服务,通过registerService方法,并把注册的服务put到map中,以供后续程序使用时获取:

private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

WindowManager注册的代码如下:

registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx);
            }});

所以我们最终获取到的对象就是WindowManagerImpl对象,那么我们此时回到如下这个地方,可以看到此处会把WindowManagerImpl强转为WindowManager:

mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

查看Window的setWindowManager方法如下:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ·····省略代码
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

会调用WindowManagerImpl的createLocalWindowManager方法去创建mWindowManager,而此处WindowManagerImpl持有了Window的引用,也就具备了操作window的能力:

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

最后我们来看下WindowManagerImpl的部分代码:

public final class WindowManagerImpl implements WindowManager {
    //WindowManagerGlobal单例类
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    //持有window对象
    private final Window mParentWindow;

    public WindowManagerImpl(Context context) {
        this(context, null);
    }

    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

    @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);
    }

    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

    @Override
    public Display getDefaultDisplay() {
        return mContext.getDisplay();
    }
}

WindowManagerImpl代码其实很少,我们发现它的大部分功能都委托给了WindowManagerGlobal来处理,WindowManagerGlobal是一个单例类,说明在一个进程中只有一个WindowManagerGlobal实例。但是WindowManagerImpl可能会实现多个Window,也就是说在一个进程中WindowManagerImpl可能会有多个实例。
上一张UML关系图:


window-UML.png

3.Window的添加过程

从上面我们可以知道,最终在addView的时候是调用的WindowManagerGlobal的addView方法,我们来下它的代码:

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {

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

        try {
            //ViewRootImpl的setview方法开始绘制view
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
        }
    }
}

这里又出现了一个新类ViewRootImpl,ViewRootImpl是干嘛的呢,点开后我们发现这么一段描述:The top of a view hierarchy, implementing the needed protocol between View
and the WindowManager. 意思就是它是ViewManager和View之间重要的协议,它实现了WindowManagerGlobal中大部分的功能,也就是说WindowManagerGlobal的大部分功能都放在了ViewRootImpl中进行实现。
下面来看下ViewRootImpl的setView方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                requestLayout();
                try {
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                } 
    }

这里我们只列出了关键代码,requestLayout()表示添加Window之前要进行一次layout布局过程,以确保在收到任何系统事件之后重新布局,这个方法是具体绘制view的地方:

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //检查线程
            checkThread();
            mLayoutRequested = true;
            //绘制
            scheduleTraversals();
        }
    }

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

在requestLayout()方法中调用了checkThread()方法,这段异常提示大家应该很熟悉吧,检查当前线程是否一致,我们平时在子线程更新UI就会抛出这个异常,因为一般在子线程操作UI都会调用到view.invalidate,而View的重绘会触发ViewRootImpl的requestLayout,就会去判断当前线程。

接着调用了scheduleTraversals()方法,在scheduleTraversals()方法中会去执行TraversalRunnable任务:

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

然后调用了doTraversal()-->performTraversals(),真正调用绘制的就是performTraversals()这个方法了,通过measure,layout,draw三个过程最终把View绘制出来,所以View的绘制是由ViewRootImpl完成,另外当手动调用invalidate,postInvalidate,requestInvalidate也会最终调用performTraversals,来重新绘制View:

private void performTraversals() {  
        ······
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        performDraw();
        ······
    }  

requestLayout()之后会调用mWindowSession的addToDisplay方法,mWindowSession是IWindowSession类型,它的实现类是Session类:

    @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);
    }

IWindowSession文件其实是个adil文件,core/java/android/view/IWindowSession.aidl,由此可见,addToDisplay是一次IPC的过程,上面的代码最终调用了WindowManagerService的addView方法,最终远程调用把window添加上去。

final WindowManagerPolicy mPolicy = new PhoneWindowManager();
public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
        int[] appOp = new int[1];
        //mPolicy实际上是一个PhoneWindowManager类型,在checkAddPermission方法中,首先判断窗口类型是否是系统级别的
        //如果不是系统级别的窗口,则返回一个ADD_OKAY,否则需要SYSTEM_ALERT_WINDOW或者INTERNAL_SYSTEM_WINDOW权限
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }

            synchronized(mWindowMap) {
            boolean addToken = false;
            WindowToken token = mTokenMap.get(attrs.token);
            if (token == null) {
                //如果窗口是子窗口
                if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
         //如果是输入法窗口
                if (type == TYPE_INPUT_METHOD) {
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
         //构造WindowToken对象
                token = new WindowToken(this, attrs.token, -1, false);
                addToken = true;
            } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
         // 获取应用的AppWindowToken
                AppWindowToken atoken = token.appWindowToken;
                if (atoken == null) {
                    return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
                } else if (atoken.removed) {
                    return WindowManagerGlobal.ADD_APP_EXITING;
                }
                if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
                    if (localLOGV) Slog.v(
                            TAG, "**** NO NEED TO START: " + attrs.getTitle());
                    return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
                }
            } else if (type == TYPE_INPUT_METHOD) {
                if (token.windowType != TYPE_INPUT_METHOD) {
                    //如果是输入法窗口,token的windowType必须是ADD_BAD_APP_TOKEN类型
                      return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } 
            ....
           // 在窗口的有效性检查完成之后,为当前窗口创建一个WindowState对象,来维护窗口的状态以及根据适当的机制来调整窗口的状态
           WindowState win = new WindowState(this, session, client, token,
                    attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
           // 如果客户端已经被销毁
            if (win.mDeathRecipient == null) {
                return WindowManagerGlobal.ADD_APP_EXITING;
            }
                  
            if (outInputChannel != null && (attrs.inputFeatures
                    & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
          //如果输出Channel的读通道为空,则创建通道
                String name = win.makeInputChannelName();
                InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
                win.setInputChannel(inputChannels[0]);
                inputChannels[1].transferTo(outInputChannel);
          //向InputManager中注册通道,以便当前窗口可以接收到事件
                mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
            }
        }
}

下面一张图来总结一下WindowManager添加View的调用流程:

调用流程.png

4.Window层级

我们都知道,在做悬浮窗的时候,有时候可能并不会只显示一个或者在和SurfaceView同时使用的时候,都会涉及到Window的层级问题,因为Window是由WindowManager来管理的,所以层级相关自然是在WindowManager中定义的。

WindowManager中LayoutParams.type类型决定了这个View显示窗口的类型,不同类型显示的窗口层级(z轴)是不一样的。大方面来讲可以分为应用窗口(APPLICATION_WINDOW)、子窗口(SUB_WINDOW)、系统窗口(SYSTEM_WINDOW)三种类型,应用窗口z轴范围是1~99,子窗口的范围是1001~1999,系统窗口是(2000~2999),所以要实现浮动窗口我们只能在系统窗口范围中实现,window层级大小如下图:


图片来自网络.png

参考博文:
https://blog.csdn.net/mockingbirds/article/details/53152426
https://www.jianshu.com/p/9da7bfe18374

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

推荐阅读更多精彩内容