帶著問題學習 Android Handler 消息機制

學習 Android Handler 消息機制

一、提出問題

面試時常被問到的問題:

  • 簡述 Android 消息機制
  • Android 中 Handler,Looper,MessageQueue,Message 有什么關系?

這倆問題其實是一個問題,其實只要搞清楚了 Handler,Looper,MessageQueue,Message 的作用和聯系,就理解了 Android 的 Handler 消息機制。那么再具體一點:

  1. 為什么在主線程可以直接使用 Handler?
  2. Looper 對象是如何綁定 MessageQueue 的?
  3. MessageQueue 里的消息從哪里來?Handler是如何往MessageQueue中插入消息的?
  4. Message 是如何綁定 Handler 的?
  5. Handler 如何綁定 MessageQueue?
  6. 關于 handler,在任何地方 new handler 都是什么線程下?
  7. Looper 循環拿到消息后怎么處理?

二、解決問題

那么,我們從主線程的消息機制開始分析:

2.1 主線程 Looper 的創建和循環

Android 應用程序的入口是 main 函數,主線程 Looper 的創建也是在這里完成的。

ActivityThread --> main() 函數

public static void main(){
        // step1: 創建主線程Looper對象
        Looper.prepareMainLooper();
        
        ActivityThread thread = new ActivityThread();
        // 綁定應用進程,布爾標記是否為系統進程
        thread.attach(false);
        // 實例化主線程 Handler
        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        // step2: 開始循環
        Loop.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
}

Looper.prepareMainLooper()用來創建主線程的 Looper 對象,接下來先看這個方法的實現。

2.1.1 創建主線程 Looper

Looper --> prepareMainLooper()

private static Looper sMainLooper;  // guarded by Looper.class

public static void prepareMainLooper(){
        // step1: 調用本類 prepare 方法
        prepare(false);
        // 線程同步,如果變量 sMainLooper 不為空拋出主線程 Looper 已經創建
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            // step2: 調用本類 myLooper 方法
            sMainLooper = myLooper();
        }
}

prepareMainLooper() 方法主要是使用 prepare(false) 創建當前線程的 Looper 對象,再使用 myLooper() 方法來獲取當前線程的 Looper 對象。

step1: Looper --> prepare()

// ThreadLocal 為每個線程保存單獨的變量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
// Looper 類的 MessageQueue 變量
final MessageQueue mQueue;
// quitAllowed 是否允許退出,這里是主線程的 Looper 不可退出
private static void prepare(boolean quitAllowed) {
        // 首先判定 Looper 是否存在
        if(sThreadLocal.get() != null){
                throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 保存線程的副本變量
        sThreadLoacal.set(new Looper(quitAllowed));
}

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
  • prepare() 方法中用 ThreadLocal 來保存主線程的 Looper 對象。ThreadLocal 可以看作是一個用來儲存數據的類,類似 HashMap、ArrayList等集合類,它存放著屬于當前線程的變量。

  • ThreadLocal 提供了 get/set 方法分別用來獲取和保存變量。
    比如在主線程通過 prepare() 方法來創建 Looper 對象,并使用 sThreadLoacal.set(new Looper(quitAllowed)) 來保存主線程的 Looper 對象,那么在主線程調用 myLooper()(實際調用了 sThreadLocal.get() 方法) 就是通過 ThreadLocal 來獲取主線程的 Looper 對象。如果在子線程調用這些方法就是通過 ThreadLocal 保存和獲取屬于子線程的 Looper 對象。

更多關于 ThreadLocal 的原理:

深入剖析ThreadLocal實現原理以及內存泄漏問題

問題1:為什么在主線程可以直接使用 Handler?
因為主線程已經創建了 Looper 對象并開啟了消息循環,通過上文的代碼就可以看出來。

問題2:Looper 對象是如何綁定 MessageQueue 的?或者說 Looper 對象創建 MessageQueue 過程。
很簡單,Looper 有個一成員變量 mQueue,它就是 Looper 對象默認保存的 MessageQueue。上面代碼中 Looper 有一個構造器,新建 Looper 對象時會直接創建 MessageQueue 并賦值給 mQueue。
問題2解決:在 new Looper 時就創建了 MessageQueue 對象并賦值給 Looper 的成員變量 mQueue。

step2: Looper --> myLooper()

// 也就是使用本類的ThreadLocal對象獲取之前創建保存的Looper對象
public static @Nullable Looper myLooper() {
     return sThreadLocal.get();
}

這個方法就是通過 sThreadLocal 變量獲取當前線程的 Looper 對象,比較常用的一個方法。上文主線程 Looper 對象創建后使用該方法獲取了 Looper 對象。

2.1.2 開始循環處理消息

回到最開始的 main() 函數,在創建了 Looper 對象以后就調用了 Looper.loop() 來循環處理消息,貼一下大致代碼:

public static void main(){
        // step1: 創建主線程Looper對象
        Looper.prepareMainLooper();
        ...
        // step2: 開始循環
        Loop.loop();
}

Looper --> loop()

public static void loop() {
    // step1: 獲取當前線程的 Looper 對象
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    // step2: 獲取 Looper 保存的 MessageQueue 對象
    final MessageQueue queue = me.mQueue;

    ...
    // step3: 循環讀取消息,如果有則調用消息對象中儲存的 handler 進行發送
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        ...
        try {
            // step4: 使用 Message 對象保存的 handler 對象處理消息
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        ...
        msg.recycleUnchecked();
    }
}
  • step1 : myLooper() 方法就是通過 ThreadLocal 獲取當前線程的 Looper 對象,注意在哪個線程使用該方法就獲取的該線程的 Looper 對象。
  • step2 :me.mQueue,這個 mQueue 就是上面問題2所說的在 Looper 對象創建時新建的 MessageQueue 變量。
  • step3 :接下來是一個 for 循環,首先通過 queue.next() 來提取下一條消息,具體是怎么提取的可以參考下面文章的 4.2 節:

Android消息機制1-Handler(Java層)

獲取到下一條消息,如果 MessageQueue 中沒有消息,就會進行阻塞。那么如果存在消息,它又是怎么放入 MessageQueue 的呢?或者說MessageQueue 里的消息從哪里來?Handler是如何往MessageQueue中插入消息的?先不說這個,把這個問題叫作問題3后面分析。

  • step4 :msg.target.dispatchMessage(msg);這個方法最終會調用 Handler 的 handleMessage(msg) 方法。同時這里又產生個問題:msg.target 是何時被賦值的?,也就是說Message 是如何綁定 Handler 的?先稱之為問題4。那么接著看 Handler 的 dispatchMessage 方法:

Handler --> dispatchMessage

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
   message.callback.run();
}

public void handleMessage(Message msg) {
}

可以看到該方法最后執行了 handleMessage() 方法,這是一個空方法也就是需要我們覆寫并實現的。另外 dispatchMessage() 也體現出一個問題:

消息分發的優先級:

  • Message 的回調方法:message.callback.run(); 優先級最高;
  • Handler 的回調方法:mCallback.handleMessage(msg)優先級次于上方;
  • Handler 的回調方法:handleMessage() 優先級最低。

到這里 Looper 循環并通過 Handler 發送消息有一個整體的流程了,接下來分析 Handler 在消息機制中的主要作用以及和 Looper、Message 的關系。

2.2 Handler 的創建和作用

上面說到 loop() 方法在不斷從消息隊列 MessageQueue 中取出消息(queue.next() 方法),如果沒有消息則阻塞,反之交給 Message 綁定的 Handler 處理。回顧一下沒解決的兩個問題:

  • 問題3:MessageQueue 里的消息從哪里來?Handler 是如何往 MessageQueue 中插入消息的?
  • 問題4:msg.target 是何時被賦值的?,也就是說Message 是如何綁定 Handler 的?

既然要解決 Handler 插入消息的問題,就要看 Handler 發送消息的過程。

2.2.1 Handler 發送消息

Handler --> sendMessage(Message msg);

final MessageQueue mQueue;

public final boolean sendMessage(Message msg){
    return sendMessageDelayed(msg, 0);
}
// 發送延時消息
public final boolean sendMessageDelayed(Message msg, long delayMillis){
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
// 指定時間發送消息
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}
// 處理消息,賦值 Message 對象的 target,消息隊列插入消息
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到調用 sendMessage(Message msg) 方法最終會調用到 enqueueMessage() 方法,這個方法主要有兩個作用:賦值 Message 對象的 target、消息隊列插入消息。

  • 賦值 msg 的 target:msg.target = this 把發送消息的 Handler 賦值給 msg 對象的 target。那么問題 4 就解決了:Handler 執行發送消息的過程中將自己綁定給了 Message 的 target,這樣兩者之間就產生了聯系
  • 消息隊列插入消息:queue.enqueueMessage(msg, uptimeMillis) queue 是 MessageQueue 的一個實例,queue.enqueueMessage(msg, uptimeMillis)是執行 MessageQueue 的enqueueMessage方法來插入消息。這樣問題 3 就找到答案:Handler 在發送消息的時候執行 MessageQueue 的enqueueMessage方法來插入消息;關于 MessageQueue 是怎么執行插入消息的過程,參考下方文章 4.3 節

Android消息機制1-Handler(Java層)

  • 上面 Handler 發送消息使用了 MessageQueue 的實例 queue,可以看到這個 queue 是上一個方法 sendMessageAtTime 中由 Handler 的成員變量 mQueue 賦值的,那么 mQueue 是哪來的?問題 5:Handler 如何綁定 MessageQueue?先劇透一下 Handler 綁定的是 Looper 的 MessageQueue 對象,Looper 的 MessageQueue 對象是在 Looper 創建時就 new 的。

要了解 Handler 的 MessageQueue 對象是怎么賦值的就要看 Handler 的構造函數了,Handler 創建的時候作了一些列操作比如獲取當前線程的 Looper,綁定 MessageQueue 對象等。

2.2.2 Handler 的創建

下面是 Handler 無參構造器和主要的構造器,另外幾個重載的構造器有些是通過傳遞不同參數調用包含兩個參數的構造器。兩個參數構造函數第一個參數為 callback 回調,第二個函數用來標記消息是否異步。

// 無參構造器
public Handler() {
     this(null, false);
}

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
        }
    }
    // step1:獲取當前線程 Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
    }
    // step2:獲取 Looper 對象綁定的 MessageQueue 對象并賦值給 Handler 的 mQueue
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
  • step1:調用myLooper() 方法,該方法是使用 sThreadLocal 對象獲取當前線程的 Looper 對象,回顧一下:
public static @Nullable Looper myLooper() {
     return sThreadLocal.get();
}

如果獲取的 Looper 對象為 null,說明沒有執行 Looper.prepare() 為當前線程保存 Looper 變量,就會拋出 RuntimeException。這里又說明了Handler 必須在有 Looper 的線程中使用,報錯不說,沒有 Looper 就無法綁定 MessageQueue 對象也就無法進行更多有關消息的操作。

  • step2:mQueue = mLooper.mQueue 說明了 Handler 的 MessageQueue 對象是由當前線程 Looper 的 MessageQueue 對象賦值的。這里問題 5 解決:Handler 在創建時綁定了當前線程 Looper 的 MessageQueue 對象。
  • 由于 Handler 和 Looper 可以看作使用的是同一個 MessageQueue 對象,所以 Handler 和 Looper 可以共享消息隊列 MessageQueue。Handler 發送消息(用 mQueue 往消息對列插入消息),Looper 可以方便的循環使用 mQueue 查詢消息,如果查詢到消息,就可以用 Message 對象綁定的 Handler 對象 target 去處理消息,反之則阻塞。

既然說到了 Handler 的構造器,就想到一個問題:問題 6:關于 handler,在任何地方 new handler 都是什么線程下?這個問題要分是否傳遞 Looper 對象來看。

  1. 不傳遞 Looper 創建 Handler:Handler handler = new Handler();上文就是 Handler 無參創建的源碼,可以看到是通過 Looper.myLooper() 來獲取 Looper 對象,也就是說對于不傳遞 Looper 對象的情況下,在哪個線程創建 Handler 默認獲取的就是該線程的 Looper 對象,那么 Handler 的一系列操作都是在該線程進行的。
  2. 傳遞 Looper 對象創建 Handler:Handler handler = new Handler(looper);那么看看傳入 Looper 的構造函數:
public Handler(Looper looper) {
    this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}
// 第一個參數是 looper 對象,第二個 callback 對象,第三個消息處理方式(是否異步)
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

可以看出來傳遞 Looper 對象 Handler 就直接使用了。所以對于傳遞 Looper 對象創建 Handler 的情況下,傳遞的 Looper 是哪個線程的,Handler 綁定的就是該線程。

到這里 Looper 和 Handler 就有一個大概的流程了,接下來看一個簡單的子線程 Handler 使用例子:

new Thread() {
    @Override
    public void run() {
        // step1
        Looper.prepare();
         // step2
        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                if(msg.what == 1){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this,"HandlerTest",Toast.LENGTH_SHORT).show();
                        }
                    });
                     // step5
                    Looper.myLooper().quit();
                }
            }
        };
         // step3
        handler.sendEmptyMessage(1);
         // step4
        Looper.loop();
    }
}.start();
  • step1: 調用 Looper.prepare(); 為當前線程創建 Looper 對象,同時也就創建了 MessageQueue,之后將該線程的 Looper 對象保存在 ThreadLocal 中。注意這里的一切操作都在子線程中,如果不調用 Looper.prepare() 就使用 Handler 會報錯。
  • step2: 創建 Handler 對象,覆寫 handleMessage 處理消息,等待該 Handler 發送的消息處理時會調用該方法。
  • step3: 使用 handler 發送消息,這里只是示例,畢竟自己給自己發送消息沒啥必要。發送的過程中會將自己賦值給 msg.target,然后再將消息插入到 Looper 綁定的 MessageQueue 對象中。
  • step4: 調用 Looper.loop(); 首先獲取當前線程的 Looper 對象,根據 Looper 對象就可以拿到 Looper 保存的 MessageQueue 對象 mQueue。有了 MessageQueue 對象就可以 for 循環獲取它保存的消息 Message 對象,如果消息不存在就返回 null 阻塞,反之則使用 Message 中保存的 Handler:msg.target 來處理消息,最終調用 handleMessage 也就是之前覆寫的方法來處理消息。
  • step5: 邏輯處理完畢以后,應在最后使用 quit 方法來終止消息循環,否則這個子線程就會一直處于等待的狀態,而如果退出Looper以后,這個線程就會立刻終止,因此建議不需要的時候終止Looper。

三、總結和其它

3.1 Handler、Looper、MessageQueue、Message

  1. Handler 用來發送消息,創建時先獲取默認或傳遞來的 Looper 對象,并持有 Looper 對象包含的 MessageQueue,發送消息時使用該 MessageQueue 對象來插入消息并把自己封裝到具體的 Message 中;
  2. Looper 用來為某個線程作消息循環。Looper 持有一個 MessageQueue 對象 mQueue,這樣就可以通過循環來獲取 MessageQueue 所維護的 Message。如果獲取的 MessageQueue 沒有消息時,便阻塞在 loop 的queue.next() 中的 nativePollOnce() 方法里,反之則喚醒主線程繼續工作,之后便使用 Message 封裝的 handler 對象進行處理。
  3. MessageQueue 是一個消息隊列,它不直接添加消息,而是通過與 Looper 關聯的 Handler 對象來添加消息。
  4. Message 包含了要傳遞的數據和信息。

3.2 Android中為什么主線程不會因為Looper.loop()里的死循環卡死?

這是知乎上的問題,感覺問的挺有意思。平時可能不太會太深究這些問題,正好有大神回答那就記錄一下吧。

  1. 為什么不會因為死循環卡死?
    線程可以看作是一段可執行代碼,當代碼執行完畢線程的生命周期就該終止了。對于主線程來說我們不希望它執行一段時間后退出,所以簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出。既然是死循環那么怎么去處理消息呢,通過創建新線程的方式。
  2. 為這個死循環準備了一個新線程
    在進入死循環之前便創建了新binder線程,在代碼ActivityThread.main()中:
public static void main(){
        ...
        Looper.prepareMainLooper();

        //創建ActivityThread對象
        ActivityThread thread = new ActivityThread();

        //建立Binder通道 (創建新線程)
        thread.attach(false);

        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        // step2: 開始循環
        Loop.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
}

thread.attach(false);便會創建一個Binder線程(具體是指ApplicationThread,Binder的服務端,用于接收系統服務AMS發送來的事件),該Binder線程通過Handler將Message發送給主線程。

  1. 主線程的死循環一直運行是不是特別消耗CPU資源呢?

其實不然,這里就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法里,詳情見Android消息機制1-Handler(Java層),此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。所以說,主線程大多數時候都是處于休眠狀態,并不會消耗大量CPU資。

  1. Activity的生命周期是怎么實現在死循環體外能夠執行起來的?
    上文 main 函數有一部分獲取 sMainThreadHandler 的代碼:
final H mH = new H();

public static void main(){
        ...
        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        ...
}

final Handler getHandler() {
    return mH;
}

類 H 繼承了 Handler,在主線程創建時就創建了這個 Handler 用于處理 Binder 線程發送來的消息。

Activity的生命周期都是依靠主線程的Looper.loop,當收到不同Message時則采用相應措施:

在H.handleMessage(msg)方法中,根據接收到不同的msg,執行相應的生命周期。
比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,創建Activity實例,然后再執行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。 上述過程,我只挑核心邏輯講,真正該過程遠比這復雜。

3.3 Handler 使用造成內存泄露

  1. 有延時消息,要在Activity銷毀的時候移除Messages
  2. 匿名內部類導致的泄露改為匿名靜態內部類,并且對上下文或者Activity使用弱引用。

具體操作可以參考文章:

Handler內存泄露原理及解決方法

參考資料:

《Android 開發藝術探索》
Android消息機制1-Handler(Java層)
Android Handler消息機制實現原理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容