Android消息机制Handler源码分析

系列文章

前言

Android和Windows一样都采用了消息机制,从开发角度说,Handler是Android消息机制的上层接口,开发过程中和Handler交互即可。Android规范限制我们不能在子线程中更新UI,只能在主线程中更新UI,Handler可以轻松的将一个任务切换到Handler所在的线程中去执行。所以,开发过程中Handler通常被用来更新UI,利用Handler将更新UI的操作切换到主线程中执行。

ViewRootImpl对我们的UI操作进行了验证,具体是在checkThread()方法中完成的,如下:

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

ViewRootImpl是所有View的根,WindowView通过ViewRootImpl建立联系,checkThread()方法校验当前线程是否是ViewRootImpl被创建时所在的线程。具体请参考Android中子线程真的不能更新UI吗?

说明:本文中的源码都是基于Android-25版本。

Handler

Android的消息机制主要是指Handler的运行机制,Handler的运行需要MessageQueueLooper支撑,MessageQueue是消息队列,内部存储了一组消息,而Looper则是消息循环,Looper会无限循环查看是否有新消息,如果有的话处理消息,没有的话就一直等待。

Looper中有一个特殊概念ThreadLocalThreadLocal并不是线程,作用是在每个线程中存储数据,可以让不同线程保存在同一个ThreadLocal中的对象数据互不干扰。ThreadLocal的工作原理请参考:Android的消息机制之ThreadLocal的工作原理

工作过程

Handler在创建过程时会采用当前线程的Looper来构造消息循环系统,那么Handler内部是如何获取到Looper呢?通过ThreadLocal可以很轻松的获取每个线程的Looper,但是线程默认是没有Looper的,要使用Handler就必须为线程创建Looper。而主线程(UI)线程即ActivityThread被创建时就初始化了Looper,所以我们在主线程中可以默认使用Looper。

Handler的默认构造方法最终会通过以下构造方法实现:

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

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从中可以看到,如果当前线程没有Looper,则会抛出异常,而mLooper是由Looper.myLooper()方法返回的,代码如下:

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

Looper.myLooper()就是返回了保存在ThreadLocal中的Looper对象。
Handler创建完毕后,Handler和Looper以及MessageQueue便协同工作,构成一个消息系统。通过Handler的post系列方法将一个Runnable投递到Looper,或者send系列方法发送一个消息到Looper中,通过调用MessageQueue的enqueueMessage()方法将消息放入消息队列中,此时Looper发现有新消息到来,处理此消息。最终消息中的Runnable或者Handler的handleMessage()方法就会被调用,这样就又将任务切换到创建Handler的线程中去了。过程如图所示:

Handler工作过程

工作原理

Handler的工作主要包括发送和接收消息。

发送消息

上面提到,发送消息是通过一系列post或者send方法实现,post系列的方法最终都是通过send系列方法实现的。Handler中有关发送消息的send系列方法源码如下:

public final boolean sendMessage(Message msg){
    return sendMessageDelayed(msg, 0);
}
 
public final boolean sendEmptyMessage(int what){
    return sendEmptyMessageDelayed(what, 0);
}

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}

public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
}

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

public final boolean sendMessageAtFrontOfQueue(Message msg) {
    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, 0);
}
    
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到除了sendMessageAtFrontOfQueue()方法,其余最终都是调用sendMessageAtTime()方法,在sendMessageAtTime()方法中调用enqueueMessage()方法,再调用MessageQueueenqueueMessage()方法,发送消息就是向消息队列中插入一条消息,MessageQueue的next()方法将这条消息返回给Looper。

处理消息

Looper接收到消息后处理,并最终交由Handler的dispatchMessage()处理,源码如下:

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

首先检查Messagecallback,不为null则调用handleCallback(),源码如下:

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

Messagecallback是一个Runnable对象,就是Handler中post方法的Runnable参数。post方法源码如下:

public final boolean post(Runnable r){
    return  sendMessageDelayed(getPostMessage(r), 0);
}

post方法利用sendMessageDelayed()方法发送消息,其中又调用了getPostMessage()方法,其源码如下:

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

该方法传入一个Runnable参数,得到一个Message对象,将Runnable参数赋值给Message对象的callback字段。

然后检查mCallback,不为null则调用mCallbackhandleMessage()Callback是一个接口,定义如下:

/**
 * Callback interface you can use when instantiating a Handler to avoid
 * having to implement your own subclass of Handler.
 *
 * @param msg A {@link android.os.Message Message} object
 * @return True if no further handling is desired
 */
public interface Callback {
    public boolean handleMessage(Message msg);
}

使用Callback创建Handler不需要派生一个Handler子类,通常开发过程中都是派生Handler子类并重写其handleMessage()方法来处理具体消息,Callback提供了另一种使用Handler方法。
最后不符合以上两点时,就调用Handler的handleMessage()来处理消息。

Looper

Looper在Android消息机制中扮演消息循环的角色,它会不断从MessageQueue中查看是否有新消息,如果有新消息就立即处理,否则一直阻塞在那里。

创建Looper

首先看一下构造方法,在构造方法中它会创建一个MessageQueue消息队列,然后将当前线程的对象保存起来。

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

Handler的工作需要Looper,没有Looper的线程就会报错,那么如何为线程创建Looper呢?通过Looper.prepare()即可为当前线程创建一个Looper,同时会将Looper保存在ThreadLocal中,Handler的构造函数便是从ThreadLocal中获取Looper对象,从代码如下:

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

接着通过Looper.loop()来开启消息循环。为一个子线程创建Looper和Handler的代码如下所示:

new Thread("Thread#2"){
    @Override
    public void run(){
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    };
}.start();

Looper除了prepare()方法外,还提供了prepareMainLooper()方法,这个方法主要是给主线程即ActivityThread创建Looper使用的,本质也是通过prepare()方法来实现的,源码如下:

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。ActivityThread中的main()方法调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法。因此我们应用程序的主线程中会始终存在一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。

由于主线程Looper比较特殊,所以Looper提供了一个getMainLooper()方法,通过它可以在任何地方获取主线程的Looper。

Looper退出

Looper也提供了退出方法,quit()quitSafely(),区别在于quit()是直接退出Looper,而quitSafely()只是设定一个退出标记,然后把消息队列中的已有消息全部处理完毕后再退出。quit()方法最终调用的是MessageQueue中的removeAllMessagesLocked()方法,quitSafely()最终调用的是MessageQueue中的removeAllFutureMessagesLocked()方法,源码如下:

private void removeAllMessagesLocked() {
        Message p = mMessages;
        while (p != null) {
            Message n = p.next;
            p.recycleUnchecked();
            p = n;
        }
        mMessages = null;
    }

private void removeAllFutureMessagesLocked() {
    final long now = SystemClock.uptimeMillis();
    Message p = mMessages;
    if (p != null) {
        if (p.when > now) {
            removeAllMessagesLocked();
        } 
        else {
            Message n;
            for (;;) {
                n = p.next;
                if (n == null) {
                    return;
                }
                if (n.when > now) {
                    break;
                }
                p = n;
            }
            p.next = null;
            do {
                p = n;
                n = p.next;
                p.recycleUnchecked();
            } while (n != null);
    }
}

Looper退出后,通过Handler发送的消息会失败,Handler的send方法会返回false,在子线程中如果为其手动创建了Looper,消息处理完毕后应该调用quit()方法来终止消息循环,否则这个子线程会一直处于等待状态,退出Looper以后,这个子线程就会立刻终止,因此建议不需要的时候终止Looper。

消息循环

Looper通过loop()方法开启消息循环,源码实现如下:

public static void loop() {

    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    
        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                + Long.toHexString(ident) + " to 0x"
                + Long.toHexString(newIdent) + " while dispatching to "
                + msg.target.getClass().getName() + " "
                + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();
    }
}

loop()方法是一个死循环,唯一跳出循环的方法是MessageQueuenext()方法返回了null。当Looper的退出方法被调用时,通知消息队列消息退出,当消息队列被标记为退出状态时,它的next()方法就会返回null。也就是说Looper必须退出,否则loop()方法会无限循环下去。
loop()方法会调用MessageQueue中的next()方法来获取新消息,而next()是一个阻塞操作,没有新消息时会一直阻塞在那里,同理导致loop()阻塞,如果MessageQueuenext()方法返回了新消息,Looper便会处理这条消息,通过以下语句执行:

msg.target.dispatchMessage(msg);

这里的msg.target是发送这条消息的Handler对象,这样通过Handler的dispatchMessage()方法来处理消息,dispatchMessage()是在创建Handler时所使用的Looper中执行的,这样就将代码逻辑切换到指定的线程中了。

Message & MessageQueue

Message在线程之间传递消息,Messagewhat字段,是消息类型字段,arg1arg2携带一些整型数据,obj字段携带一个object对象。
MessageQueue是消息队列,存放所有通过Handler发送的消息。消息一直存放在消息队列中,等待被处理。每个线程只会有一个MessageQueue对象。
MessageQueue主要包含两个操作,插入和读取,读取伴随着删除操作。euqueueMessage()的作用是往消息队列中插入一条消息,next()的作用是从消息队列中读取一条消息并移除。尽管MessageQueue叫做消息队列,但是其内部是通过单链表的数据结构来维护消息列表。euqueueMessage()的源码如下:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        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;
}

从源码中可以分析出,当前消息队列的头结点为空或待插入的消息需要被立即执行时,就让当前消息成为消息队列的新的头结点,并且如果消息队列处于阻塞状态,则将消息队列唤醒;否则则按消息等待被执行的时间顺序,将待插入消息插入消息队列中,最后如果需要唤醒消息队列,则通过native方法nativeWake()来唤醒消息队列。
下面再看看next()方法的源码:

Message next() {
   // Return here if the message loop has already quit and been disposed.
   // This can happen if the application tries to restart a looper after quit
   // which is not supported.
   final long ptr = mPtr;
   if (ptr == 0) {
       return null;
   }

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

       nativePollOnce(ptr, nextPollTimeoutMillis);

       synchronized (this) {
           // Try to retrieve the next message.  Return if found.
           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 (DEBUG) Log.v(TAG, "Returning message: " + msg);
                   msg.markInUse();
                   return msg;
               }
           } else {
               // No more messages.
               nextPollTimeoutMillis = -1;
           }

           // Process the quit message now that all pending messages have been handled.
           if (mQuitting) {
               dispose();
               return null;
           }

           ...
       }

       ...
   }
}

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

Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

问题描述

Android程序的入口点可以认为是ActivityThread类的main()方法,源码如下:

public static void main(String[] args) {

    ...
    
    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();
    
    ...
}

可以看到Looper开启了消息循环,loop()方法是一个死循环,但是并没有看见有相关代码为这个死循环准备了一个新线程去运转,但是主线程却并不会因为Looper.loop()中的这个死循环卡死,这是为什么呢?

原因

从上述代码我们可以发现,首先调用prepareMainLooper()方法为主线程创建一个消息队列;其次,生成一个ActivityThread对象,在其初始化代码中会创建一个H(Handler)对象,即ActivityThread.H,它内部定义了一组消息类型,主要包含了四大组件的启动和停止过程,如下所示:

private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    public static final int PAUSE_ACTIVITY          = 101;
    public static final int PAUSE_ACTIVITY_FINISHING= 102;
    public static final int STOP_ACTIVITY_SHOW      = 103;
    public static final int STOP_ACTIVITY_HIDE      = 104;
    public static final int SHOW_WINDOW             = 105;
    public static final int HIDE_WINDOW             = 106;
    public static final int RESUME_ACTIVITY         = 107;
    public static final int SEND_RESULT             = 108;
    public static final int DESTROY_ACTIVITY        = 109;
    public static final int BIND_APPLICATION        = 110;
    public static final int EXIT_APPLICATION        = 111;
    public static final int NEW_INTENT              = 112;
    public static final int RECEIVER                = 113;
    public static final int CREATE_SERVICE          = 114;
    public static final int SERVICE_ARGS            = 115;
    public static final int STOP_SERVICE            = 116;
    
    ...    
}

thread.attach(false)生成了一个AppplicationThread(Binder)对象,ActivityThread通过ApplicationThreadAMS进行进程间通信,AMS以进程间通信方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,Binder负责接远程ActivityManagerService(AMS)的IPC调用,用于接收系统服务AMS发来的消息,收到消息后,通过Handler将消息发送到消息队列,UI主线程会异步的从消息队列中取出消息并执行操作;最后,UI主线程调用Looper.loop()进入消息循环。

Android的Handler消息机制涉及到Linux的pipe/epoll机制,MessageQueue没有消息时,阻塞在那里,主线程会释放CPU进入休眠状态,通过Linux系统的epoll机制中的epoll_wait函数进行等待,当有新消息来临时,往pipe(管道)写入端写入消息来唤醒主线程,其实就是一个生产消费模型。
(还有一个疑问,那就是怎么响应点击事件呢?或者说对通常的GUI模型,如windows都是怎么实现的呢?生产消费模型?)

以上部分摘自柯元旦<Android内核剖析>

Handler的其它用法

除了通过编写子线程并结合Handler发送消息改变UI外,Handler还有一些其他用法。

View中的post()方法

代码如下所示:

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

发现View的post()方法就是调用了Handler中的post()方法,前文已经说过Handler的post()方法了,不再多解释。

Activity中的runOnUiThread()方法

代码如下所示:

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

先判断当前线程是否是UI线程,如果不是则调用Handler的post()方法,否则就直接调用Runnable对象的run()方法。

参考信息

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

推荐阅读更多精彩内容

  • 孩子不管大小,都会有些自己的小心思小想法耍点小心眼的。 比如三年级的孩子,老师要求背一篇课文,逐段...
    寒夜中的点点星光阅读 513评论 0 0
  • 放慢自己的脚步,一点一点慢慢来。 一张图,画了四遍线稿。 特地,慢慢地画,去感受这个过程。 速度慢下来,一笔一笔地...
    七酱的手绘日常阅读 561评论 4 10
  • 雨水反复清洗 一棵静默的梨树 让那一切关于寒冷的回忆 跌进尘埃 褐色的枝头聚集了一些可以呐喊的力量 那是明媚 那是...
    快雪神刀阅读 631评论 0 2
  • 霞光万里,皓月当空。 如斯美景,何人与共? 待几时,你我携手归途。
    JOJO是啾啾阅读 248评论 0 0