你真的了解Android的Handler机制吗?

在Android系统中,Handler机制应用较广,尤其是在App层面,基本每个App都会用到。使用的场景主要是向主线程发送事件,更新UI。但大家真的了解Handler机制吗?看一下面的几个问题是否可以回答:

a.Handler是如何实现多个线程之前事件传递的?

b.Handler、Message、MessageQueue、Looper相互之间的数量比是多少,都是1:1吗?

c.每个变量运行的线程是那个?

d.ThreadLocal是怎么回事,在Handler机制中起什么作用?

能准确回答上述问题,说明对Handler机制的理解已相当到位了。

一、下面论述一下Handler机制各类的实现原理,从而揭露内部的工作流程。(源码环境为Android8.0)

1. 先来看Handler类:

1.1 构造方法:

Handler的构造方法总共有如下几个,最终实现为下面两个方法:

public Handler(Callback callback, boolean async) {

    if (FIND_POTENTIAL_LEAKS) {

        final Class 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;

}

public Handler(Looper looper, Callback callback, boolean async) {

    mLooper = looper;

    mQueue = looper.mQueue;

    mCallback = callback;

    mAsynchronous = async;

}

最终都调至Handler(Callback callback, boolean async)和Handler(Looper looper, Callback callback, boolean async),看一下源码可知,主要为初始化几个变量:mLooper、mQueue、mCallback,mAsynchronous,前三个变量非常重要,有以下几个结论:

a.一个Handler中含有一个Looper

b.Handler中持有的MessageQueue源于Looper中,与Looper中该变量行为一致

c.一个Handler中含有一个Callback

Handler中还有如下几个重要的方法,提供了Handler对外的能力:

1.2 obtainMessage方法,对外提供一个获取Message缓存池中Message的方法,避免应用产生过多的Message对象。根据参数的不同,重载有多个该方法,实现原理都一样,以其中之一分析一下:

//obtainMessage@Handler.java

public final Message obtainMessage()

{

    return Message.obtain(this);

}

//obtain@Message.java

public static Message obtain(Handler h) {

    Message m = obtain();

    m.target = h;

    return m;

}

// obtain@Message.java

public static Message obtain() {

    synchronized (sPoolSync) {

        if (sPool != null) {

            Message m = sPool;

            sPool = m.next;

            m.next = null;

            m.flags = 0; // clear in-use flag

            sPoolSize--;

            return m;

        }

    }

    return new Message();

}

sPool提供复用功能的实现,其存储结构为链表,通过sPoolSize的大小及SPool头部指针位置作到这一点,感兴趣的同学可以研究下

1.3 post方法、sendMessage方法,Handler提供了多个post方法,按参数不同,提供了诸如指定时间、延时的功能,基本功能是一样的,以其中某一个方法说一下其实现:

public final boolean post(Runnable r)

{

  return  sendMessageDelayed(getPostMessage(r), 0);

}

private static Message getPostMessage(Runnable r) {

    Message m = Message.obtain();

    m.callback = r;

    return m;

}

从getPostMessage方法可以看出,传入的Runnable被Message所持有。

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

}

由上面可知如下结论:

a.post方法的实现是与sendMessage一致的,最终都调至sendMessage中

b.干了个什么事呢:得到一个message,将它的callback设为传入的callback(如果有的话),将它的target设为自身,然后调用MessageQueue的enqueueMessage处理该Message,实现原理讲到MessageQueue再行讨论。

1.4 dispatchMessage方法,该方法从Handler发起流程中难以看到在什么地方调用了,是一个消息处理时的回调,先讲一下该方法的逻辑,后面再说调用时机

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

}

从上面的逻辑中看到如下结论:

消费该Hanlder事件总共有三种可能的途径,分别为Message中的callback、Handler所持有的Callback对象、Handler的自身的handleMessage方法(一般写法是构造匿名子类,实现该方法),三种途径的优先级从高到低。

Handler在该机制中的作用为:

a.对外提供可复用的Message

b.对外提供发消息的接口

c.对外提供回调的实现,三种方式

2.Looper类

在主线程中使用Handler,看不到Looper,因为系统已经帮我们自动生成了Looper,在工作线程中使用Handler则必须先调用Looper的prepare生成Looper,否则会报错,原因如下:

测试代码:

Thread t =  new Thread(new Runnable() {

    @Override

    public void run() {

        Handler handler = new Handler();

    }

});

t.start();

运行后报错:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

Handler最终会调至(1.1中第一个构造方法)

if (mLooper == null) {

        throw new RuntimeException(

            "Can't create handler inside thread that has not called Looper.prepare()");

    }

因此正确的写法为:

Thread t =  new Thread(new Runnable() {

    @Override

    public void run() {

        Looper.prepare();

        Handler handler = new Handler();

        // ignore

        Looper.loop();

    }

});

2.1 prepare方法:

public static void prepare() {

    prepare(true);

}

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

}

sThreadLocal变量定义如下:

static final ThreadLocal sThreadLocal = new ThreadLocal();

使用ThreadLocal类对Looper进行了一层封装。

像c++的内联函数一样,此处我们直接内联一下ThreadLocal这个类,看一下该类的作用,先从Looper中跳出去一下

--------------------------------此处插入ThreadLocal类----------------------------------

3. ThreadLocal类

ThreadLocal达到的目的是:每一个ThreadLocal的变量在多个线程中都有一份copy,相互间独立存储,互不影响,但变量名只有一个。也就是说对于该变量的使用方来讲,看起来像是只定义了一个变量,但实际上在多线程环境中,有互不影响的、每个线程都有一份的对象存在。如何作到这一点呢,是通过其对外提供的get方法和set方法做到的。

3.1 先来看set方法:

public void set(T value) {

    Thread t = Thread.currentThread();

// 第一步,拿到每个线程所对应的ThreadLocalMap

    ThreadLocalMap map = getMap(t);

// 第二步,将该值存入或更新map的对应位置

    if (map != null)

        map.set(this, value);

    else

        createMap(t, value);

}

ThreadLocalMap getMap(Thread t) {

    return t.threadLocals;

}

void createMap(Thread t, T firstValue) {

    t.threadLocals = new ThreadLocalMap(this, firstValue);

}

// ThreadLocalMap的构造方法

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {

    table = new Entry[INITIAL_CAPACITY];

    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    table[i] = new Entry(firstKey, firstValue);

    size = 1;

    setThreshold(INITIAL_CAPACITY);

}

// set@ThreadLocalMap

private void set(ThreadLocal key, Object value) {

    // We don't use a fast path as with get() because it is at

    // least as common to use set() to create new entries as

    // it is to replace existing ones, in which case, a fast

    // path would fail more often than not.

    Entry[] tab = table;

    int len = tab.length;

    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];

        e != null;

        e = tab[i = nextIndex(i, len)]) {

        ThreadLocal k = e.get();

        if (k == key) {

            e.value = value;

            return;

        }

        if (k == null) {

            replaceStaleEntry(key, value, i);

            return;

        }

    }

    tab[i] = new Entry(key, value);

    int sz = ++size;

    if (!cleanSomeSlots(i, sz) && sz >= threshold)

        rehash();

}

上述逻辑第一步拿到每个线程的ThreadLocalMap对象

// threadlocals#Thread

ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说每个线程维护一个ThreadLocalMap来存储该线程所用到的所有ThreadLocal变量,ThreadLocalMap的实现原理与HashMap基本一致,之前讨论过HashMap的实现原理,感兴趣的同学对比一下源码就能明白。

第二步中将传入的值存入ThreadLocalMap的对应位置,该位置的key由ThreadLocal本身的hash值去运算得到的。从上面的代码中可以看出。每一个线程持有一个ThreadLocalMap,set的value将和ThreadLocal自身组成key-value结构存在于该Map中。

3.2 get方法

public T get() {

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map != null) {

        ThreadLocalMap.Entry e = map.getEntry(this);

        if (e != null) {

            @SuppressWarnings("unchecked")

            T result = (T)e.value;

            return result;

        }

    }

    return setInitialValue();

}

private T setInitialValue() {

    T value = initialValue();

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map != null)

        map.set(this, value);

    else

        createMap(t, value);

    return value;

}

理解了set方法之后,get方法就较为简单了,反向拿到存入的值即可,ThreadLocal对象本身在多个线程中只有一份,但通过其set和get,可以得到其包装对象在每个线程中只有一份。

--------------------------ThreadLocal分析完毕,回至Looper----------------------------------

Looper的prepare为一个静态方法,目的是可以生成每个线程都有一份的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));

}

2.2 Looper构造方法:

private Looper(boolean quitAllowed) {

    mQueue = new MessageQueue(quitAllowed);

    mThread = Thread.currentThread();

}

持有一个MessageQueue对象,持有当前的线程对象

2.3 loop方法

public static void loop() {

// 第一步:myLooper的实现看下方,得到当前线程的Looper

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

// 第二步:定义一个无限循环体,遍历MessageQueue,取其中的有效Message,进行处理,处理语名为:msg.target.dispatchMessage(msg);该方法请看上面1.4

    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 slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {

            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));

        }

        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();

        final long end;

        try {

            msg.target.dispatchMessage(msg);

            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();

        } finally {

            if (traceTag != 0) {

                Trace.traceEnd(traceTag);

            }

        }

        if (slowDispatchThresholdMs > 0) {

            final long time = end - start;

            if (time > slowDispatchThresholdMs) {

                Slog.w(TAG, "Dispatch took " + time + "ms on "

                        + Thread.currentThread().getName() + ", h=" +

                        msg.target + " cb=" + msg.callback + " msg=" + msg.what);

            }

        }

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

        }

// 此处实现Message的循环利用

        msg.recycleUnchecked();

    }

}

public static @Nullable Looper myLooper() {

    return sThreadLocal.get();

}

在loop的处理中,有一点需重点关注,loop在线程中是一个无限循环,跳出该循环的条件为MessageQueue的next返回值为空,看一下4.2的next方法实现,返回空依赖于mQuitting变量,也就是说在线程中不需要一直等待事件时,要把MessageQueue该变量置为true,设置入口为quit@Looper

2.4 quit方法

public void quit() {

    mQueue.quit(false);

}

//quit@MessageQueue

void quit(boolean safe) {

    if (!mQuitAllowed) {

        throw new IllegalStateException("Main thread not allowed to quit.");

    }

    synchronized (this) {

        if (mQuitting) {

            return;

        }

        mQuitting = true;

        if (safe) {

            removeAllFutureMessagesLocked();

        } else {

            removeAllMessagesLocked();

        }

        // We can assume mPtr != 0 because mQuitting was previously false.

        nativeWake(mPtr);

    }

}

总结一下:Looper在Handler机制中起的作用是:

a.提供每个线程唯一的实例

b.触发MessageQueue的循环遍历

4. MessageQueue类

顾名思义,该类需提供三个能力:

a.Message队列存储

b.Message入队

c.Message出队

Message队列以链表形式存储,其头部指针存放于mMessages

4.1 enqueueMessage方法,该方法用于Message入队

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;

// 以下为入队的关键代码,找到链表的队尾,放入该Message

        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;

// 注意,入队的时候,已经按时间次序将Message插入队列中的合适位置

            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;

}

需要注意,在上面的入队处理中,要按Handler传入的执行时间插入

enqueueMessage调用的入口请查看1.3

4.2 next方法,该方法用于MessageQueue出队

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;

// 第二步:取到最早入队的有效的Message

            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;

            }

            // If first time idle, then get the number of idlers to run.

            // Idle handles only run if the queue is empty or if the first message

            // in the queue (possibly a barrier) is due to be handled in the future.

            if (pendingIdleHandlerCount < 0

                    && (mMessages == null || now < mMessages.when)) {

                pendingIdleHandlerCount = mIdleHandlers.size();

            }

            if (pendingIdleHandlerCount <= 0) {

                // No idle handlers to run.  Loop and wait some more.

                mBlocked = true;

                continue;

            }

            if (mPendingIdleHandlers == null) {

                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];

            }

            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

        }

        // Run the idle handlers.

        // We only ever reach this code block during the first iteration.

        for (int i = 0; i < pendingIdleHandlerCount; i++) {

            final IdleHandler idler = mPendingIdleHandlers[i];

            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;

            try {

                keep = idler.queueIdle();

            } catch (Throwable t) {

                Log.wtf(TAG, "IdleHandler threw exception", t);

            }

            if (!keep) {

                synchronized (this) {

                    mIdleHandlers.remove(idler);

                }

            }

        }

        // Reset the idle handler count to 0 so we do not run them again.

        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered

        // so go back and look again for a pending message without waiting.

        nextPollTimeoutMillis = 0;

    }

}

next方法的调用入口位于2.3中

小结一下,通过上面的代码讲解,可以得到以下几个结论:

1. 首先Handler对象本身位于构造Handler的当前线程,他的事件要发往那个线程,取决于构造方法传 的Looper变量

2. MessageQueue与Looper位于Handler所要发往的工作线程中,Looper持有MessageQueue对象,MessageQueue中看不到Looper

3. Looper与线程为1对1的关系,也就是说一个线程只有一个Looper,一个Looper只服务于一个线程,达到这个目的是通过ThreadLocal包装Looper实现的

4. 从线程的角度讲,Handler对象自身与线程没有任何关系,它是通过Looper持有的MessageQueue实现向Looper所服务的线程事件队列(MessageQueue)插入事件(Message)的

看一下文章开头所说的几个问题是不是已经有了答案:

a.Handler是如何实现多个线程之前事件传递的?

请看上面总结的第4点

b.Handler、Message、MessageQueue、Looper相互之间的数量比是多少,都是1:1吗?

Handler n--1 Looper(以主线程为例,主线程的Looper可以定义非常多Handler)

Looper持有一个MessageQueue(不可逆,看上面总结的第2点)

MessageQueue 1--n Message

c.每个变量运行的线程是那个?

Handler位于定义线程、Looper、MessageQueue位于工作线程,线程的轮询依赖于Looper.loop

d.ThreadLocal是怎么回事,在Handler机制中起什么作用?

可查看代码分析部分的第3部分

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

推荐阅读更多精彩内容