Toast通知欄權(quán)限填坑指南

本文章已授權(quán)鴻洋微信公眾號轉(zhuǎn)載:Toast不顯示了嫂便?

吐司彈不出來完美的解決方案:Toaster躁锡,接下來讓我們來一步步開始分析這個問題是如何出現(xiàn)希停,解決的過程对省,以及解決的方法

首先我們先看一下大廠 APP 的彈吐司

疑問

  • 連吐司彈不出來的手機是個什么梗?

  • 是少部分機型問題還是大多數(shù)機型的問題众辨?

  • 為什么關(guān)閉了通知欄權(quán)限彈不出來豪嚎?

  • 為什么有的機型可以彈有的卻不行?

解答

  • 自從我的 Toaster 框架發(fā)布了之后厢塘,被問最多的一個問題茶没,你的Toast框架關(guān)閉通知欄權(quán)限還能彈出來嗎?我心想這 Toast 跟通知欄扯不上啥關(guān)系吧晚碾,但是既然有人這樣問了礁叔,也只能半信半疑了,于是我便拿了我的小米8還有紅米Note5進行了測試迄薄,發(fā)現(xiàn)并沒有該問題琅关,于是我統(tǒng)一回復(fù),這個是兼容問題讥蔽,極少數(shù)機型才可能出現(xiàn)的問題涣易,為保證框架穩(wěn)定性,不給予兼容

  • 于是還有人陸陸續(xù)續(xù)給我反饋了這個問題冶伞,反饋的人都是用華為機型出現(xiàn)的問題新症,我便開始重視起來,剛好有同事用的是華為 P9响禽,我跟他借了一下手機徒爹,一借不要緊,一借一下午芋类。估計同事的內(nèi)心是崩潰的隆嗅,因為這個問題被 100% 復(fù)現(xiàn)了,真的關(guān)閉通知欄權(quán)限后吐司彈不出來了

  • 于是我翻遍了 Toast 的源碼侯繁,吐司底層是 WindowManager 實現(xiàn)的胖喳,但是這跟通知欄權(quán)限有什么關(guān)系呢?就算有關(guān)系也是和 NotificationManager 有關(guān)系贮竟,到底和通知欄權(quán)限扯上啥關(guān)系了呢丽焊?經(jīng)過查看系統(tǒng)源碼發(fā)現(xiàn)较剃,吐司的創(chuàng)建是使用到了 WindowManager 去創(chuàng)建,但是顯示吐司的時候使用了 INotificationManager 技健,看類名就知道肯定和 NotificationManager 有聯(lián)系写穴,這就是為什么關(guān)閉了通知欄權(quán)限后導(dǎo)致了吐司顯示不出來的問題

  • 現(xiàn)在經(jīng)過測試,大部分小米機型不會因為通知欄權(quán)限被關(guān)閉而原生的Toast彈不出來雌贱,而華為榮耀啊送,三星等都會出現(xiàn)通知欄權(quán)限被關(guān)閉后導(dǎo)致原生Toast顯示不出來,這可能是小米手機對這個吐司的顯示做了特殊處理帽芽,這個問題在Github上排名前幾的Toast框架都會出現(xiàn)删掀,并且一些大廠的APP(除QQ微信和美團外)也會出現(xiàn)該問題

吐司彈不出來的后果

Toast是我們?nèi)粘i_發(fā)中最常用的類翔冀,如果我們的APP在通知欄推送的消息比較多导街,用戶就會把我們的通知欄權(quán)限屏蔽了,但是這個會引起一個連帶反應(yīng)纤子,就是應(yīng)用中所有使用到 Toast 的地方都會顯示不出來搬瑰,徹底成為一個啞巴應(yīng)用,例如以下情景:

  • 賬戶密碼輸入錯誤控硼,吐司彈不出來

  • 用戶網(wǎng)絡(luò)支付失敗泽论,吐司彈不出來

  • 網(wǎng)絡(luò)請求錯誤,吐司彈不出來

  • 雙擊退出應(yīng)用卡乾,吐司彈不出來

  • 等等情況翼悴,只要用到原生 Toast 都顯示不出來

其實這是一個系統(tǒng)的Bug,谷歌為了讓應(yīng)用的 Toast 能夠顯示在其他應(yīng)用上面幔妨,所以使用了通知欄相關(guān)的 API鹦赎,但是這個 API 隨著用戶屏蔽通知欄而變得不可用,系統(tǒng)錯誤地認(rèn)為你沒有通知欄權(quán)限误堡,從而間接導(dǎo)致 Toast 有 show 請求時被系統(tǒng)所攔截

Toast 源碼解析

首先看一下 Toast 的構(gòu)成

再看一下 Toast 內(nèi)部的 API

里面還有一個內(nèi)部類古话,再看一下內(nèi)部的 API

從這里我們不難推斷,Toast 只是一個外觀類锁施,最終實現(xiàn)還是由其內(nèi)部類來實現(xiàn)陪踩,由于這個內(nèi)部類太長,這里放一下這個內(nèi)部類的源碼悉抵,簡單過一遍就好

private static class TN extends ITransientNotification.Stub {
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    private static final int SHOW = 0;
    private static final int HIDE = 1;
    private static final int CANCEL = 2;
    final Handler mHandler;

    int mGravity;
    int mX, mY;
    float mHorizontalMargin;
    float mVerticalMargin;


    View mView;
    View mNextView;
    int mDuration;

    WindowManager mWM;

    String mPackageName;

    static final long SHORT_DURATION_TIMEOUT = 4000;
    static final long LONG_DURATION_TIMEOUT = 7000;

    TN(String packageName, @Nullable Looper looper) {
        // XXX This should be changed to use a Dialog, with a Theme.Toast
        // defined that sets up the layout params appropriately.
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = com.android.internal.R.style.Animation_Toast;
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        params.setTitle("Toast");
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

        mPackageName = packageName;

        if (looper == null) {
            // Use Looper.myLooper() if looper is not specified.
            looper = Looper.myLooper();
            if (looper == null) {
                throw new RuntimeException(
                        "Can't toast on a thread that has not called Looper.prepare()");
            }
        }
        mHandler = new Handler(looper, null) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        IBinder token = (IBinder) msg.obj;
                        handleShow(token);
                        break;
                    }
                    case HIDE: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        break;
                    }
                    case CANCEL: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        try {
                            getService().cancelToast(mPackageName, TN.this);
                        } catch (RemoteException e) {
                        }
                        break;
                    }
                }
            }
        };
    }

    /**
     * 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();
    }

    public void cancel() {
        if (localLOGV) Log.v(TAG, "CANCEL: " + this);
        mHandler.obtainMessage(CANCEL).sendToTarget();
    }

    public void handleShow(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                + " mNextView=" + mNextView);
        // If a cancel/hide is pending - no need to show - at this point
        // the window token is already invalid and no need to do any work.
        if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
            return;
        }
        if (mView != mNextView) {
            // remove the old view if necessary
            handleHide();
            mView = mNextView;
            Context context = mView.getContext().getApplicationContext();
            String packageName = mView.getContext().getOpPackageName();
            if (context == null) {
                context = mView.getContext();
            }
            mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            // We can resolve the Gravity here by using the Locale for getting
            // the layout direction
            final Configuration config = mView.getContext().getResources().getConfiguration();
            final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
            mParams.gravity = gravity;
            if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                mParams.horizontalWeight = 1.0f;
            }
            if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                mParams.verticalWeight = 1.0f;
            }
            mParams.x = mX;
            mParams.y = mY;
            mParams.verticalMargin = mVerticalMargin;
            mParams.horizontalMargin = mHorizontalMargin;
            mParams.packageName = packageName;
            mParams.hideTimeoutMilliseconds = mDuration ==
                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
            mParams.token = windowToken;
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeView(mView);
            }
            if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
            // Since the notification manager service cancels the token right
            // after it notifies us to cancel the toast there is an inherent
            // race and we may attempt to add a window after the token has been
            // invalidated. Let us hedge against that.
            try {
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            } catch (WindowManager.BadTokenException e) {
                /* ignore */
            }
        }
    }

    private void trySendAccessibilityEvent() {
        AccessibilityManager accessibilityManager =
                AccessibilityManager.getInstance(mView.getContext());
        if (!accessibilityManager.isEnabled()) {
            return;
        }
        // treat toasts as notifications since they are used to
        // announce a transient piece of information to the user
        AccessibilityEvent event = AccessibilityEvent.obtain(
                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
        event.setClassName(getClass().getName());
        event.setPackageName(mView.getContext().getPackageName());
        mView.dispatchPopulateAccessibilityEvent(event);
        accessibilityManager.sendAccessibilityEvent(event);
    }

    public void handleHide() {
        if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
        if (mView != null) {
            // note: checking parent() just to make sure the view has
            // been added...  i have seen cases where we get here when
            // the view isn't yet added, so let's try not to crash.
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeViewImmediate(mView);
            }

            mView = null;
        }
    }
}

只需要稍微簡單看一下就看懂肩狂,Toast 底層就是用這個內(nèi)部類去實現(xiàn),請記住姥饰,這個內(nèi)部類叫做 TN婚温,字段名為 mTN,接下來先讓我們看一下 Toast 中 cancel 方法的源碼

cancel最終還是調(diào)用了內(nèi)部類 TN 中的同名方法媳否,接下來再看 Toast 中 show 方法的源碼

仔細(xì)觀察的同學(xué)就會發(fā)現(xiàn)了栅螟,這個 show 的方法可不是像 cancel 一樣只調(diào)用了 TN 內(nèi)部類中的同名方法荆秦,還調(diào)用了 INotificationManager 這個 API,其實不難發(fā)現(xiàn)力图,這個 INotificationManager 是系統(tǒng)的 AIDL步绸,不信的話我們再看一下這個 INotificationManager

我相信學(xué)過 AIDL 的同學(xué)會明白,這里不再講 AIDL 相關(guān)知識吃媒,如需了解請自行百度

重點講一下 INotificationManager瓤介,這個 AIDL 由系統(tǒng)實現(xiàn)的一個類,不同系統(tǒng)這個 AIDL 所對應(yīng)的類也不相同赘那,這就充分說明了為什么導(dǎo)致小米的機型關(guān)閉了通知欄權(quán)限還可以顯示刑桑,而華為就不行的原因,具體原因請再看源碼

因為這里傳了應(yīng)用的包名給系統(tǒng)通知欄募舟,如果這個包名對應(yīng)的APP的通知欄權(quán)限被關(guān)閉了祠斧,吐司自然也就彈不出來了

那么該如何著手解決這個問題

先思考一個問題,Toast 顯示是使用了 INotificationManager拱礁,和通知欄有關(guān)系琢锋,而Toast 的創(chuàng)建是使用了 WindowManager,和通知欄沒有關(guān)系呢灶,那么我們可不可以通過 WindowManager 的方式來創(chuàng)建類似于 Toast 一樣的東西呢吴超,答案也是可以的,只不過在過程中會遇到非常棘手的問題鸯乃,接下來讓我們解決這些遇到的問題

首先創(chuàng)建一個 WindowManager 需要 一個 View 參數(shù)和 WindowManager.LayoutParams 參數(shù)鲸阻,這里說一下 WindowManager.LayoutParams 的創(chuàng)建,直接復(fù)制 Toast 部分代碼

WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
// 找不到 com.android.internal.R.style.Animation_Toast
// params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.windowAnimations = -1;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

然后使用 WindowManager 調(diào)用 addView 顯示缨睡,然后報了錯

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

其原因在于我們使用了 type鸟悴,為什么不能加 TYPE_TOAST,因為通知權(quán)限在關(guān)閉后設(shè)置顯示的類型為Toast會報錯宏蛉,所以這里我們把這句代碼注釋掉遣臼,然后就可以顯示出來了

params.type = WindowManager.LayoutParams.TYPE_TOAST;

WindowManager 沒有吐司的顯示效果

其原因在于我們復(fù)制了 Toast 的部分代碼,而其中的動畫代碼引用了系統(tǒng) R 文件中資源拾并,而我無法直接在 Java 代碼中引用

params.windowAnimations = com.android.internal.R.style.Animation_Toast;

Java代碼不能引用這個Style不代表XML就不行揍堰,在這里創(chuàng)建一個 Style 并且繼承原生 Toast 樣式,這里我們可以自定義嗅义,也可以直接使用系統(tǒng)的屏歹,為了和系統(tǒng)的樣式統(tǒng)一,這里就直接使用系統(tǒng)的

<style name="ToastAnimation" parent="@android:style/Animation.Toast">
    <!--<item name="android:windowEnterAnimation">@anim/toast_enter</item>-->
    <!--<item name="android:windowExitAnimation">@anim/toast_exit</item>-->
</style>

然后重新指定 params.windowAnimations 即可解決該問題

params.windowAnimations = R.style.ToastAnimation;

WindowManager 沒有自動消失的問題

首先 WindowManager 并不能像 Toast 顯示后自動消失之碗,如果要像 Toast 一樣自動消失很容易蝙眶,在 WindowManager 顯示后發(fā)送一個定時關(guān)閉的任務(wù),那么問題來了,這個顯示的時間如何定義幽纷?系統(tǒng) Toast 顯示的時間是什么樣子式塌?首先我們需要先看一下 Toast 給我們提供的兩個常量值

從這張圖上我們并沒有發(fā)現(xiàn)什么有價值的東西,我們繼續(xù)往下找友浸,看看是什么地方引用了這些常量

繼續(xù)通過查看源碼得知

但是通過測試峰尝,短吐司顯示的時長為2-3秒彤灶,而長吐司顯示的時長是3-4秒俱恶,所以這兩個值并不是吐司顯示時長的毫秒數(shù),那么我們該如何得出正確的毫秒數(shù)呢逊抡?這個問題就留給大家去思考伦意,這里不做解答

只能使用當(dāng)前 Activity 創(chuàng)建 WindowManager 的缺陷

發(fā)現(xiàn)一個問題火窒,Activity 和 Application 同樣是 Context 的子類,如果使用 Activity 獲取的 WindowManager 對象可以創(chuàng)建出來驮肉,但是如果使用 Application 獲取的 WindowManager 對象卻報了錯

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application

報錯已經(jīng)說得很清楚了熏矿,創(chuàng)建 WindowManager 不能使用 Application 對象去創(chuàng)建,也就是說只能通過 Activity 對象去創(chuàng)建 WindowManager

那么問題來了缆八,每次彈這種 “Toast” 需要當(dāng)前 Activity 對象曲掰,這個問題對于常年使用框架的同學(xué)是致命的

這里以我做的框架 Toaster 為例子疾捍,顯示一個吐司是這樣子調(diào)用的

Toaster.show("我是吐司");

如果要解決在關(guān)閉通知欄權(quán)限后吐司還能再彈出來的問題奈辰,就需要改成

Toaster.show(MainActivity.this, "我是吐司");

先說一下這個問題帶來的影響吧,我是框架的作者乱豆,對于我來說奖恰,只需要在 Toaster 中 show 方法多添加一個 Activity 參數(shù)即可,但是對于使用框架的人宛裕,在更新完框架后瑟啃,整個項目所有使用到這個Toaster.show()方法都會報錯,需要多傳入一個Activity 參數(shù)揩尸,相信他們的內(nèi)心幾乎是崩潰的蛹屿,那么有沒有一種好的辦法解決這個問題,答案當(dāng)然是有了岩榆,可以用一個冷門的 API

Application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback);

這個 API 是在 安卓 4.0 之后才有的错负,而現(xiàn)在大多數(shù)設(shè)備已經(jīng)在 安卓 5.0 及以上,所以這個 API 還是有前途的勇边,接下看一下 ActivityLifecycleCallbacks 這個接口有什么方法吧

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}

看到這里犹撒,相信各位已經(jīng)知道真相了,這個方法用于監(jiān)聽?wèi)?yīng)用中 Activity 中的生命周期方法

那么我們就可以通過這個 API 來獲取當(dāng)前和用戶交互的 Activity 對象粒褒,從而完成讓當(dāng)前 Activity 對象去創(chuàng)建 WindowManager

使用 WindowManager 實現(xiàn) Toast 出現(xiàn)局限性的問題

當(dāng)然用 WindowManager 創(chuàng)建的 View 必然也會受 Activity 的限制识颊,因為就只能顯示這個 Activity 上,如果在其他界面上則會顯示不了奕坟,而系統(tǒng)原生的 Toast 則可以出現(xiàn)別的界面上祥款,那有沒有什么解決辦法呢清笨?

WindowManager 在沒有懸浮窗權(quán)限的時候就只能顯示依附于調(diào)用的 Activity,當(dāng)有授予了懸浮窗權(quán)限之后刃跛,可以通過改變type參數(shù)來更改 WindowManager 顯示范圍函筋,可以讓這個 WindowManager 顯示在其他界面之上,這樣 Toast 就不會隨著 Activity 的不可見而變得不可見

// 判斷是否為 Android 6.0 及以上系統(tǒng)并且有懸浮窗權(quán)限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(mToast.getView().getContext())) {
    // 解決使用 WindowManager 創(chuàng)建的 Toast 只能顯示在當(dāng)前 Activity 的問題
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    }else {
        params.type = WindowManager.LayoutParams.TYPE_PHONE;
    }
}

如何在原生 Toast 和 WindowManager 中取舍

這樣我們比對一組數(shù)據(jù):

類型 顯示范圍 需要參數(shù) 兼容性 效率 通知欄權(quán)限 懸浮窗權(quán)限
原生 Toast 所有界面 Context子類 一般 需要 不需要
WindowManager 當(dāng)前Activity Activity子類 一般 不需要 不需要

經(jīng)過對比奠伪,原生的 Toast 的優(yōu)勢還是要大于 WindowManager 的跌帐,所以如果在有在通知欄權(quán)限的前提下,建議使用原生的 Toast绊率,我們可以通過判斷通知欄權(quán)限是否被關(guān)閉谨敛,來判斷是來顯示原生 Toast 還是 WindowManager,方法代碼如下:

/**
 * 檢查通知欄權(quán)限有沒有開啟
 */
public static boolean isNotificationEnabled(Context context){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        ApplicationInfo appInfo = context.getApplicationInfo();
        String pkg = context.getApplicationContext().getPackageName();
        int uid = appInfo.uid;

        try {
            Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
            Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
            Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
            int value = (Integer) opPostNotificationValue.get(Integer.class);
            return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
        } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
            return true;
        }
    } else {
        return true;
    }
}

詳細(xì)的源碼地址請戳這里

Android 技術(shù)討論 Q 群:10047167

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滤否,一起剝皮案震驚了整個濱河市脸狸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌藐俺,老刑警劉巖炊甲,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欲芹,居然都是意外死亡卿啡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門菱父,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颈娜,“玉大人,你說我怎么就攤上這事浙宜」倭桑” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵粟瞬,是天一觀的道長同仆。 經(jīng)常有香客問我,道長裙品,這世上最難降的妖魔是什么俗批? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮清酥,結(jié)果婚禮上扶镀,老公的妹妹穿的比我還像新娘。我一直安慰自己焰轻,他們只是感情好臭觉,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般蝠筑。 火紅的嫁衣襯著肌膚如雪狞膘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天什乙,我揣著相機與錄音挽封,去河邊找鬼。 笑死臣镣,一個胖子當(dāng)著我的面吹牛辅愿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忆某,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼点待,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弃舒?” 一聲冷哼從身側(cè)響起癞埠,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聋呢,沒想到半個月后苗踪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡削锰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年通铲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喂窟。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡测暗,死狀恐怖央串,靈堂內(nèi)的尸體忽然破棺而出磨澡,到底是詐尸還是另有隱情,我是刑警寧澤质和,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布稳摄,位于F島的核電站,受9級特大地震影響饲宿,放射性物質(zhì)發(fā)生泄漏厦酬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一瘫想、第九天 我趴在偏房一處隱蔽的房頂上張望仗阅。 院中可真熱鬧,春花似錦国夜、人聲如沸减噪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筹裕。三九已至醋闭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朝卒,已是汗流浹背证逻。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抗斤,地道東北人囚企。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像瑞眼,于是被迫代替她去往敵國和親洞拨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容