踩坑之路:主線程修改UI也會(huì)崩潰?

前言

某天早晨,吃完早餐嘉裤,坐回工位郑临,打開電腦,開啟chrome屑宠,進(jìn)入友盟頁面厢洞,發(fā)現(xiàn)了一個(gè)崩潰信息:

java.lang.RuntimeException: Unable to resume activity {com.youdao.youdaomath/com.youdao.youdaomath.view.PayCourseVideoActivity}: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3824)
    at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3856)
    at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:51)
    at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:201)
    at android.app.ActivityThread.main(ActivityThread.java:6806)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
    at android.view.View.requestLayout(View.java:23147)
    at android.view.View.requestLayout(View.java:23147)
    at android.widget.TextView.checkForRelayout(TextView.java:8914)
    at android.widget.TextView.setText(TextView.java:5736)
    at android.widget.TextView.setText(TextView.java:5577)
    at android.widget.TextView.setText(TextView.java:5534)
    at android.widget.Toast.setText(Toast.java:332)
    at com.youdao.youdaomath.view.common.CommonToast.showShortToast(CommonToast.java:40)
    at com.youdao.youdaomath.view.PayCourseVideoActivity.checkNetWork(PayCourseVideoActivity.java:137)
    at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
    at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1413)
    at android.app.Activity.performResume(Activity.java:7400)
    at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3816)

一眼看上去似乎是比較常見的子線程修改UI的問題。并且是在Toast上面報(bào)出的,常識(shí)告訴我Toast在子線程彈出是會(huì)報(bào)錯(cuò)躺翻,但是應(yīng)該是提示Looper沒有生成的錯(cuò)丧叽,而不應(yīng)該是上面所報(bào)出的錯(cuò)誤。那么會(huì)不會(huì)是生成Looper以后報(bào)的錯(cuò)的?


一公你、

所以我先做了一個(gè)demo踊淳,如下:

    @Override
    protected void onResume() {
        super.onResume();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT).show();
            }
        });
        thread.start();
    }

運(yùn)行一下,果不其然崩潰掉,錯(cuò)誤信息就是提示我必須準(zhǔn)備好looper才能彈出toast:

    java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
        at android.widget.Toast$TN.<init>(Toast.java:393)
        at android.widget.Toast.<init>(Toast.java:117)
        at android.widget.Toast.makeText(Toast.java:280)
        at android.widget.Toast.makeText(Toast.java:270)
        at com.netease.photodemo.MainActivity$1.run(MainActivity.java:22)
        at java.lang.Thread.run(Thread.java:764)

接下來就在toast里面準(zhǔn)備好looper陕靠,再試試吧:

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        });
        thread.start();

運(yùn)行發(fā)現(xiàn)是能夠正確的彈出Toast的:


子線程彈出Toast.jpg

那么問題就來了迂尝,為什么會(huì)在友盟中出現(xiàn)這個(gè)崩潰呢?

二剪芥、

然后仔細(xì)看了下報(bào)錯(cuò)信息有兩行重要信息被我之前略過了:

at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
t android.widget.Toast.setText(Toast.java:332)

發(fā)現(xiàn)是在主線程報(bào)了Toast設(shè)置Text的時(shí)候的錯(cuò)誤雹舀。這就讓我很納悶了,子線程修改UI會(huì)報(bào)錯(cuò)粗俱,主線程也會(huì)報(bào)錯(cuò)说榆?
感覺這么多年Android白做了。這不是最基本的知識(shí)么寸认?
于是我只能硬著頭皮往源碼深處看了:
先來看看Toast是怎么setText的:

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

很常規(guī)的一個(gè)做法签财,先是inflate出來一個(gè)View對(duì)象,再從View對(duì)象找出對(duì)應(yīng)的TextView偏塞,然后TextView將文本設(shè)置進(jìn)去唱蒸。至于setText在我之前的文章震驚!Android子線程也能修改UI灸叼?有詳細(xì)說過神汹,是在ViewRootImpl里面進(jìn)行checkThread是否在主線程上面。所以感覺似乎一點(diǎn)問題都沒有古今。那么既然出現(xiàn)了這個(gè)錯(cuò)誤屁魏,總得有原因吧,或許是自己源碼看漏了?
那就重新再看一遍ViewRootImpl#checkThread方法吧:

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

這一看捉腥,還真的似乎給我了一點(diǎn)頭緒氓拼,系統(tǒng)在checkThread的時(shí)候并不是將Thread.currentThread和MainThread作比較,而是跟mThread作比較抵碟,那么有沒有一種可能mThread是子線程?一想到這里桃漾,我就興奮了,全類查看mThread到底是怎么初始化的:

    public ViewRootImpl(Context context, Display display) {
        ...代碼省略...
        mThread = Thread.currentThread();
       ...代碼省略...
    }

可以發(fā)現(xiàn)全類只有這一處對(duì)mThread進(jìn)行了賦值拟逮。那么會(huì)不會(huì)是子線程初始化了ViewRootimpl呢撬统?似乎我之前好像也沒有研究過Toast為什么會(huì)彈出來,所以順便就先去了解下Toast是怎么show出來的好了:

    /**
     * Show the view for the specified duration.
     */
    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
        }
    }

調(diào)用Toast的show方法時(shí)敦迄,會(huì)通過Binder獲取Service即NotificationManagerService恋追,然后執(zhí)行enqueueToast方法(NotificationManagerService的源碼就不做分析)粒竖,然后會(huì)執(zhí)行Toast里面如下方法:

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

發(fā)送一個(gè)Message,通知進(jìn)行show的操作:

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

在Handler的handleMessage方法中找到了SHOW的case几于,接下來就要進(jìn)行真正show的操作了:

        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 */
                }
            }
        }

代碼有點(diǎn)長蕊苗,我們最需要關(guān)心的就是mWm.addView方法。相信看過ActivityThread的同學(xué)應(yīng)該知道m(xù)Wm.addView方法是在ActivityThread的handleResumeActivity里面也有調(diào)用過沿彭,意思就是進(jìn)行ViewRootImpl的初始化朽砰,然后通過ViewRootImp進(jìn)行View的測(cè)量,布局喉刘,以及繪制瞧柔。
看到這里,我想到了一個(gè)可能的原因:那就是我的Toast是一個(gè)全局靜態(tài)的Toast對(duì)象睦裳,然后第一次是在子線程的時(shí)候show出來造锅,這個(gè)時(shí)候ViewRootImpl在初始化的時(shí)候就會(huì)將子線程的對(duì)象作為mThread,然后下一次在主線程彈出來就出錯(cuò)了吧廉邑?想想應(yīng)該是這樣的哥蔚。

三、

所以繼續(xù)做我的demo來印證我的想法:

    @Override
    protected void onResume() {
        super.onResume();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                sToast = Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT);
                sToast.show();
                Looper.loop();
            }
        });
        thread.start();
    }

    public void click(View view) {
        sToast.setText("主線程彈出Toast");
        sToast.show();
    }

做了個(gè)靜態(tài)的toast蛛蒙,然后點(diǎn)擊按鈕的時(shí)候彈出toast糙箍,運(yùn)行一下:


主線程彈出toast.gif

發(fā)現(xiàn)竟然沒問題,這時(shí)候又開始懷疑人生了牵祟,這到底怎么回事深夯。ViewRootImpl此時(shí)的mThread應(yīng)該是子線程啊,沒道理還能正常運(yùn)行诺苹,怎么辦呢咕晋?debug一步一步調(diào)試吧,一步一步調(diào)試下來收奔,發(fā)現(xiàn)在View的requestLayout里面parent竟然為空了:


image.png

然后在仔細(xì)看了下當(dāng)前View是一個(gè)LinearLayout掌呜,然后這個(gè)View的子View是TextView,文本內(nèi)容是"主線程彈出toast"筹淫,所以應(yīng)該就是Toast在new的時(shí)候inflate的布局
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

Android源碼社區(qū)中搜索"transient_notification"找到了對(duì)應(yīng)的toast布局文件站辉,打開一看呢撞,果然如此:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="?android:attr/toastFrameBackground">

    <TextView
        android:id="@android:id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginHorizontal="24dp"
        android:layout_marginVertical="15dp"
        android:layout_gravity="center_horizontal"
        android:textAppearance="@style/TextAppearance.Toast"
        android:textColor="@color/primary_text_default_material_light"
        />

</LinearLayout>

也就是說此時(shí)的View已經(jīng)是頂級(jí)View了损姜,它的parent應(yīng)該就是ViewRootImpl善玫,那么為什么ViewRootImpl是null呢贬媒,明明之前已經(jīng)show過了∥海看來只能往Toast的hide方法找原因了

四绷蹲、

所以重新回到Toast的類中,查看下Toast的hide方法(此處直接看Handler的hide處理棒卷,之前的操作與show類似):

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


                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }

                mView = null;
            }
        }

此處調(diào)用了mWm的removeViewImmediate顾孽,即WindowManagerImpl里面的removeViewImmediate方法:

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

會(huì)調(diào)用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);
        }
    }

然后調(diào)用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) {
            //此處調(diào)用View的assignParent方法將viewParent置空
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

所以也就是說在Toast時(shí)間到了以后,會(huì)調(diào)用hide方法比规,此時(shí)會(huì)將parent置成空若厚,所以我剛才試的時(shí)候才沒有問題。那么按道理說只要在Toast沒有關(guān)閉的時(shí)候點(diǎn)擊再次彈出toast應(yīng)該就會(huì)報(bào)錯(cuò)蜒什。
所以還是原來的代碼测秸,再來一次,這次不等Toast關(guān)閉灾常,再次點(diǎn)擊:


主線程彈出Toast崩潰.gif

果然如預(yù)期所料霎冯,此時(shí)在主線程彈出Toast就會(huì)崩潰。

五钞瀑、

那么問題原因找到了:是在項(xiàng)目子線程中有彈出過Toast沈撞,然后Toast并沒有關(guān)閉,又在主線程彈出了同一個(gè)對(duì)象的toast雕什,會(huì)造成崩潰缠俺。此時(shí)內(nèi)心有個(gè)困惑:如果是子線程彈出Toast,那我就需要寫Looper.prepare方法和Looper.loop方法贷岸,為什么我自己一點(diǎn)印象都沒有晋修。于是我全局搜索了Looper.prepare,發(fā)現(xiàn)并沒有找到對(duì)應(yīng)的代碼凰盔。所以我就全局搜索了Toast調(diào)用的地方墓卦,發(fā)現(xiàn)在JavaBridge的回調(diào)當(dāng)中找到了:

    class JSInterface {
        @JavascriptInterface
        public void handleMessage(String msg) throws JSONException {
            LogHelper.e(TAG, "msg::" + msg);
            JSONObject jsonObject = new JSONObject(msg);
            String callType = jsonObject.optString(JS_CALL_TYPE);
            switch (callType) {
                ...代碼省略..
                case JSCallType.SHOW_TOAST:
                    showToast(jsonObject);
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 彈出吐司
     * @param jsonObject
     * @throws JSONException
     */
    public void showToast(JSONObject jsonObject) throws JSONException {
        JSONObject payDataObj = jsonObject.getJSONObject("data");
        String message = payDataObj.optString("data");
        CommonToast.showShortToast(message);
    }

但是看到這段代碼,又有疑問了户敬,我并沒有在Javabridge的回調(diào)中看到有任何準(zhǔn)備Looper的地方落剪,那么為什么Toast沒有崩潰掉?
所以在此處加了一段代碼:

    class JSInterface {
        @JavascriptInterface
        public void handleMessage(String msg) throws JSONException {
            LogHelper.e(TAG, "msg::" + msg);
            JSONObject jsonObject = new JSONObject(msg);
            String callType = jsonObject.optString(JS_CALL_TYPE);
            Thread currentThread = Thread.currentThread();
            Looper looper = Looper.myLooper();
            switch (callType) {
                ...代碼省略..
                case JSCallType.SHOW_TOAST:
                    showToast(jsonObject);
                    break;
                default:
                    break;
            }
        }
    }

并且加了一個(gè)斷點(diǎn)尿庐,來查看下此時(shí)的情況:


Thread和Looper.jpg

確實(shí)當(dāng)前線程是JavaBridge線程,另外JavaBridge線程中已經(jīng)提前給開發(fā)者準(zhǔn)備好了Looper忠怖。所以也難怪一方面奇怪自己怎么沒有寫Looper的印象,一方面又很好奇為什么這個(gè)線程在開發(fā)者沒有準(zhǔn)備Looper的情況下也能正常彈出Toast抄瑟。


總結(jié)

至此凡泣,真相終于找出來了。相比較發(fā)生這個(gè)bug 的原因,解決方案就顯得非常簡(jiǎn)單了皮假。只需要在CommonToast的showShortToast方法內(nèi)部判斷是否為主線程調(diào)用鞋拟,如果不是的話,new一個(gè)主線程的Handler惹资,將Toast扔到主線程彈出來贺纲。這樣就會(huì)避免了子線程彈出。
PS:本人還得吐槽一下Android褪测,Android官方一方面明明宣稱不能在主線程以外的線程進(jìn)行UI的更新猴誊,另一方面在初始化ViewRootImpl的時(shí)候又不把主線程作為成員變量保存起來潦刃,而是直接獲取當(dāng)前所處的線程作為mThread保存起來,這樣做就有可能會(huì)出現(xiàn)子線程更新UI的操作懈叹。從而引起類似我今天的這個(gè)bug乖杠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市澄成,隨后出現(xiàn)的幾起案子滑黔,更是在濱河造成了極大的恐慌,老刑警劉巖环揽,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件略荡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歉胶,警方通過查閱死者的電腦和手機(jī)汛兜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來通今,“玉大人粥谬,你說我怎么就攤上這事”杷” “怎么了漏策?”我有些...
    開封第一講書人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長臼氨。 經(jīng)常有香客問我掺喻,道長,這世上最難降的妖魔是什么储矩? 我笑而不...
    開封第一講書人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任感耙,我火速辦了婚禮,結(jié)果婚禮上持隧,老公的妹妹穿的比我還像新娘即硼。我一直安慰自己,他們只是感情好屡拨,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開白布只酥。 她就那樣靜靜地躺著,像睡著了一般呀狼。 火紅的嫁衣襯著肌膚如雪裂允。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評(píng)論 1 291
  • 那天赠潦,我揣著相機(jī)與錄音叫胖,去河邊找鬼。 笑死她奥,一個(gè)胖子當(dāng)著我的面吹牛瓮增,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哩俭,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼绷跑,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了凡资?” 一聲冷哼從身側(cè)響起砸捏,我...
    開封第一講書人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隙赁,沒想到半個(gè)月后垦藏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伞访,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年掂骏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厚掷。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弟灼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冒黑,到底是詐尸還是另有隱情田绑,我是刑警寧澤,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布抡爹,位于F島的核電站掩驱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏冬竟。R本人自食惡果不足惜昙篙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诱咏。 院中可真熱鬧苔可,春花似錦、人聲如沸袋狞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苟鸯。三九已至同蜻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間早处,已是汗流浹背湾蔓。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砌梆,地道東北人默责。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓贬循,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桃序。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杖虾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350