带着问题学习 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阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容