Android消息机制Handler学习记录

注:本来只是想整理成Handler部分的面试题及答案拿来背诵的,哪知一看源码就没停下来,变成了大篇文章...本文根据源码对Handler进行分析,但由于能力有限,仍然和市面上的大多Handler消息机制的文章相似——即,仅探讨表层逻辑,对于其中的Binder、native、epoll之类的并不涉及。所以如果你已经看多了表层解析,就可以右上角啦~。

而且有个大问题:我把自己当作一个旁观者再读了一遍这篇文章发现自己讲的并不清楚,所以各位还是自己去看一遍源码比较好。至少我自己感觉在看了这一遍源码之后,印象就很深刻了,估计能记很久。

Android的消息机制主要是指Handler的运行机制。

1. 什么是Handler


HandlerAndroid消息机制的上层接口。我们可以使用Handler将一个任务切换到某个指定的线程中去执行。在Android 中,Handler主要是为了解决在子线程中无法访问UI的矛盾。

2. 为什么子线程无法访问UI


因为AndroidUI是线程不安全的,UI负责与用户的交互,如果多线程中并发访问UI会导致UI处于不可控的状态。其次也不能选择使用加锁处理,首先因为加锁会阻塞某些线程的执行,降低UI访问的效率,其次加锁会让UI访问的逻辑变得复杂。

//该方法用于检查当前是否是UI线程

void checkThread() {

  if (mThread != Thread.currentThread()) {

      throw new CalledFromWrongThreadException(

          "Only the original thread that created a view hierarchy can touch its views.");

  }}

3. 为什么使用Handler


UI线程直接与用户交互,如果在UI线程处理耗时操作,用户将无法继续执行其他UI操作,用户体验很差。其次Android规定,如果任意一个Acitivity没有响应5秒钟以上就会弹出ANR窗口。因此我们可以使用Handler将耗时操作放到子线程中去执行以避免上述情况。

4. 怎么使用Handler


首先

private Handler handler = new Handler(){

  @Override  public void handleMessage(Message msg) {  

}};

然后再子线程中处理完耗时操作后构建一个Message对象,然后使用handler.post(Runnable r)或者handler.sendMessage(Message msg)等方法传入Message即可。post()方法最后也是使用sendMessage()方法,因为Runnable对象会被转化成一个Message

这里有一点需要注意的是,由于我们是使用Message来进行消息传递,所以如果每次都new 一个Message就很可能会产生大量用了一次就不再用的Message对象,消耗有限的内存资源。针对这种情况,HandlerobtainMessage()方法内部采用了享元模式。我们应该优先使用HandlerobtainMessage()方法构建Message对象。

5. Handler中的享元模式


享元模式适用于可能存在大量重复对象的场景。具体的定义忘了。一般经典的享元模式会使用Map作为对象容器。不过在Message中,是通过一个单链表来实现的,具体的说就是sPool,而sPool就是一个Message

那为啥sPool这个Message对象会是一个单链表呢?见如下代码:

//Message.java中

Message next; //这不就是自定义单链表的中next指向下一个节点嘛

调用obtainMessage()最终会调用Messageobtain()方法,obtain()方法先会判断sPool如果为空,就返回一个新的Message对象,不为空就返回sPool,然后sPool指向其next

6. Handler的内存泄露问题及其处理方法


成因:

Handler的内存泄露问题大致是这样形成的:就是主线程中创建的Handler会和主线程Looper的消息队列MessageQueue相关联,于是MessageQueue中的每个Message都会持有一个Handler的引用。而由于非静态内部类和匿名类都会隐式的持有它们所属外部类的引用,所以就导致了Message持有Handler引用,Handler持有其外部类的引用的引用链。在Message被处理前,这条引用链会阻止垃圾回收器的回收,于是发生内存泄露。

验证:

可以使用 handler.postDelayed()方法进行验证。

解决:

解决方法:1. 使用内部静态类构造Handlr,因为内部静态类不会持有外部类的引用。但是这样的话就无法操控外部Activity的对象,于是还需要增加一个队Activity的弱引用。2. 在Activity退出的时候调用Looper的quit和quitSafely方法,以及使用对应的handler.removeCallback方法,这些方法会最后会执行 msg.target = null 操作,让msg不再持有handler引用。

7. 讲解一下Handler的运行机制


private Handler myHandler = null;

new Thread("handler线程"){

@Override

public void run() {

    Looper.prepare();

    myHandler = new MyHandler();

    Looper.loop();   

    }

}.start();

private class MyHandler extends Handler{

    @Override

    public void handleMessage(Message msg) {

        super.handleMessage(msg);

    }

}

Handler运行机制的主要参与者是HandlerMessageQueueLooper以及MessageThreadLocal 这5个角色。

(感觉有些复杂)

这5个角色关系有点复杂,

-----------------------------ThreadLocal-------------------------------------

先讲相对简单的ThreadLocalThreadLocal是在Looper中使用的,ThreadLocal的作用是我们可以通过它把数据存储到指定的线程,并且只有在指定的线程才能获取数据。

而其中把数据储存到指定的线程这个操作在Looper.prepare()中进行,方法调用的最后会调用ThreadLocalset方法传入一个Looper对象(Looper对象包含了MessageQueue和当前线程)。

//Looper.java中

//全局唯一一份的ThreadLocal,将会在每个ThreadLocalMap中作为key

static final ThreadLocal sThreadLocal = new ThreadLocal<~>();

private static void prepare(boolean quitAllowed) {

    if (sThreadLocal.get()!= null) {

        throw new RuntimeException("Only one Looper may be created per thread");

    }

    //这个sThreadLocal是static final 的.

    sThreadLocal.set(new Looper(quitAllowed));

}

private Looper(boolean quitAllowed) {

    mQueue = new MessageQueue(quitAllowed);

    mThread = Thread.currentThread();

}

然后我们可以看到ThreadLocal中的set(T value)中的createMap()方法中传入的是Threadnew Looper(),但并不是用Thread作为key呦,如下。

//ThreadLocal.java中

public void set(T value) {

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map != null)

        map.set(this, value);

    else{

        createMap(t, value);

    }

}

void createMap(Thread t, T firstValue) {

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

}

//Thread.java中

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap(this , firstValue)中,this就是那个全局唯一份的sThrealLocal。所以我们很清楚了,在每个线程中Looper.prapare()的时候都会给当前线程传入一个ThreadLocalMap,这个Mapkey其实不重要,因为所有线程中的ThreadLocal都指向同一个对象,就是Looper.java中的static final ThreadLocal sThreadLocal…因为key不变,所以如果多次调用Looper.prepare()的话,是会覆盖的。

上面讲了如何通过ThreadLocal把数据存储到指定线程,至于后部分的只有在指定的线程才能获取数据也就一目了然,通过当前线程t.threadLocals获取线程中的变量副本,然后取出值即可。在源码中的体现如下:

//Looper.java中

public static @Nullable Looper myLooper() {

    return sThreadLocal.get();//注意这个myLooper()。返回了一个Looper对象。

}

//ThreadLocal.java中

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

            //下面这个value。。。之前存进去的那个Looper对象了

            T result = (T)e.value;         

            return result;

        }

    }

    return setInitialValue();//这个就是在map中没有的时候返回个null

}

ThreadLocalMap getMap(Thread t) {

    return t.threadLocals;

}

简单的讲,每个线程都有个存储变量的副本,互相隔离。

那么这个特性对我们的Handler机制有什么影响呢?从上面可以了解到每个Thread其实都对应了一个Looper,而每个Looper都对有一个MessageQueue

具体有什么影响需要先看一看Handler的构造方法才能看出来。

-----------------------------Handler--------------------------------

先来看一个错误示例:

new Thread(){

    public void run(){

        Handler handler =new Handler();

    };

}.start();//正确的方法是handler前面用Looper.prepare(),后面用Looper.loop()

上述代码会抛出“Can’t create handler inside thread that has not called Looper.prepare()”异常,这是因为在Handler构造方法中所有规定,如下:

//Hnadler中

public Handler(Callback callback, boolean async) {

    …省略代码

    mLooper = Looper.myLooper(); //最终调用sThreadLocal.get()

    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中

public static @Nullable Looper myLooper() {

    return sThreadLocal.get();//如果没有获取到Map,则返回null

}

ThreadLocal一节的分析中可以看到sThreadLocal.get()方法在没有获取到ThreadLocalMap的情况下,会返回一个null

因为其下一步 mQueue = mLooper.mQueue;需要将Looper中的MessageQueue拿出来进行操作,所以必然之前就需要有一个Looper对象,所以就必然要先Looper.prepare()啦。

至于在主线程中我们为啥可以直接new一个Handler呢?因为系统已经帮我们做好了这一步,代码如下:

//ActivityThread中

public static void main(String[] args) {

    …代码省略

    Looper.prepareMainLooper();

    …代码省略

    ActivityThread thread = new ActivityThread();

    thread.attach(false);

    if (sMainThreadHandler == null) {

        sMainThreadHandler = thread.getHandler();

    }

    …代码省略

    Looper.loop();

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

}

扯远了咳咳。

从前面的Handler的构造方法中就可以看到,Handler其实使用的是Looper对象中的MessageQueue,而Looper对象又是和Thread对应的。

那么我们就知道了,Handler的各种发消息的操作send() , post() 等,最后都会把数据Message传递到“那个线程(假定先命名为Thread_1)”中。

知道这个我们就可以知道为啥handler可以在不同的线程中给自己的线程的线程发消息啦。

对于使用者来说,使用Handler的步骤并不复杂,定义一个Handler,然后调用handler.sendMessage()发送消息,然后在handleMessage()中对消息进行处理即可。

那么我们就从发送消息开始一步步了解Handler的消息机制。

Handler的发消息有很多种发送消息的接口,如下

postAtTime(Runnable r, Object token, long uptimeMillis)

postAtTime(Runnable r, long uptimeMillis)

post(Runnable r)

postDelayed(Runnable r, long delayMillis)

sendMessageAtFrontOfQueue(Message msg)

sendMessageAtTime(Message msg, long uptimeMillis)

sendMessageDelayed(Message msg, long delayMillis)

sendEmptyMessageDelayed(int what, long delayMillis)

sendMessage(Message msg)

sendEmptyMessage(int what)

postAtFrontOfQueue(Runnable r)

……等方法

但是这么多方法,最后都是调用了enqueueMessage()方法

//Handler.java中

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {

    msg.target = this; //这个地方要注意,在这里msg持有了handler引用

    if (mAsynchronous) {

        msg.setAsynchronous(true);

    }

    return queue.enqueueMessage(msg, uptimeMillis);

}

最后的queue还有印象么,就是之前说的,从Looper对象中存储的那个MessageQueue

所以最后调用了MessageQueue.java中的enqueueMessage(Message msg, long when)方法,如下:

//MessageQueue.java

boolean enqueueMessage(Message msg, long when) {

    if (msg.target == null) {

        throw new IllegalArgumentException("Message must have a target.");

    }

    …省略代码

    synchronized (this) {

        if (mQuitting) { 

            …省略代码

            msg.recycle();

            return false;

        }

      …省略代码

        msg.when = when;

        Message p = mMessages;

        if (p == null || when == 0 || when < p.when) {

            msg.next = p;

            mMessages = msg;

        } else {

            …省略代码

            Message prev;

            for (;;) {

                prev = p;

                p = p.next;

                if (p == null || when < p.when) {

                    break;

                }

            …省略代码

            }

            msg.next = p; // invariant: p == prev.next

            prev.next = msg;

        }

    …省略代码

    }

    return true;

}

上述的代码不扣细节的话,就一件事,把消息插入mMessages中。

sMessages明明是Message为何能插入呢,因为在1.5的享元模式中就介绍了Message可以构成单链表。所以虽然喊MessageQueue.java是消息队列,但是其实它自身并不是什么链表队列结构,它的作用是对Message这个单链表进行各种操作。

所以Handler发送消息的最终结果,就是把消息插入到了Message的单链表中。而且这个Message持有Handler的引用。

那么现在消息插入到消息链表中了,怎么把msg拿出来呢?这就到了我们的LooperMessageQueue环节了。

还记得之前的Looper.prapare() – new Handler() – Looper.loop() 三部曲吗。拿消息的操作就在Looper.loop()中,当然也在MessageQueue中,具体的流程如下。

-------------------Looper和MessageQueue和Message----------------------

话不多说,上代码:

//Looper.java中

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

    Binder.clearCallingIdentity();

    final long ident = Binder.clearCallingIdentity();

    …省略代码

    for (;;) {

        Message msg = queue.next(); // might block

        if (msg == null) {

            return;

        }

        …省略代码

        try {

            msg.target.dispatchMessage(msg);

        } finally {

          …省略代码

        }

        …省略代码

        final long newIdent = Binder.clearCallingIdentity();

        …省略代码

    }

}

我留下了几行Binder的代码,我看不懂,但是觉得这个和Binder有关的代码肯定有很大的深意o(╯□╰ )o ,去除掉了其他我不懂的代码之后,剩下的代码的逻辑很清晰:先是获取到当前线程的MessageQueue queue = me.mQueue,然后用一个死循环不断使用queue.next()方法得到msg。如果有msg,就调用handler.disptachMessage(msg)进行消息分发。要问Handler在哪儿?就是msg.target哇。

这里先不看disptachMessage(msg)的过程,我先看看for(;;)这个死循环里,只有一处可以返回。就是当queue.next()返回null的时候,并且所有需要处理的msg都是从queue.next()拿来的。所以看样子,我们的Looper.loop()中最主要的工作是由queue.next()来完成的。

让我们深入queue.next()看看:

Message next() {

    …省略代码

    for (;;) {

        if (nextPollTimeoutMillis != 0) {

            Binder.flushPendingCommands();

        }

        nativePollOnce(ptr, nextPollTimeoutMillis);//这里甚至有个native方法!!!肯定有深意

        synchronized (this) {

            Message prevMsg = null;

            Message msg = mMessages;

            if (msg != null && msg.target == null) {

                do {

                    prevMsg = msg;

                    msg = msg.next;

                } while (msg != null && !msg.isAsynchronous());

            }

            if (msg != null) {

                mBlocked = false;

                if (prevMsg != null) {

                    prevMsg.next = msg.next;

                } else {

                    mMessages = msg.next;

                }

                msg.next = null;

                return msg;

            }

            if (mQuitting) {

                dispose();

                return null;

            }

      }

}

这里面我省略了一堆我看不懂的代码,剩下的代码逻辑也不难。这里又有一个for(;;)死循环,这个死循环是不断的mMessages中拿出msg,还记得我们之前见过的mMessages吗,之前看到mMessages是插入节点,这次见到就是要从中取出节点了,取出的操作就是各种.next

这里重点关注两个return的地方,记住只有两个return。第一个是返回msg,第二是mQuitting == true 的时候返回null

那么返回null会怎么样呢?还记得Looper.loop()中当msg == null的时候会发生什么吗,当msg = null的时候,Looper.loop()退出for(;;)死循环。既然退出死循环了,loop()方法也就执行完毕,也就意味着这个Looper不再获取新消息,handler再发什么消息都没有用了。

甚至我们可以看到当mQuittiong == true的时候,handler试图enqueueMessage() 发送的消息直接就被回收了:

//MessageQueue.java中

boolean enqueueMessage(Message msg, long when) {

  …省略代码

    synchronized (this) {

        if (mQuitting) {

            IllegalStateException e = new IllegalStateException(

                    msg.target + " sending message to a Handler on a dead thread");

            msg.recycle();

            return false;

        }

    …省略代码

}

上面的msg.recycle()和享元模式有关,大概就是这个msg直接给你抹除所有信息然后插入到sPool中。当然sPool这个单链表也不是给你无限插入的,最大数量是MAX_POOL_SIZE = 50。可以自己看下源码哈。

从上面看出mQuitting的威力巨大,那么哪儿设置这个mQuitting的值的呢,见源码:

//MessageQueue.java中

void quit(boolean safe) {

    …省略代码

    synchronized (this) {

        if (mQuitting) {

            return;

        }

        mQuitting = true;

        …省略代码

    }

}

而这个quit( boolean safe )则是在Looper中被调用,如下:

//Looper.java中

public void quit() {

    mQueue.quit(false);

}

public void quitSafely() {

    mQueue.quit(true);

}

怎么样,是不是感觉很简单。这两者的区别在于如何处理那些延迟发送的msg,看最后调用的方法名就清楚了:removeAllMessagesLocked()removeAllFutureMessagesLocked()

两个方法里都用到了msg.recycleUnchecked(),也就是说这些msg对象最后会被回收进sPool里。所以理解为啥官方建议使用handler.obtainMessage()方法了吧——因为很多地方的msg都会被回收进sPool。而且直接获取现有msg肯定也比每次新new一个message资源消耗少,效率更高。

上面讲了MessageQueue.next()返回null的特殊情况,那么如果正常返回了一个msg呢?正常的话则会调用Looper.loop() 中的msg.target.dispatchMessage(msg)方法,这个方法调用的是Handler中的dispatchMessage()方法,如下

//Handler.java中

public void dispatchMessage(Message msg) {

    if (msg.callback != null) {

        handleCallback(msg);

    } else {

        if (mCallback != null) {

            if (mCallback.handleMessage(msg)) {

                return;

            }

        }

        handleMessage(msg);

    }

}

(假装看不到handleCallback(msg)^_^因为看着有点复杂啊)最后都是调用了handleMessage(msg),就是我们最最最最最最最最开始的——

private Handler handler = new Handler(){

//重写handleMessage方法

    @Override

    public void handleMessage(Message msg) {

        super.handleMessage(msg);

    }

};

——这里面的handleMessage(msg)了,然后就可以拿msg去玩耍啦。

还记得这handleMessage()最初哪儿调用的吗?是msg.target.dispatchMessage(msg),而这个msg最终的源头又是存在于t.sThreadLocal这个ThreadLocalMap里的,所以在handleMessage(msg){}方法也就是执行在t这个线程中啦。

于是就达成了——handler这个在外行走可以在不同的线程发送消息,最后消息都是传送到hanler最初创建的Thread里进行处理——这个功能。

1.8 总结

总结几个要点:

1. ThreadLocal是个好东西,它的存在让Thread、Handler、MessageQueue绑定了起来,于是让handler可以做到不管在哪个线程发消息,最终消息都会传送到原来的Thread里。ThreadLocalMap也是功不可没。

2. LooperMessageQueue共同协作来让整个消息队列动起来,不断的取出新消息

3. Message更多是用来构造成单链表,不管是MessageQueue中的sMessages还是自己的享元模式中的sPool。所以最好使用handler.obtainMessage()来获取Message对象。

4. 如果上述文章哪儿有问题或者哪儿看不懂请轻轻的喷,最好是留个言我可以改善改善O(∩_∩)O

于是,一圈分析结束~ ~ ~ ~ ~ ~ ~啦啦啦啦啦。

秋豆麻袋,那这个msg.callback的运行机制呢?还有那些看着名字奇奇怪怪的方法呢?还有为什么UI线程死循环竟然不阻塞呢?还有….你不是说那个内存泄露有问题吗?你的最终结论呢?

O(╯□╰)o

Msg.callback实在不想看了,内存泄露问题等我再问问大神,那些奇奇怪怪名字的方法如果书上木有我也研究不出啥。

但是至于为什么UI线程死循环不阻塞这个问题,问的好!

看我再秀(Baidu)一波!

其实我在源码中看到了一堆Binder、native、epoll字眼,所以我一直怀疑不阻塞这个东西应该和Binder机制甚至和Linux的底层机制管道epoll有关。

...

算了直接放链接点我你可以发现新大陆

epoll模型

当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。

所有的ui操作都通过handler来发消息操作。

比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。

这部分留待以后如果有机会研究Linux再详细看吧。和Binder机制一样,如果要研究的话,就太深入了。

本文参考《Android开发艺术探索》 + 《Android源码设计模式解析与实战》+ AOSP以及感谢1Offer童鞋。

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

推荐阅读更多精彩内容