Android面试题(五)—— Android的消息机制

前言


Handler是Android消息机制的上层接口,平时使用起来很方便,我们可以通过它把一个任务切换到Handler所在的线程中去运行。而最常用的就是拿来从子线程切换到主线程以便更新UI。关于Android的消息机制无法以题目为导向来进行讲解,面试中可能会问关于Handler、Looper、MessageQueue、Message之间的关系,要完整回答,我们需要了解Handler内部是如何工作的,而这一部分的源码并不复杂。所以先整体分析得出结论,再从源码中验证结论。

Android的消息机制整体剖析


Android的消息机制工作原理大致如下:


Android消息机制.jpg
  1. MessageQueue:它的内部存储了一组数据,以队列的形式向外提供了插入和删除的工作。但是它的内部实现并不是队列,而是单链表。对应图中长方形格子

  2. Looper:会不停检查是否有新的消息,如果有就调用最终消息中的Runnable或者Handler的handleMessage方法。对应提取并处理消息

  3. Handler:Handler的工作主要包含消息的发送和接收过程。消息的发送可以通过post的一系列方法以及send的一系列方法来实现,不过最后都是通过send的一系列方法实现的。对应添加消息处理线程

  4. Message:封装了需要传递的消息,并且本身可以作为链表的一个节点,方便MessageQueue的存储。

  5. ThreadLocal:一个线程内部的数据存储类,通过它可以在指定的线程中储存数据,而其它线程无法获取到。在Looper、AMS中都有使用。

  6. Thread:Android的线程类

Android消息机制的类的关系总结如下:


Android消息机制的类的关系UML图

由上图总结出以下结论:

  1. MessageQueue持有一个mMessages,作为消息队列内部存储数据的链表头。它具有两个重要的操作:对消息的插入和读取,对应的方法分别是enqueueMessage和next。其中enqueueMessage是往消息队列中插入一条信息,而next的作用是从消息队列中取出一条信息并将其从消息队列中移除。

  2. Message内部除了obj,what,arg1,arg2等存储数据的成员,还有一个可以指向其他Message的指针,所以MessageQueue可以使用它来作为链表的节点。

  3. Looper内部持有一个消息队列、线程、主线程、ThreadLocal。主要的方法有:

- prepare:为当前线程创建一个Looper。
- quit:退出Looper,Looper退出后,Handler的send方法会返回false,在子线程手动创建的Looper最好在不需要的时候终止掉。
- quitSafely:把消息队列中已有的消息处理完毕后退出。
- getMainLooper:在任何地方获取主线程的Looper。
- getLooper:获取当前线程的Looper。
- loop:最重要的一个方法,只有调用了loop方法后,消息循环系统才能起作用。(后面再做详细解释)
  1. 一个Thread只能持有一个Looper。

  2. Handler持有一个消息队列、Looper、Callback。提供多种创建方法,默认的Handler()将使用当前线程的Looper,如果当前线程没有Looper会抛出异常,也可以通过传参指定Looper。sendMessage方法可以往消息队列添加消息。handleMessage方法在创建Handler的线程中或者指定的Looper持有的线程中处理消息。

  3. 一个Looper可以被多个Handler持有

  4. ThreadLocal的get和set方法操作的数据,在每个线程中是相互独立,互不干扰的。

源码分析


1. ThreadLocal的工作原理

  • ThreadLocal是什么?
    ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,而其它线程无法获取到数据。在Looper、ActiivtyThread和AMS中都用到了ThreadLocal。
  • ThreadLocal的使用场景
    • 当某些数据是以线程为作用域,并且不同线程有不同的数据副本的时候
    • 复杂逻辑下的对象传递,比如监听器的传递。使用参数传递的话:当函数调用栈过深时,设计会很糟糕。为每一个线程定义一个静态变量存储监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展,这时候使用ThreadLocal可以解决问题。

从ThreadLocal的set和get方法可以看出,他们所操作的对象都是当前线程的localValues对象的table数组,因此在不同的线程访问ThreadLocal的set和get方法,他们对ThreadLocal的读写操作都是仅限于各自线程的内部。这就是ThreadLocal可以在多个线程中互不干扰地存储和修改数据的原因。
简单来讲:就是每个线程都可以操作ThreadLocal,但他们操作的数据是分隔开的,互不干扰的,代码如下:

        private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>();

        mBooleanThreadLocal.set(true);
        Log.d(TAG, "[Thread#main]mBooleanThreadLocal="+ mBooleanThreadLocal.get());

        new Thread("Thread#1") {
            @Override
            public void run() {
                mBooleanThreadLocal.set(false);
                Log.d(TAG, "[Thread#1]mBooleanThreadLocal="+ mBooleanThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2") {
            @Override
            public void run() {
                Log.d(TAG, "[Thread#2]mBooleanThreadLocal="+ mBooleanThreadLocal.get());
            }
        }.start();

运行结果:

07-17 16:23:23.222 23286-23286/com.ryg.chapter_15 D/MainActivity: [Thread#main]mBooleanThreadLocal=true
07-17 16:23:23.222 23286-23312/com.ryg.chapter_15 D/MainActivity: [Thread#1]mBooleanThreadLocal=false
07-17 16:23:23.222 23286-23313/com.ryg.chapter_15 D/MainActivity: [Thread#2]mBooleanThreadLocal=null

由此可以得出结论7是正确的。

2. 消息队列的工作原理

消息队列在Android中指的是MessageQueue,它主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作。插入和删除对应的方法分别为enqueueMessage和next。代码如下:

    boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            ...
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

从enqueueMessage的实现中,我们可以明显看出这是一个单链表的插入操作,不多解释,接着看next方法:

    Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            // We can assume mPtr != 0 because the loop is obviously still running.
            // The looper will not call this method after the loop quits.
            nativePollOnce(mPtr, nextPollTimeoutMillis);

            synchronized (this) {
                // 尝试获取一个消息,如果找到就返回它。
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
               ...
            }
            ...
        }
    }

next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。

3. Looper的工作原理

Looper在Android消息机制中扮演消息循环的角色,它会不停地从MessageQueue中查看是否有新消息,如果有就立即处理,没有就阻塞在那里。现在,我们从Looper的使用的一个常见例子来分析这个Looper类。代码如下:

    class LooperThread extends Thread {
        public Handler h;
        public void run() {
            // 1. 调用prepare
            Looper.prepare();
            // 2.进入消息循环 
            Looper.loop();
            ...
        }
    }
    // 应用程序使用LooperThread
    {
        ...
        new LooperThread().start(); //启动新线程,线程函数是run
    }

在上面代码中,Looper有两处关键调用,分别是1. 调用prepare 2.进入消息循环。接下来我们重点分析这两个函数。

  1. 第一个调用的函数是Looper的prepare函数。代码如下:
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        // 一个线程中只能有一个Looper。只能调用一次prepare
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 构造一个Looper对象,设置到调用线程的局部变量中。
        sThreadLocal.set(new Looper(quitAllowed));
    }
    // sThreadLocal的定义
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

根据上面对ThreadLocal的分析,我们知道使用局部变量sThreadLocal存储的变量作用域是针对线程的。即通过prepare为调用的线程的设置了一个Looper对象。在看一看Looper的构造方法。

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

在构造方法中,创建了一个新的消息队列,和持有当前线程的引用。
所以prepare函数主要完成的工作是:在调用prepare的线程中,设置一个Looper对象,这个Looper对象保存在Thread的localValues中,而Looper对象内部封装了一个消息队列。
prepare通过ThreadLocal机制,巧妙地将Looper和调用线程关联在一块。接着看第二个重要函数loop。

  1. Looper的循环
    代码如下:
    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper(); //myLooper返回当前线程的Looper对象
        final MessageQueue queue = me.mQueue; // 取出Looper中的消息队列
        ...
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ...
            // 调用该消息的Handler,交给它的dispatchMessage处理
            msg.target.dispatchMessage(msg);
            ...
            // 消息的回收,回收到消息池中。
            msg.recycle();
        }
    }

从代码中可以看出,loop是一个死循环,唯一跳出循环的条件是MessageQueue的next方法返回null。而只有当Looper调用quit或者quitSafely方法,方法内部再调用MessageQueue的quit或者quitSafely方法通知消息队列退出,当消息队列退出后,next方法才会返回null。从上面对MessageQueue的分析中,我们知道next方法是一个阻塞操作,当消息队列中没有消息时,next方法就会一直阻塞在那里,这也导致了loop方法一直阻塞在那里,直到next方法返回新的消息,才会调用msg.target.dispatchMessage(msg)来处理消息。这里的msg.target就是发送这条消息的Handler对象。需要注意的是:通过这一过程。因为Handler的dispatchMessage方法在loop中执行,所以发送消息所在的线程成功地把代码逻辑切换到了Looper所在的线程中执行,完成了线程间的切换。

4. Handler的工作原理

首先看一下Handler所包括的成员:

    final MessageQueue mQueue; // Handler中也有一个消息队列,从mLooper中取出的
    final Looper mLooper; // 当前线程的Looper或者指定的Looper
    final Callback mCallback; //有一个回调用的类

这几个变量是如何被使用的?首先分析Handler的构造方法,代码如下:

    public Handler()

    public Handler(Callback callback)

    public Handler(Looper looper) 

    public Handler(Looper looper, Callback callback)

在Handler中我们常用的构造方法有上面4个,如果没有指定Callback,默认mCallback为null。如果没有指定Looper,默认使用当前线程的Looper,当前线程Looper为null时,抛出异常。mQueue是通过mLooper的myLooper方法获取的。
Handler的工作主要包含消息的发送和接收过程。消息的发送可以通过post的一系列方法以及send的一系列方法来实现,不过最后都是通过send的一系列方法实现的。代码如下:

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

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

可以发现,Handler发送消息的过程不过是向消息队列插入一条消息,MessageQueue的next方法会返回这条消息给Looper,Looper收到消息会交给Handler处理,Handler的dispatchMessage方法就会被调用DispatchMessage的实现:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
Handler消息处理流程图

dispatchMessage会先调用Message的callback接口,在调用Handler的Callback,最后调用handlerMessage方法。

Handler的实际应用


1. Looper和Handler的同步关系

Looper和Handler会有什么同步关系呢?它们之间存在的同步关系跟多线程有关,如果不注意,就容易引起错误。
下面看一个例子:

    class LooperThread extends Thread {
        public Looper myLooper = null;

        @Override
        public void run() { // 假使run在线程2中执行
            Looper.prepare();
            myLooper = Looper.myLooper();
            Looper.loop();
        }
    }
    // 假使下面代码在线程1中运行
    {
        LooperThread thread2 = new LooperThread();
        thread2.start();
        Looper looper = thread2.myLooper;
        Handler thread1Hanlder = new Handler(looper);
        thread1Hanlder.sendEmptyMessage(0);
    }

以上代码的作用:

  • 在线程1中创建线程2,并且线程2通过Looper处理消息。
  • 线程1中得到线程2的Looper,并且根据这个Looper创建一个Handler,这样发送给该Handler的消息将由线程2处理。
    理想是美好的,现实是残酷的。如果我们熟悉多线程,就很容易发现这段代码中存在巨大漏洞,需要注意的是:myLooper的赋值是在线程2的run方法中,而looper的赋值又是在线程1中,这样就可能导致线程2的run函数还没来得及给myLooper赋值,线程1中的looper就取得了myLooper的初值,即looper等于null。
    解决这个问题,只需要在其中加入线程锁就可以了。不过不用我们自己动手,Android已经为这个问题提供了解决方案,那就是HandlerThread。
    HandlerThread可以完美解决myLooper可能为空的问题。直接上代码:
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

HanderThread很简单,使用wait/notifyAll就解决了这个问题。

2. 小心内存泄露

Handler的使用是有可能引起内存泄露的,先看一个例子

public class MainActivity extends Activity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mTextView = new TextView(this);
        mTextView.setText("内存泄露?");
        setContentView(mTextView);
        MyHandler handler = new MyHandler(mTextView);
        handler.sendEmptyMessage(0x11);
    }

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mTextView.setText("有内存泄露");
        }
    };
}

上面的代码如果是在Android Studio中编写,Android Lint会提示可能存在内存泄露,并提供相应的解决方案:

This Handler class should be static or leaks might occur (anonymous android.os.Handler) less... (Ctrl+F1)
Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

这段话的大概意思是:这个Handler应该声明为静态的,否则可能导致内存泄露。当Handler声明为内部类时,他可能持有外部类的引用。如果这时Handler使用一个并非来自主线程的Looper或者MessageQueue时,那就没有问题。否则你需要修改你的Handler。具体步骤:
1. 将Handler声明为静态类,
2. 当你的Handler类需要引用外部类的成员时,使用WeakReference弱引用来获得它们。

具体改造后,就变成了:

    public class MainActivity extends Activity {

        private TextView mTextView;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mTextView = new TextView(this);
            mTextView.setText("有内存泄露");
            setContentView(mTextView);
            UiHandler handler = new UiHandler(mTextView);
            handler.sendEmptyMessage(0x11);
        }

        static class UiHandler extends Handler {
            WeakReference<MainActivity> mActivity;

            UiHandler(MainActivity activity) {
                mActivity = new WeakReference<MainActivity>(activity);
            }

            @Override
            public void handleMessage(Message msg) {
                TextView textView = mActivity.get().mTextView;
                if (textView != null) {
                    textView.setText("无内存泄漏");
                }
            }
        }
    }

为以上代码作以下几点解释:
1、上述Handler的作用,是在无内存泄漏的情况下,为外部Activity的mTextView设置文本信息。
2、静态类不持有外部类的对象,所以外部Activity可以随意被回收,不会因delay的Message持有了Handler的引用,而Handler又持有Activity的引用,导致Activity被关闭后无法被GC回收。多次的打开和关闭,会造成OOM。
3、WeakReference是弱引用类型,我们可以借助弱引用类型对外部非静态变量进行操作,且Handler仅有一条弱引用指向了textView,不会影响textView的回收。

3. IntentService

这是 Service 的子类,它使用工作线程逐一处理所有启动请求。如果您不要求服务同时处理多个请求,这是最好的选择。 您只需实现onHandleIntent()方法即可,该方法会接收每个启动请求的 Intent,使您能够执行后台工作。
以下是 IntentService 的实现示例:

public class HelloIntentService extends IntentService {

  /**
   * A constructor is required, and must call the super IntentService(String)
   * constructor with a name for the worker thread.
   */
  public HelloIntentService() {
      super("HelloIntentService");
  }

  /**
   * The IntentService calls this method from the default worker thread with
   * the intent that started the service. When this method returns, IntentService
   * stops the service, as appropriate.
   */
  @Override
  protected void onHandleIntent(Intent intent) {
      // Normally we would do some work here, like download a file.
      // For our sample, we just sleep for 5 seconds.
      long endTime = System.currentTimeMillis() + 5*1000;
      while (System.currentTimeMillis() < endTime) {
          synchronized (this) {
              try {
                  wait(endTime - System.currentTimeMillis());
              } catch (Exception e) {
              }
          }
      }
  }
}

您只需要一个构造函数和一个 onHandleIntent() 实现即可。
IntentService内部使用Handler来实现,以下提供了Service类实现与IntentService相同功能的代码:

public class HelloService extends Service {
  private Looper mServiceLooper;
  private ServiceHandler mServiceHandler;

  // Handler that receives messages from the thread
  private final class ServiceHandler extends Handler {
      public ServiceHandler(Looper looper) {
          super(looper);
      }
      @Override
      public void handleMessage(Message msg) {
          // Normally we would do some work here, like download a file.
          // For our sample, we just sleep for 5 seconds.
          long endTime = System.currentTimeMillis() + 5*1000;
          while (System.currentTimeMillis() < endTime) {
              synchronized (this) {
                  try {
                      wait(endTime - System.currentTimeMillis());
                  } catch (Exception e) {
                  }
              }
          }
          // Stop the service using the startId, so that we don't stop
          // the service in the middle of handling another job
          stopSelf(msg.arg1);
      }
  }

  @Override
  public void onCreate() {
    // Start up the thread running the service.  Note that we create a
    // separate thread because the service normally runs in the process's
    // main thread, which we don't want to block.  We also make it
    // background priority so CPU-intensive work will not disrupt our UI.
    HandlerThread thread = new HandlerThread("ServiceStartArguments",
            Process.THREAD_PRIORITY_BACKGROUND);
    thread.start();

    // Get the HandlerThread's Looper and use it for our Handler
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

      // For each start request, send a message to start a job and deliver the
      // start ID so we know which request we're stopping when we finish the job
      Message msg = mServiceHandler.obtainMessage();
      msg.arg1 = startId;
      mServiceHandler.sendMessage(msg);

      // If we get killed, after returning from here, restart
      return START_STICKY;
  }

  @Override
  public IBinder onBind(Intent intent) {
      // We don't provide binding, so return null
      return null;
  }

  @Override
  public void onDestroy() {
    Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
  }
}

正如您所见,与使用 IntentService 相比,这需要执行更多工作。
但是,因为是由您自己处理对 onStartCommand() 的每个调用,因此可以同时执行多个请求。此示例并未这样做,但如果您希望如此,则可为每个请求创建一个新线程,然后立即运行这些线程(而不是等待上一个请求完成)。

推荐

《我的个人博客》

参考资料

《Android开发艺术探究》
《深入理解Android卷1》
服务 | Android Developers
Android自定义无内存泄露的Handler
自定义无内存泄漏的Handler内部类

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

推荐阅读更多精彩内容