Android Framework-Activity,Dialog,Toast-添加window流程

一、概论

    通过上一篇文章(Window & WindowManager理解)中已经知道了View 不能单独存在,必须依附在 Window 上面,因此有视图的地方就有 Window。这些视图包括 :ActivityDialogToastPopupWindow 等等。我们通过这篇文章深入理解这几个东西是如何添加window

二、Activity 添加 Window 流程分析

2.1 创建对应Window & 回调

    在Activity中的attach()方法中,系统会创建 Activity 所属的 Window,并未其设置回调接口。由于 Activity 实现了 WindowCallback 接口,因此当 Window 接受到外接的状态改变时就会回调 Activity中的方法。

##Activity

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, IBinder assistToken,
            IBinder shareableActivityToken) {

    //创建 PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    //设置 window 的回调
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }

这个Callback 中的方法有很多,但是有些我们是非常熟悉的,例如

  • dispatchTouchEvent(MotionEvent event)
  • onAttachedToWindow() 等等。
2.2 创建Activity

Activity 的创建过程比较复杂,最终会通过 ActivityThread 中的 performLaunchActivity() 来完成整个启动过程,在这个方法中会通过类加载器创建 Activity 的实例对象,并调用其 attach 方法为其关联所需的环境变量(看以下源码):

##ActivityThread

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //.....
        if (activity != null) {
            
            appContext.setOuterContext(activity);
            // 为activity对象绑定window所需环境变量
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback,
                    r.assistToken);

            //....
        }
    return activity;
}
2.3 初始化Activity所属DecorView

    由于Activity 的视图是通过 setContentView方法提供的,我们直接看 setContentView 即可:

##Activity
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

看代码知道,调用到了getWindowsetContentView方法,而在 Android中Window的实现是 PhoneWindow。因此我们看到 PhoneWindowsetContentView方法。

##PhoneWindow

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            // 1,创建 DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
               //2 添加 activity 的布局文件
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            // 3.通知Activity onContentChanged 。
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

在上面代码中,如果没有 DecorView 就创建它,一般来说它内部包含了标题栏内容栏,但是这个会随着主题的改变而发生改变。但是不管怎么样,内容栏是一定存在的,并且内容栏有固定的 id content,完整的 id 是 android.R.id.content

  • 注释1:通过 generateDecor创建了 DecorView,接着会调用generateLayout 来加载具体的布局文件到DecorView 中,这个要加载的布局就和系统版本以及定义的主题有关了。加载完之后就会将内容区域的 View返回出来,也就是 mContentParent如下源码:
private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // DecorView 为null 就创建
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }

  if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
          //.....
}

protected DecorView generateDecor(int featureId) {
        //.....
        return new DecorView(context, featureId, this, getAttributes());
    }

protected ViewGroup generateLayout(DecorView decor) {
      //.......
      return contentParent;
}

紧接着上面的PhoneWindowsetContentView

  • 注释2:将 activity 需要显示的布局添加到 mContentParent 中。
  • 注释3:由于 activity 实现了 windowcallback 接口,这里表示activity 的布局文件已经被添加到 decorViewmParentView 中了,于是通知 ActivityonContentChanged接口。

经过上面三个步骤,DecorView 已经初始完成,Activity 的布局文件以及加载到了DecorViewmParentView中了,但是这个时候DecorView还没有被 WindowManager 正式添加到Window 中。

2.4 添加DecorView到Window

ActivityThreadhandleResumeActivity中,会调用 activityonResume 方法,接着就会将 DecorView 添加到 Window

##ActivityThread 

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
       String reason) {
   //..... 
   
   //调用 activity 的 onResume 方法
   final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
   
   final Activity a = r.activity;

   if (r.window == null && !a.mFinished && willBeVisible) {
       r.window = r.activity.getWindow();
       View decor = r.window.getDecorView();
       decor.setVisibility(View.INVISIBLE);
       ViewManager wm = a.getWindowManager();
       WindowManager.LayoutParams l = r.window.getAttributes();
       a.mDecor = decor;
       l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
       l.softInputMode |= forwardBit;
       if (r.mPreserveWindow) {
           a.mWindowAdded = true;
           r.mPreserveWindow = false;
           ViewRootImpl impl = decor.getViewRootImpl();
           if (impl != null) {
               impl.notifyChildRebuilt();
           }
       }
       if (a.mVisibleFromClient) {
           if (!a.mWindowAdded) {
               a.mWindowAdded = true;
               //DecorView 完成了添加和显示的过程
               wm.addView(decor, l);
           } else {
               a.onWindowAttributesChanged(l);
           }
       }
   } else if (!willBeVisible) {
       if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
       r.hideForNow = true;
   }
   //..........
}

看到上面的 wm.addView(decor, l);就到了上一篇文章的windowManageraddView 流程了。

三、Dialog 添加 Window 流程分析

3.1 创建Window

Dialog 中创建Window是在其构造方法中完成,具体如下:

Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
        boolean createContextThemeWrapper) {
    //...
    //获取 WindowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //创建 Window
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    //设置 Callback
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}
3.2 为DecorView添加视图

初始化DecorView,将 Dialog的视图添加到 DecorView 中

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

这个和 activity 的类似,都是通过 Window 去添加指定的布局文件

3.3 添加DecorView到Window 显示
public void show() {
    //...
    mDecor = mWindow.getDecorView();
    mWindowManager.addView(mDecor, l);
    //发送回调消息
    sendShowMessage();
}

private static final class ListenersHandler extends Handler {
        private final WeakReference<DialogInterface> mDialog;

        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<>(dialog);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DISMISS:
                    ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                    break;
                case CANCEL:
                    ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                    break;
                case SHOW:
                    ((OnShowListener) msg.obj).onShow(mDialog.get());
                    break;
            }
        }
    }

从上面三个步骤可以发现,DialogWindow 创建和ActivityWindow创建很类似,二者几乎没有什么区别。
dialog关闭时,它会通过WindowManager来移除DecorViewmWindowManager.removeViewImmediate(mDecor)。具体看下面源码:

    @Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }

void dismissDialog() {
        if (mDecor == null || !mShowing) {
            return;
        }

        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            // 通过 windowManager 进行移除
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;

            sendDismissMessage();
        }

普通的Dialog有一个特殊的地方,就是必须采用Activity 的 Context,如果采用Application 的 Context,就会报错:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

错误信息很明确,是没有Token导致的,而Token一般只有 Activity 拥有,所以这里只需要用 Activity作为Context 即可。
另外,系统 Window比较特殊,他可以不需要Token,我们可以将Dialog 的 Window Type修改为系统类型就可以了,如下所示:

val dialog = Dialog(application)
    dialog.setContentView(textView)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
    }else{
        dialog.window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
    }
dialog.show()

需要注意的是,弹出系统级别的弹框需要申请 悬浮窗权限

四、Toast 添加 Window 流程分析

3.1 概论

Toast 也是基于Window来实现的,但是他的工作过程有些复杂。在Toast的内部有两类 IPC的过程,第一类是 Toast访问NotificationManagerService过程。第二类是NotificationManagerServer 回调 Toast里的TN接口。下面将NotificationManagerService简称为NMS
Toast 属于系统Window,内部视图有两种定义方式,一种是系统默认的,另一种是通过 setView 方法来指定一个 View(setView 方法在 android 11 以后已经废弃了,不会再展示自定义视图),他们都对应 Toast 的一个内部成员mNextView

3.2 展示show()

Toast提供了 showcancel分别用于显示和隐藏 Toast,它们的内部是一个IPC的过程,实现如下:

public void show() {
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();
    try {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            if (mNextView != null) {
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            } else {
                // ...
            }
        }
    } 
    //....
}
public void cancel() {

    try {
        getService().cancelToast(mContext.getOpPackageName(), mToken);
    } catch (RemoteException e) {
        // Empty
    }
    //....
}

static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(
                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
        return sService;
    }

从上面代码中可以看出,显示和影藏都需要通过NMS来实现,由于NMS运行在系统进程中,所以只通过能跨进程的调用方式来显示和隐藏Toast

首先看 Toast显示的过程,它调用了NMS 中的 enqueueToast 方法,上面的INotificationManager只是一个AIDL接口, 这个接口使用来和 NMS进行通信的,实际调用到的是NMSenqueueToast方法:

##NotificationManagerService

static final int MAX_PACKAGE_TOASTS = 5;

public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
        int duration, int displayId) {
    enqueueToast(pkg, token, null, callback, duration, displayId, null);
}

private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {

    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        final long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, token);
            //如果队列中有,就更新它,而不是重新排在末尾
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                int count = 0;
                final int N = mToastQueue.size();
                for (int i = 0; i < N; i++) {
                    final ToastRecord r = mToastQueue.get(i);
                    //对于同一个应用,taost 不能超过 5 个
                    if (r.pkg.equals(pkg)) {
                        count++;
                        if (count >= MAX_PACKAGE_TOASTS) {
                            Slog.e(TAG, "Package has already queued " + count
                                   + " toasts. Not showing more. Package=" + pkg);
                            return;
                        }
                    }
                }

                //创建对应的 ToastRecord
                record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,text, callback, duration, windowToken, displayId, textCallback);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveForToastIfNeededLocked(callingPid);
            }
            // ==0 表示只有一个 toast了,直接显示,否则就是还有toast,真在进行显示
            if (index == 0) {
                showNextToastLocked(false);
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

    private ToastRecord getToastRecord(int uid, int pid, String packageName, boolean isSystemToast,
            IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback,
            int duration, Binder windowToken, int displayId,
            @Nullable ITransientNotificationCallback textCallback) {
        if (callback == null) {
            return new TextToastRecord(this, mStatusBar, uid, pid, packageName,
                    isSystemToast, token, text, duration, windowToken, displayId, textCallback);
        } else {
            return new CustomToastRecord(this, uid, pid, packageName,
                    isSystemToast, token, callback, duration, windowToken, displayId);
        }
    }

我们来看一下NMSenqueueToast方法,这个方法中已经属于别的进程了。调用的时候传了 五个参数

  • 第一个表示当前应用的包名
  • 第二个token
  • 第三个 Tn 表示远程回调
  • 也是一个IPC的过程
  • 第四个 时长
  • 第五个是显示的id
    1) 上面代码中对给定应用的toast数量进行判断,如果超过 50条,就直接退出,这是为了防止DOS,如果某个应用一直循环弹出 taost 就会导致其他应用无法弹出,这显然是不合理的。
    2) 判断完成之后,就会创建 ToastRecord,它分为两种,一种是TextToastRecord,还有一种是 CustomToastRecord。由于调用enqueueToast的时候传入了 Tn,所以 getToastRecord 返回的是 CustomToastRecord对象。
    3) 最后判断只有一个 toast ,就调用 showNextToastLocked 显示,否则就是还有好多个 taost 真在显示。
void showNextToastLocked(boolean lastToastWasTextRecord) {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        //...
        if (tryShowToast(
                record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
            scheduleDurationReachedLocked(record, lastToastWasTextRecord);
            mIsCurrentToastShown = true;
            if (rateLimitingEnabled && !isPackageInForeground) {
                mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
            }
            return;
        }

        int index = mToastQueue.indexOf(record);
        if (index >= 0) {
            mToastQueue.remove(index);
        }
        //是否还有剩余的taost需要显示
        record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    }
}

    private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
            boolean isWithinQuota, boolean isPackageInForeground) {
        //.....
        return record.show();
    }

上面代码中最后调用的是record.show()这个record也就是 CustomToastRecord了。

接着我们来看一下他的 show方法:

##CustomToastRecord

public final ITransientNotification callback;

@Override
public boolean show() {
    if (DBG) {
        Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
    }
    try {
        callback.show(windowToken);
        return true;
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "+ pkg);
        mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
        return false;
    }
}

可以看到,调用的是callbackshow方法,这个 callback 就是在CustomToastRecord创建的时候传入的 Tn了。这里回就调到了 Tnshow方法中。

##Toast  #Tn
TN(Context context, String packageName, Binder token, List<Callback> callbacks,
   @Nullable Looper looper) {
    mPresenter = new ToastPresenter(context, accessibilityManager, getService(),packageName);

        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

mHandler = new Handler(looper, null) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW: {
                    IBinder token = (IBinder) msg.obj;
                    handleShow(token);
                    break;
                }
             //.....
            }
        }
    };
}

public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                         + " mNextView=" + mNextView);
    //...
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                        mHorizontalMargin, mVerticalMargin,
                        new CallbackBinder(getCallbacks(), mHandler));
    }
}

由于 show 方法是被NMS跨进程的方式调用的,所以他们运行在Binder线程池中,为了切换到Toast请求所在的线程,这里使用了Handler。通过上面代码,我们可以看出,最终是交给ToastPresenter去处理。

3.3 ToastPresenter最终处理
public class ToastPresenter {
//....
    @VisibleForTesting
    public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;
    /**
     * Returns the default text toast view for message {@code text}.
     */
    public static View getTextToastView(Context context, CharSequence text) {
        View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
        TextView textView = view.findViewById(com.android.internal.R.id.message);
        textView.setText(text);
        return view;
    }

    //....
    public ToastPresenter(Context context, IAccessibilityManager accessibilityManager, INotificationManager notificationManager, String packageName) {
        mContext = context;
        mResources = context.getResources();
        //获取 WindowManager
        mWindowManager = context.getSystemService(WindowManager.class);
        mNotificationManager = notificationManager;
        mPackageName = packageName;
        mAccessibilityManager = accessibilityManager;
        //创建参数
        mParams = createLayoutParams();
    }
    
    private WindowManager.LayoutParams createLayoutParams() {
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = R.style.Animation_Toast;
        params.type = WindowManager.LayoutParams.TYPE_TOAST; //TYPE_TOAST:2005
        params.setFitInsetsIgnoringVisibility(true);
        params.setTitle(WINDOW_TITLE);
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        setShowForAllUsersIfApplicable(params, mPackageName);
        return params;
    }
    
     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback) {
        show(view, token, windowToken, duration, gravity, xOffset, yOffset, horizontalMargin,
                verticalMargin, callback, false /* removeWindowAnimations */);
    }
    
     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,int xOffset, int yOffset, float horizontalMargin, float verticalMargin,@Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
    //.....
        addToastView();
        trySendAccessibilityEvent(mView, mPackageName);
        if (callback != null) {
            try {
                //回调
                callback.onToastShown();
            } catch (RemoteException e) {
                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
            }
        }
    }
    
    private void addToastView() {
        if (mView.getParent() != null) {
            mWindowManager.removeView(mView);
        }
        try {
            // 将 Toast 视图添加到 Window 中
            mWindowManager.addView(mView, mParams);
        }
    }
}

3.3 Toast 总结:

弹出Toast也是一个IPC的过程,最终通过Handler切换到App对应线程。
使用IPC的原因是:

  • 为了统一管理系统中所有 Toast 的消失与显示.真正显示和消失操作还是在 App 中完成的。

其次,Toast的窗口类型是 TYPE_TOAST,属于系统类型Toast有自己的 token,不受 Activity控制。
Toast通过 WindowManagerview直接添加到了Window中,并没有创建PhoneWindowDecorView,这点和ActivityDialog 不同。
Toast 的添加流程如图:

Toast 流程图

两篇文章的总结是:

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

推荐阅读更多精彩内容